schnorr.mx raw
1 package secp256k1
2
3 import (
4 "smesh.lol/web/common/crypto/sha256"
5 )
6
7 // VerifySchnorr verifies a BIP-340 Schnorr signature.
8 // pubkey: 32-byte x-only public key
9 // msg: 32-byte message hash
10 // sig: 64-byte signature (R.x || s)
11 func VerifySchnorr(pubkey, msg [32]byte, sig [64]byte) bool {
12 // Parse public key.
13 px := feFromBytes(pubkey[:])
14 P, ok := LiftX(px)
15 if !ok {
16 return false
17 }
18
19 // Parse signature.
20 rx := feFromBytes(sig[:32])
21 s := scalarFromBytes(sig[32:])
22
23 // e = tagged_hash("BIP0340/challenge", R.x || P.x || msg) mod n
24 e := computeChallenge(sig[:32], pubkey[:], msg[:])
25
26 // R = s*G - e*P
27 sG := ScalarBaseMult(s)
28 eP := ScalarMult(pointFromAffine(P), e).toAffine()
29
30 // s*G - e*P = s*G + (-e)*P
31 negEP := AffinePoint{eP.X, feNeg(eP.Y)}
32 R := pointAdd(pointFromAffine(sG), pointFromAffine(negEP)).toAffine()
33
34 // Check R.x == rx and R.y is even.
35 if feIsZero(R.X) && feIsZero(R.Y) {
36 return false
37 }
38 if R.X != rx {
39 return false
40 }
41 if !feIsEven(R.Y) {
42 return false
43 }
44
45 return true
46 }
47
48 // SignSchnorr creates a BIP-340 Schnorr signature.
49 // seckey: 32-byte secret key
50 // msg: 32-byte message hash
51 // auxRand: 32-byte auxiliary randomness
52 // Returns zero [64]byte and false on failure.
53 func SignSchnorr(seckey, msg, auxRand [32]byte) (sig [64]byte, ok bool) {
54 sk := scalarFromBytes(seckey[:])
55 if scalarIsZero(sk) {
56 return sig, false
57 }
58
59 // P = sk * G
60 P := ScalarBaseMult(sk)
61
62 // If P.y is odd, negate the secret key.
63 d := sk
64 if !feIsEven(P.Y) {
65 d = scalarNeg(d)
66 }
67
68 pkBytes := feToBytes(P.X)
69
70 // t = d XOR tagged_hash("BIP0340/aux", auxRand)
71 var t [32]byte
72 aux := taggedHash("BIP0340/aux", auxRand[:])
73 dBytes := feToBytes(d)
74 for i := 0; i < 32; i++ {
75 t[i] = dBytes[i] ^ aux[i]
76 }
77
78 // k' = tagged_hash("BIP0340/nonce", t || pkBytes || msg) mod n
79 var nonceInput [96]byte
80 copy(nonceInput[:32], t[:])
81 copy(nonceInput[32:64], pkBytes[:])
82 copy(nonceInput[64:96], msg[:])
83 kHash := taggedHash("BIP0340/nonce", nonceInput[:])
84 k := scalarFromBytes(kHash[:])
85
86 // Reduce k mod n.
87 if feCmp(k, curveN) >= 0 {
88 k = scalarSub(k, curveN)
89 }
90 if scalarIsZero(k) {
91 return sig, false
92 }
93
94 // R = k * G
95 R := ScalarBaseMult(k)
96
97 // If R.y is odd, negate k.
98 if !feIsEven(R.Y) {
99 k = scalarNeg(k)
100 }
101
102 rxBytes := feToBytes(R.X)
103
104 // e = tagged_hash("BIP0340/challenge", R.x || P.x || msg) mod n
105 e := computeChallenge(rxBytes[:], pkBytes[:], msg[:])
106
107 // sig = R.x || (k + e*d) mod n
108 s := scalarAdd(k, scalarMul(e, d))
109 sBytes := feToBytes(s)
110
111 copy(sig[:32], rxBytes[:])
112 copy(sig[32:], sBytes[:])
113 return sig, true
114 }
115
116 // PubKeyFromSecKey derives the x-only public key from a secret key.
117 // Returns zero [32]byte and false on failure.
118 func PubKeyFromSecKey(seckey [32]byte) (pubkey [32]byte, ok bool) {
119 sk := scalarFromBytes(seckey[:])
120 if scalarIsZero(sk) {
121 return pubkey, false
122 }
123 P := ScalarBaseMult(sk)
124 return feToBytes(P.X), true
125 }
126
127 // ECDH computes the shared secret x-coordinate: seckey * pubkey.
128 // pubkey is a 32-byte x-only public key.
129 // Returns the 32-byte x-coordinate and true on success.
130 func ECDH(seckey, pubkey [32]byte) ([32]byte, bool) {
131 sk := scalarFromBytes(seckey[:])
132 if scalarIsZero(sk) {
133 return [32]byte{}, false
134 }
135 px := feFromBytes(pubkey[:])
136 P, ok := LiftX(px)
137 if !ok {
138 return [32]byte{}, false
139 }
140 R := ScalarMult(pointFromAffine(P), sk).toAffine()
141 if feIsZero(R.X) && feIsZero(R.Y) {
142 return [32]byte{}, false
143 }
144 return feToBytes(R.X), true
145 }
146
147 // tagged_hash(tag, msg) = SHA256(SHA256(tag) || SHA256(tag) || msg)
148 func taggedHash(tag string, msg []byte) [32]byte {
149 tagHash := sha256.Sum([]byte(tag))
150 data := []byte{:0:64+len(msg)}
151 data = append(data, tagHash[:]...)
152 data = append(data, tagHash[:]...)
153 data = append(data, msg...)
154 return sha256.Sum(data)
155 }
156
157 func computeChallenge(rx, px, msg []byte) Fe {
158 input := []byte{:0:96}
159 input = append(input, rx...)
160 input = append(input, px...)
161 input = append(input, msg...)
162 hash := taggedHash("BIP0340/challenge", input)
163 e := scalarFromBytes(hash[:])
164 // Reduce mod n.
165 if feCmp(e, curveN) >= 0 {
166 e = scalarSub(e, curveN)
167 }
168 return e
169 }
170