schnorr_wasm.go raw
1 //go:build js || wasm || tinygo || wasm32
2
3 package p256k1
4
5 import (
6 "errors"
7 )
8
9 // BIP-340 nonce tag
10 var bip340NonceTag = []byte("BIP0340/nonce")
11
12 // BIP-340 aux tag
13 var bip340AuxTag = []byte("BIP0340/aux")
14
15 // BIP-340 challenge tag
16 var bip340ChallengeTag = []byte("BIP0340/challenge")
17
18 // Zero mask for BIP-340 nonce generation (precomputed TaggedHash("BIP0340/aux", 0x0000...00))
19 var zeroMask = [32]byte{
20 84, 241, 105, 207, 201, 226, 229, 114,
21 116, 128, 68, 31, 144, 186, 37, 196,
22 136, 244, 97, 199, 11, 94, 165, 220,
23 170, 247, 175, 105, 39, 10, 165, 20,
24 }
25
26 // NonceFunctionBIP340 implements BIP-340 nonce generation
27 func NonceFunctionBIP340(nonce32 []byte, msg []byte, key32 []byte, xonlyPk32 []byte, auxRand32 []byte) error {
28 if len(nonce32) != 32 {
29 return errors.New("nonce32 must be 32 bytes")
30 }
31 if len(key32) != 32 {
32 return errors.New("key32 must be 32 bytes")
33 }
34 if len(xonlyPk32) != 32 {
35 return errors.New("xonlyPk32 must be 32 bytes")
36 }
37
38 // Mask key with aux random data
39 var maskedKey [32]byte
40 if auxRand32 != nil && len(auxRand32) == 32 {
41 // TaggedHash("BIP0340/aux", aux_rand32)
42 auxHash := TaggedHash(bip340AuxTag, auxRand32)
43 for i := 0; i < 32; i++ {
44 maskedKey[i] = key32[i] ^ auxHash[i]
45 }
46 } else {
47 // Use zero mask
48 for i := 0; i < 32; i++ {
49 maskedKey[i] = key32[i] ^ zeroMask[i]
50 }
51 }
52
53 // TaggedHash("BIP0340/nonce", masked_key || xonly_pk || msg)
54 // Use fixed-size buffer to avoid heap allocation (32 + 32 + 32 = 96 bytes)
55 var nonceInput [96]byte
56 copy(nonceInput[0:32], maskedKey[:])
57 copy(nonceInput[32:64], xonlyPk32)
58 copy(nonceInput[64:96], msg)
59
60 nonceHash := TaggedHash(bip340NonceTag, nonceInput[:])
61 copy(nonce32, nonceHash[:])
62
63 // Clear sensitive data
64 for i := range maskedKey {
65 maskedKey[i] = 0
66 }
67
68 return nil
69 }
70
71 // SchnorrSignature represents a 64-byte Schnorr signature (r || s)
72 type SchnorrSignature [64]byte
73
74 // SchnorrSign creates a Schnorr signature following BIP-340
75 func SchnorrSign(sig64 []byte, msg32 []byte, keypair *KeyPair, auxRand32 []byte) error {
76 if len(sig64) != 64 {
77 return errors.New("signature must be 64 bytes")
78 }
79 if len(msg32) != 32 {
80 return errors.New("message must be 32 bytes")
81 }
82 if keypair == nil {
83 return errors.New("keypair cannot be nil")
84 }
85
86 // Load secret key
87 var sk Scalar
88 if !sk.setB32Seckey(keypair.seckey[:]) {
89 return errors.New("invalid secret key")
90 }
91
92 // Load public key
93 var pk GroupElementAffine
94 pk.fromBytes(keypair.pubkey.data[:])
95 if pk.isInfinity() {
96 return errors.New("invalid public key")
97 }
98
99 // Negate secret key if Y coordinate is odd (BIP-340 requires even Y)
100 pk.y.normalize()
101 var skBytes [32]byte
102 sk.getB32(skBytes[:])
103
104 if pk.y.isOdd() {
105 sk.negate(&sk)
106 sk.getB32(skBytes[:]) // Update skBytes with negated key
107 // Update pk to have even Y
108 pk.negate(&pk)
109 }
110
111 // Get x-only public key (X coordinate)
112 var pkX [32]byte
113 pk.x.normalize()
114 pk.x.getB32(pkX[:])
115
116 // Generate nonce (use the possibly-negated secret key)
117 var nonce32 [32]byte
118 if err := NonceFunctionBIP340(nonce32[:], msg32, skBytes[:], pkX[:], auxRand32); err != nil {
119 return err
120 }
121
122 // Parse nonce scalar
123 var k Scalar
124 if !k.setB32Seckey(nonce32[:]) {
125 return errors.New("nonce generation failed")
126 }
127
128 if k.isZero() {
129 return errors.New("nonce is zero")
130 }
131
132 // Compute R = k * G
133 var rj GroupElementJacobian
134 EcmultGen(&rj, &k)
135
136 // Convert to affine
137 var r GroupElementAffine
138 r.setGEJ(&rj)
139 r.y.normalize()
140
141 // If R.y is odd, negate k
142 if r.y.isOdd() {
143 k.negate(&k)
144 // Recompute R with negated k
145 EcmultGen(&rj, &k)
146 r.setGEJ(&rj)
147 }
148
149 // Extract r = X(R)
150 r.x.normalize()
151 var r32 [32]byte
152 r.x.getB32(r32[:])
153 copy(sig64[:32], r32[:])
154
155 // Compute challenge e = TaggedHash("BIP0340/challenge", r || pk || msg)
156 // Use fixed-size buffer to avoid heap allocation (32 + 32 + 32 = 96 bytes)
157 var challengeInput [96]byte
158 copy(challengeInput[0:32], r32[:])
159 copy(challengeInput[32:64], pkX[:])
160 copy(challengeInput[64:96], msg32)
161
162 challengeHash := TaggedHash(bip340ChallengeTag, challengeInput[:])
163 var e Scalar
164 e.setB32(challengeHash[:])
165
166 // Compute s = k + e * sk
167 var s Scalar
168 s.mul(&e, &sk)
169 s.add(&s, &k)
170
171 // Serialize s
172 var s32 [32]byte
173 s.getB32(s32[:])
174 copy(sig64[32:], s32[:])
175
176 // Clear sensitive data
177 sk.clear()
178 k.clear()
179 e.clear()
180 s.clear()
181 for i := range nonce32 {
182 nonce32[i] = 0
183 }
184 for i := range pkX {
185 pkX[i] = 0
186 }
187 for i := range skBytes {
188 skBytes[i] = 0
189 }
190 rj.clear()
191 r.clear()
192
193 return nil
194 }
195
196 // SchnorrVerify verifies a BIP-340 Schnorr signature
197 func SchnorrVerify(sig64 []byte, msg32 []byte, xonlyPubkey *XOnlyPubkey) bool {
198 if len(sig64) != 64 {
199 return false
200 }
201 if len(msg32) != 32 {
202 return false
203 }
204 if xonlyPubkey == nil {
205 return false
206 }
207
208 ctx := getSchnorrVerifyContext()
209
210 // Convert x-only pubkey to secp256k1_xonly_pubkey format
211 var secp_xonly secp256k1_xonly_pubkey
212 copy(secp_xonly.data[:], xonlyPubkey.data[:])
213
214 result := secp256k1_schnorrsig_verify(ctx, sig64, msg32, len(msg32), &secp_xonly)
215 return result == 1
216 }
217