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