aboutsummaryrefslogtreecommitdiffstats
path: root/internal/secp256k1/schnorr.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/secp256k1/schnorr.go')
-rw-r--r--internal/secp256k1/schnorr.go236
1 files changed, 236 insertions, 0 deletions
diff --git a/internal/secp256k1/schnorr.go b/internal/secp256k1/schnorr.go
new file mode 100644
index 0000000..ae7fa75
--- /dev/null
+++ b/internal/secp256k1/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}