aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--schnorr.go236
-rw-r--r--schnorr_test.go242
2 files changed, 478 insertions, 0 deletions
diff --git a/schnorr.go b/schnorr.go
new file mode 100644
index 0000000..a9ff3b7
--- /dev/null
+++ b/schnorr.go
@@ -0,0 +1,236 @@
1package secp256k1
2
3import (
4 "crypto/sha256"
5 "fmt"
6 "math/big"
7)
8
9// Signature represents a Schnorr signature (r, s)
10// r is the x-coordinate of R (32 bytes)
11// s is the scalar response (32 bytes)
12type Signature struct {
13 R *big.Int // x-coordinate of the nonce point
14 S *big.Int // the response scalar
15}
16
17// TaggedHash computes SHA256(SHA256(tag) || SHA256(tag) || msg)
18// This is BIP-340's domain separation technique
19func TaggedHash(tag string, data ...[]byte) []byte {
20 tagHash := sha256.Sum256([]byte(tag))
21 h := sha256.New()
22 h.Write(tagHash[:])
23 h.Write(tagHash[:])
24 for _, d := range data {
25 h.Write(d)
26 }
27 return h.Sum(nil)
28}
29
30// liftX recovers a point from just its x-coordinate
31// Returns the point with even y (BIP-340 convention)
32func liftX(x *big.Int) (*Point, error) {
33 // Check x is in valid range
34 if x.Sign() < 0 || x.Cmp(P) >= 0 {
35 return nil, fmt.Errorf("x out of range")
36 }
37
38 // Compute y² = x³ + 7
39 xFe := NewFieldElement(x)
40 ySquared := xFe.Square().Mul(xFe).Add(curveB)
41
42 // Compute y = sqrt(y²) mod p
43 // For secp256k1, sqrt(a) = a^((p+1)/4) mod p
44 exp := new(big.Int).Add(P, big.NewInt(1))
45 exp.Div(exp, big.NewInt(4))
46 y := new(big.Int).Exp(ySquared.value, exp, P)
47
48 // Verify it's actually a square root
49 ySquaredCheck := new(big.Int).Mul(y, y)
50 ySquaredCheck.Mod(ySquaredCheck, P)
51 if ySquaredCheck.Cmp(ySquared.value) != 0 {
52 return nil, fmt.Errorf("x is not on the curve")
53 }
54
55 // BIP-340: use the even y
56 if y.Bit(0) == 1 {
57 y.Sub(P, y)
58 }
59
60 return &Point{
61 x: NewFieldElement(x),
62 y: NewFieldElement(y),
63 infinity: false,
64 }, nil
65}
66
67// hasEvenY returns true if the point's y-coordinate is even
68func hasEvenY(p *Point) bool {
69 if p.infinity {
70 return false
71 }
72 return p.y.value.Bit(0) == 0
73}
74
75// xOnlyBytes returns the 32-byte x-coordinate of a public key
76func (pub *PublicKey) XOnlyBytes() []byte {
77 result := make([]byte, 32)
78 xBytes := pub.Point.x.value.Bytes()
79 copy(result[32-len(xBytes):], xBytes)
80 return result
81}
82
83// Sign creates a Schnorr signature for a message
84// Follows BIP-340 specification
85// aux is optional auxiliary randomness (32 bytes); nil uses zeros
86func Sign(priv *PrivateKey, message []byte, aux ...[]byte) (*Signature, error) {
87 // Get the public key point
88 P := G.ScalarMul(priv.D)
89
90 // BIP-340: if P.y is odd, negate the private key
91 d := new(big.Int).Set(priv.D)
92 if !hasEvenY(P) {
93 d.Sub(N, d)
94 P = P.Negate()
95 }
96
97 // Serialize public key x-coordinate (32 bytes)
98 pBytes := make([]byte, 32)
99 pxBytes := P.x.value.Bytes()
100 copy(pBytes[32-len(pxBytes):], pxBytes)
101
102 // BIP-340 nonce generation:
103 // t = d XOR tagged_hash("BIP0340/aux", aux)
104 // k = tagged_hash("BIP0340/nonce", t || P || m)
105 // For deterministic signing, use aux = 32 zero bytes
106 dBytes := make([]byte, 32)
107 dBytesRaw := d.Bytes()
108 copy(dBytes[32-len(dBytesRaw):], dBytesRaw)
109
110 // Use provided aux or default to 32 zero bytes
111 var auxBytes []byte
112 if len(aux) > 0 && len(aux[0]) == 32 {
113 auxBytes = aux[0]
114 } else {
115 auxBytes = make([]byte, 32)
116 }
117 auxHash := TaggedHash("BIP0340/aux", auxBytes)
118
119 t := make([]byte, 32)
120 for i := 0; i < 32; i++ {
121 t[i] = dBytes[i] ^ auxHash[i]
122 }
123
124 kHash := TaggedHash("BIP0340/nonce", t, pBytes, message)
125 k := new(big.Int).SetBytes(kHash)
126 k.Mod(k, N)
127
128 // k cannot be zero (extremely unlikely)
129 if k.Sign() == 0 {
130 return nil, fmt.Errorf("nonce is zero")
131 }
132
133 // R = k * G
134 R := G.ScalarMul(k)
135
136 // BIP-340: if R.y is odd, negate k
137 if !hasEvenY(R) {
138 k.Sub(N, k)
139 R = R.Negate()
140 }
141
142 // Serialize R.x (32 bytes)
143 rBytes := make([]byte, 32)
144 rxBytes := R.x.value.Bytes()
145 copy(rBytes[32-len(rxBytes):], rxBytes)
146
147 // Compute challenge e = hash(R.x || P.x || m)
148 eHash := TaggedHash("BIP0340/challenge", rBytes, pBytes, message)
149 e := new(big.Int).SetBytes(eHash)
150 e.Mod(e, N)
151
152 // Compute s = k + e * d (mod N)
153 s := new(big.Int).Mul(e, d)
154 s.Add(s, k)
155 s.Mod(s, N)
156
157 return &Signature{
158 R: R.x.value,
159 S: s,
160 }, nil
161}
162
163// Verify checks if a Schnorr signature is valid
164// Follows BIP-340 specification
165func Verify(pub *PublicKey, message []byte, sig *Signature) bool {
166 // Check signature values are in range
167 if sig.R.Sign() < 0 || sig.R.Cmp(P) >= 0 {
168 return false
169 }
170 if sig.S.Sign() < 0 || sig.S.Cmp(N) >= 0 {
171 return false
172 }
173
174 // Lift R from x-coordinate
175 R, err := liftX(sig.R)
176 if err != nil {
177 return false
178 }
179
180 // Get public key with even y
181 P := pub.Point
182 if !hasEvenY(P) {
183 P = P.Negate()
184 }
185
186 // Serialize for hashing
187 rBytes := make([]byte, 32)
188 rxBytes := sig.R.Bytes()
189 copy(rBytes[32-len(rxBytes):], rxBytes)
190
191 pBytes := make([]byte, 32)
192 pxBytes := P.x.value.Bytes()
193 copy(pBytes[32-len(pxBytes):], pxBytes)
194
195 // Compute challenge e = hash(R.x || P.x || m)
196 eHash := TaggedHash("BIP0340/challenge", rBytes, pBytes, message)
197 e := new(big.Int).SetBytes(eHash)
198 e.Mod(e, N)
199
200 // Verify: s*G == R + e*P
201 sG := G.ScalarMul(sig.S)
202 eP := P.ScalarMul(e)
203 expected := R.Add(eP)
204
205 return sG.Equal(expected)
206}
207
208// Bytes returns the signature as 64 bytes (r || s)
209func (sig *Signature) Bytes() []byte {
210 result := make([]byte, 64)
211
212 rBytes := sig.R.Bytes()
213 sBytes := sig.S.Bytes()
214
215 copy(result[32-len(rBytes):32], rBytes)
216 copy(result[64-len(sBytes):64], sBytes)
217
218 return result
219}
220
221// SignatureFromBytes parses a 64-byte signature
222func SignatureFromBytes(b []byte) (*Signature, error) {
223 if len(b) != 64 {
224 return nil, fmt.Errorf("signature must be 64 bytes")
225 }
226
227 r := new(big.Int).SetBytes(b[:32])
228 s := new(big.Int).SetBytes(b[32:])
229
230 return &Signature{R: r, S: s}, nil
231}
232
233// Hex returns the signature as a 128-character hex string
234func (sig *Signature) Hex() string {
235 return fmt.Sprintf("%x", sig.Bytes())
236}
diff --git a/schnorr_test.go b/schnorr_test.go
new file mode 100644
index 0000000..acd709c
--- /dev/null
+++ b/schnorr_test.go
@@ -0,0 +1,242 @@
1package secp256k1
2
3import (
4 "bytes"
5 "encoding/hex"
6 "math/big"
7 "testing"
8)
9
10func TestTaggedHash(t *testing.T) {
11 // Just verify it produces 32 bytes
12 result := TaggedHash("test", []byte("hello"))
13 if len(result) != 32 {
14 t.Errorf("expected 32 bytes, got %d", len(result))
15 }
16
17 // Same inputs should produce same output
18 result2 := TaggedHash("test", []byte("hello"))
19 if !bytes.Equal(result, result2) {
20 t.Error("tagged hash should be deterministic")
21 }
22
23 // Different tag should produce different output
24 result3 := TaggedHash("other", []byte("hello"))
25 if bytes.Equal(result, result3) {
26 t.Error("different tags should produce different hashes")
27 }
28}
29
30func TestLiftX(t *testing.T) {
31 // Lift G.x should give us G (or its negation with even y)
32 p, err := liftX(Gx)
33 if err != nil {
34 t.Fatalf("failed to lift G.x: %v", err)
35 }
36
37 if !p.IsOnCurve() {
38 t.Error("lifted point should be on curve")
39 }
40
41 // x should match
42 if p.x.value.Cmp(Gx) != 0 {
43 t.Error("lifted x should match input")
44 }
45
46 // y should be even (BIP-340 convention)
47 if !hasEvenY(p) {
48 t.Error("lifted point should have even y")
49 }
50}
51
52func TestLiftXInvalid(t *testing.T) {
53 // x = 0 is not on the curve (0³ + 7 = 7, and 7 has no sqrt mod p)
54 _, err := liftX(big.NewInt(0))
55 if err == nil {
56 t.Error("x=0 should not be on curve")
57 }
58}
59
60func TestSignAndVerify(t *testing.T) {
61 priv, _ := GeneratePrivateKey()
62 pub := priv.PublicKey()
63 message := []byte("hello world")
64
65 sig, err := Sign(priv, message)
66 if err != nil {
67 t.Fatalf("signing failed: %v", err)
68 }
69
70 if !Verify(pub, message, sig) {
71 t.Error("signature should verify")
72 }
73}
74
75func TestSignatureIsDeteministic(t *testing.T) {
76 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
77 message := []byte("test message")
78
79 sig1, _ := Sign(priv, message)
80 sig2, _ := Sign(priv, message)
81
82 if sig1.R.Cmp(sig2.R) != 0 || sig1.S.Cmp(sig2.S) != 0 {
83 t.Error("BIP-340 signing should be deterministic")
84 }
85}
86
87func TestVerifyWrongMessage(t *testing.T) {
88 priv, _ := GeneratePrivateKey()
89 pub := priv.PublicKey()
90
91 sig, _ := Sign(priv, []byte("correct message"))
92
93 if Verify(pub, []byte("wrong message"), sig) {
94 t.Error("signature should not verify with wrong message")
95 }
96}
97
98func TestVerifyWrongPublicKey(t *testing.T) {
99 priv1, _ := GeneratePrivateKey()
100 priv2, _ := GeneratePrivateKey()
101 pub2 := priv2.PublicKey()
102 message := []byte("test")
103
104 sig, _ := Sign(priv1, message)
105
106 if Verify(pub2, message, sig) {
107 t.Error("signature should not verify with wrong public key")
108 }
109}
110
111func TestVerifyTamperedSignature(t *testing.T) {
112 priv, _ := GeneratePrivateKey()
113 pub := priv.PublicKey()
114 message := []byte("test")
115
116 sig, _ := Sign(priv, message)
117
118 // Tamper with s
119 tamperedSig := &Signature{
120 R: sig.R,
121 S: new(big.Int).Add(sig.S, big.NewInt(1)),
122 }
123
124 if Verify(pub, message, tamperedSig) {
125 t.Error("tampered signature should not verify")
126 }
127}
128
129func TestSignatureBytes(t *testing.T) {
130 priv, _ := GeneratePrivateKey()
131 message := []byte("test")
132
133 sig, _ := Sign(priv, message)
134 b := sig.Bytes()
135
136 if len(b) != 64 {
137 t.Errorf("signature should be 64 bytes, got %d", len(b))
138 }
139}
140
141func TestSignatureRoundTrip(t *testing.T) {
142 priv, _ := GeneratePrivateKey()
143 message := []byte("test")
144
145 sig1, _ := Sign(priv, message)
146 b := sig1.Bytes()
147 sig2, err := SignatureFromBytes(b)
148 if err != nil {
149 t.Fatalf("failed to parse signature: %v", err)
150 }
151
152 if sig1.R.Cmp(sig2.R) != 0 || sig1.S.Cmp(sig2.S) != 0 {
153 t.Error("signature should survive round-trip")
154 }
155}
156
157func TestSignatureFromBytesInvalid(t *testing.T) {
158 _, err := SignatureFromBytes(make([]byte, 63))
159 if err == nil {
160 t.Error("should reject wrong-length input")
161 }
162}
163
164// BIP-340 Test Vector 0
165func TestBIP340Vector0(t *testing.T) {
166 privHex := "0000000000000000000000000000000000000000000000000000000000000003"
167 msgHex := "0000000000000000000000000000000000000000000000000000000000000000"
168 expectedSigHex := "e907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0"
169
170 priv, err := NewPrivateKeyFromHex(privHex)
171 if err != nil {
172 t.Fatalf("failed to parse private key: %v", err)
173 }
174
175 msg, _ := hex.DecodeString(msgHex)
176
177 sig, err := Sign(priv, msg)
178 if err != nil {
179 t.Fatalf("signing failed: %v", err)
180 }
181
182 sigHex := sig.Hex()
183 if sigHex != expectedSigHex {
184 t.Errorf("signature mismatch\ngot: %s\nwant: %s", sigHex, expectedSigHex)
185 }
186
187 // Also verify it
188 pub := priv.PublicKey()
189 if !Verify(pub, msg, sig) {
190 t.Error("BIP-340 test vector should verify")
191 }
192}
193
194// BIP-340 Test Vector 1
195func TestBIP340Vector1(t *testing.T) {
196 privHex := "b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef"
197 msgHex := "243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89"
198 auxHex := "0000000000000000000000000000000000000000000000000000000000000001"
199 expectedSigHex := "6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de33418906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a"
200
201 priv, err := NewPrivateKeyFromHex(privHex)
202 if err != nil {
203 t.Fatalf("failed to parse private key: %v", err)
204 }
205
206 msg, _ := hex.DecodeString(msgHex)
207 aux, _ := hex.DecodeString(auxHex)
208
209 sig, err := Sign(priv, msg, aux)
210 if err != nil {
211 t.Fatalf("signing failed: %v", err)
212 }
213
214 sigHex := sig.Hex()
215 if sigHex != expectedSigHex {
216 t.Errorf("signature mismatch\ngot: %s\nwant: %s", sigHex, expectedSigHex)
217 }
218
219 pub := priv.PublicKey()
220 if !Verify(pub, msg, sig) {
221 t.Error("BIP-340 test vector should verify")
222 }
223}
224
225func TestXOnlyBytes(t *testing.T) {
226 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
227 pub := priv.PublicKey()
228
229 xOnly := pub.XOnlyBytes()
230 if len(xOnly) != 32 {
231 t.Errorf("x-only pubkey should be 32 bytes, got %d", len(xOnly))
232 }
233
234 // Should match G.x
235 expectedX := make([]byte, 32)
236 gxBytes := Gx.Bytes()
237 copy(expectedX[32-len(gxBytes):], gxBytes)
238
239 if !bytes.Equal(xOnly, expectedX) {
240 t.Error("x-only bytes should match G.x")
241 }
242}