schnorr.mx raw
1 package secp256k1
2
3 import "crypto/sha256"
4
5 // BIP-340 protocol sizes.
6 const (
7 PubKeyBytesLen = 32
8 SignatureSize = 64
9 )
10
11 // ValidateSecretKey returns true if sk is a valid secp256k1 scalar (32 bytes,
12 // in [1, n-1]). Callers wanting a fresh random key should loop reading bytes
13 // from their preferred source until ValidateSecretKey returns true; this
14 // package intentionally does not import crypto/rand so WASM consumers that
15 // only need verification do not link unused entropy machinery.
16 func ValidateSecretKey(sk []byte) bool {
17 if len(sk) != 32 {
18 return false
19 }
20 k := scalarFromBytes(sk)
21 return !scalarIsZero(k) && feCmp(k, curveN()) < 0
22 }
23
24 // ValidatePubKey returns true if pubkey is a valid 32-byte BIP-340 x-only
25 // public key: correct length, x < p, and the x-coordinate lifts to a point on
26 // the curve.
27 func ValidatePubKey(pubkey []byte) bool {
28 if len(pubkey) != PubKeyBytesLen {
29 return false
30 }
31 x := feFromBytes(pubkey)
32 if feCmp(x, _feP()) >= 0 {
33 return false
34 }
35 _, ok := LiftX(x)
36 return ok
37 }
38
39 // VerifySchnorr verifies a BIP-340 Schnorr signature over a 32-byte message hash.
40 func VerifySchnorr(pubkey, msg [32]byte, sig [64]byte) bool {
41 return VerifySchnorrBytes(pubkey, msg[:], sig)
42 }
43
44 // VerifySchnorrBytes verifies a BIP-340 Schnorr signature over a variable-length
45 // message. BIP-340 (post-2022) defines signing/verification over arbitrary-length
46 // input; Nostr always passes a 32-byte sha256 hash but the spec is general.
47 func VerifySchnorrBytes(pubkey [32]byte, msg []byte, sig [64]byte) bool {
48 px := feFromBytes(pubkey[:])
49 P, ok := LiftX(px)
50 if !ok {
51 return false
52 }
53
54 rx := feFromBytes(sig[:32])
55 s := scalarFromBytes(sig[32:])
56
57 // BIP-340 explicit reject: sig[0:32] >= p or sig[32:64] >= n.
58 if feCmp(rx, _feP()) >= 0 {
59 return false
60 }
61 if feCmp(s, curveN()) >= 0 {
62 return false
63 }
64
65 e := computeChallenge(sig[:32], pubkey[:], msg)
66
67 sG := ScalarBaseMult(s)
68 eP := ScalarMult(pointFromAffine(P), e).toAffine()
69
70 negEP := AffinePoint{eP.X, feNeg(eP.Y)}
71 R := pointAdd(pointFromAffine(sG), pointFromAffine(negEP)).toAffine()
72
73 if feIsZero(R.X) && feIsZero(R.Y) {
74 return false
75 }
76 if R.X != rx {
77 return false
78 }
79 if !feIsEven(R.Y) {
80 return false
81 }
82 return true
83 }
84
85 // SignSchnorr creates a BIP-340 Schnorr signature over a 32-byte message hash.
86 func SignSchnorr(seckey, msg, auxRand [32]byte) (sig [64]byte, ok bool) {
87 return SignSchnorrBytes(seckey, msg[:], auxRand)
88 }
89
90 // SignSchnorrBytes signs a variable-length message per BIP-340.
91 func SignSchnorrBytes(seckey [32]byte, msg []byte, auxRand [32]byte) (sig [64]byte, ok bool) {
92 sk := scalarFromBytes(seckey[:])
93 if scalarIsZero(sk) || feCmp(sk, curveN()) >= 0 {
94 return sig, false
95 }
96
97 P := ScalarBaseMult(sk)
98
99 d := sk
100 if !feIsEven(P.Y) {
101 d = scalarNeg(d)
102 }
103
104 pkBytes := feToBytes(P.X)
105
106 var t [32]byte
107 aux := taggedHash("BIP0340/aux", auxRand[:])
108 dBytes := feToBytes(d)
109 for i := 0; i < 32; i++ {
110 t[i] = dBytes[i] ^ aux[i]
111 }
112
113 // k' = tagged_hash("BIP0340/nonce", t || pkBytes || msg) mod n
114 nonceInput := []byte{:0:64+len(msg)}
115 nonceInput = append(nonceInput, t[:]...)
116 nonceInput = append(nonceInput, pkBytes[:]...)
117 nonceInput = append(nonceInput, msg...)
118 kHash := taggedHash("BIP0340/nonce", nonceInput)
119 k := scalarFromBytes(kHash[:])
120
121 if feCmp(k, curveN()) >= 0 {
122 k = scalarSub(k, curveN())
123 }
124 if scalarIsZero(k) {
125 return sig, false
126 }
127
128 R := ScalarBaseMult(k)
129
130 if !feIsEven(R.Y) {
131 k = scalarNeg(k)
132 }
133
134 rxBytes := feToBytes(R.X)
135
136 e := computeChallenge(rxBytes[:], pkBytes[:], msg)
137
138 s := scalarAdd(k, scalarMul(e, d))
139 sBytes := feToBytes(s)
140
141 copy(sig[:32], rxBytes[:])
142 copy(sig[32:], sBytes[:])
143 return sig, true
144 }
145
146 // PubKeyFromSecKey derives the x-only public key from a secret key.
147 // Returns zero [32]byte and false on failure.
148 func PubKeyFromSecKey(seckey [32]byte) (pubkey [32]byte, ok bool) {
149 sk := scalarFromBytes(seckey[:])
150 if scalarIsZero(sk) {
151 return pubkey, false
152 }
153 P := ScalarBaseMult(sk)
154 return feToBytes(P.X), true
155 }
156
157 // ECDH computes the shared secret x-coordinate: seckey * pubkey.
158 // pubkey is a 32-byte x-only public key.
159 // Returns the 32-byte x-coordinate and true on success.
160 func ECDH(seckey, pubkey [32]byte) ([32]byte, bool) {
161 sk := scalarFromBytes(seckey[:])
162 if scalarIsZero(sk) {
163 return [32]byte{}, false
164 }
165 px := feFromBytes(pubkey[:])
166 P, ok := LiftX(px)
167 if !ok {
168 return [32]byte{}, false
169 }
170 R := ScalarMult(pointFromAffine(P), sk).toAffine()
171 if feIsZero(R.X) && feIsZero(R.Y) {
172 return [32]byte{}, false
173 }
174 return feToBytes(R.X), true
175 }
176
177 // ScalarAddModN returns (a + b) mod n where n is the secp256k1 group order.
178 // a and b are 32-byte big-endian scalars.
179 func ScalarAddModN(a, b [32]byte) ([32]byte, bool) {
180 sa := scalarFromBytes(a[:])
181 sb := scalarFromBytes(b[:])
182 r := scalarAdd(sa, sb)
183 if feCmp(r, curveN()) >= 0 {
184 r = scalarSub(r, curveN())
185 }
186 if scalarIsZero(r) {
187 return [32]byte{}, false
188 }
189 return feToBytes(r), true
190 }
191
192 // CompressedPubKey returns the 33-byte SEC1 compressed public key for a secret key.
193 // Format: 0x02 or 0x03 prefix (even/odd Y) followed by 32-byte X coordinate.
194 func CompressedPubKey(seckey [32]byte) ([33]byte, bool) {
195 sk := scalarFromBytes(seckey[:])
196 if scalarIsZero(sk) {
197 return [33]byte{}, false
198 }
199 P := ScalarBaseMult(sk)
200 xb := feToBytes(P.X)
201 yb := feToBytes(P.Y)
202 var out [33]byte
203 if yb[31]&1 == 0 {
204 out[0] = 0x02
205 } else {
206 out[0] = 0x03
207 }
208 copy(out[1:], xb[:])
209 return out, true
210 }
211
212 // tagged_hash(tag, msg) = SHA256(SHA256(tag) || SHA256(tag) || msg)
213 func taggedHash(tag string, msg []byte) [32]byte {
214 tagHash := sha256.Sum([]byte(tag))
215 data := []byte{:0:64+len(msg)}
216 data = append(data, tagHash[:]...)
217 data = append(data, tagHash[:]...)
218 data = append(data, msg...)
219 return sha256.Sum(data)
220 }
221
222 func computeChallenge(rx, px, msg []byte) Fe {
223 input := []byte{:0:96}
224 input = append(input, rx...)
225 input = append(input, px...)
226 input = append(input, msg...)
227 hash := taggedHash("BIP0340/challenge", input)
228 e := scalarFromBytes(hash[:])
229 // Reduce mod n.
230 if feCmp(e, curveN()) >= 0 {
231 e = scalarSub(e, curveN())
232 }
233 return e
234 }
235