This commit introduces the implementation of Schnorr signatures following BIP-340, including the `SchnorrSign` and `SchnorrVerify` functions. It also adds comprehensive tests to validate the signing and verification processes, ensuring correct functionality with both standard and auxiliary randomness. The tests cover various scenarios, including signature generation, verification, and edge cases, enhancing the robustness of the secp256k1 implementation. Additionally, new files for Schnorr signature operations and tests have been created, contributing to the overall cryptographic capabilities of the library.
290 lines
6.3 KiB
Go
290 lines
6.3 KiB
Go
package p256k1
|
|
|
|
import (
|
|
"errors"
|
|
"unsafe"
|
|
)
|
|
|
|
// BIP-340 nonce tag
|
|
var bip340NonceTag = []byte("BIP0340/nonce")
|
|
|
|
// BIP-340 aux tag
|
|
var bip340AuxTag = []byte("BIP0340/aux")
|
|
|
|
// BIP-340 challenge tag
|
|
var bip340ChallengeTag = []byte("BIP0340/challenge")
|
|
|
|
// Zero mask for BIP-340 nonce generation (precomputed TaggedHash("BIP0340/aux", 0x0000...00))
|
|
var zeroMask = [32]byte{
|
|
84, 241, 105, 207, 201, 226, 229, 114,
|
|
116, 128, 68, 31, 144, 186, 37, 196,
|
|
136, 244, 97, 199, 11, 94, 165, 220,
|
|
170, 247, 175, 105, 39, 10, 165, 20,
|
|
}
|
|
|
|
// NonceFunctionBIP340 implements BIP-340 nonce generation
|
|
func NonceFunctionBIP340(nonce32 []byte, msg []byte, key32 []byte, xonlyPk32 []byte, auxRand32 []byte) error {
|
|
if len(nonce32) != 32 {
|
|
return errors.New("nonce32 must be 32 bytes")
|
|
}
|
|
if len(key32) != 32 {
|
|
return errors.New("key32 must be 32 bytes")
|
|
}
|
|
if len(xonlyPk32) != 32 {
|
|
return errors.New("xonlyPk32 must be 32 bytes")
|
|
}
|
|
|
|
// Mask key with aux random data
|
|
var maskedKey [32]byte
|
|
if auxRand32 != nil && len(auxRand32) == 32 {
|
|
// TaggedHash("BIP0340/aux", aux_rand32)
|
|
auxHash := TaggedHash(bip340AuxTag, auxRand32)
|
|
for i := 0; i < 32; i++ {
|
|
maskedKey[i] = key32[i] ^ auxHash[i]
|
|
}
|
|
} else {
|
|
// Use zero mask
|
|
for i := 0; i < 32; i++ {
|
|
maskedKey[i] = key32[i] ^ zeroMask[i]
|
|
}
|
|
}
|
|
|
|
// TaggedHash("BIP0340/nonce", masked_key || xonly_pk || msg)
|
|
var nonceInput []byte
|
|
nonceInput = append(nonceInput, maskedKey[:]...)
|
|
nonceInput = append(nonceInput, xonlyPk32...)
|
|
nonceInput = append(nonceInput, msg...)
|
|
|
|
nonceHash := TaggedHash(bip340NonceTag, nonceInput)
|
|
copy(nonce32, nonceHash[:])
|
|
|
|
// Clear sensitive data
|
|
memclear(unsafe.Pointer(&maskedKey[0]), 32)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SchnorrSignature represents a 64-byte Schnorr signature (r || s)
|
|
type SchnorrSignature [64]byte
|
|
|
|
// SchnorrSign creates a Schnorr signature following BIP-340
|
|
func SchnorrSign(sig64 []byte, msg32 []byte, keypair *KeyPair, auxRand32 []byte) error {
|
|
if len(sig64) != 64 {
|
|
return errors.New("signature must be 64 bytes")
|
|
}
|
|
if len(msg32) != 32 {
|
|
return errors.New("message must be 32 bytes")
|
|
}
|
|
if keypair == nil {
|
|
return errors.New("keypair cannot be nil")
|
|
}
|
|
|
|
// Load secret key
|
|
var sk Scalar
|
|
if !sk.setB32Seckey(keypair.seckey[:]) {
|
|
return errors.New("invalid secret key")
|
|
}
|
|
|
|
// Load public key
|
|
var pk GroupElementAffine
|
|
pk.fromBytes(keypair.pubkey.data[:])
|
|
if pk.isInfinity() {
|
|
return errors.New("invalid public key")
|
|
}
|
|
|
|
// Negate secret key if Y coordinate is odd (BIP-340 requires even Y)
|
|
pk.y.normalize()
|
|
var skBytes [32]byte
|
|
sk.getB32(skBytes[:])
|
|
|
|
if pk.y.isOdd() {
|
|
sk.negate(&sk)
|
|
sk.getB32(skBytes[:]) // Update skBytes with negated key
|
|
// Update pk to have even Y
|
|
pk.negate(&pk)
|
|
}
|
|
|
|
// Get x-only public key (X coordinate)
|
|
var pkX [32]byte
|
|
pk.x.normalize()
|
|
pk.x.getB32(pkX[:])
|
|
|
|
// Generate nonce (use the possibly-negated secret key)
|
|
var nonce32 [32]byte
|
|
if err := NonceFunctionBIP340(nonce32[:], msg32, skBytes[:], pkX[:], auxRand32); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse nonce scalar
|
|
var k Scalar
|
|
if !k.setB32Seckey(nonce32[:]) {
|
|
return errors.New("nonce generation failed")
|
|
}
|
|
|
|
if k.isZero() {
|
|
return errors.New("nonce is zero")
|
|
}
|
|
|
|
// Compute R = k * G
|
|
var rj GroupElementJacobian
|
|
EcmultGen(&rj, &k)
|
|
|
|
// Convert to affine
|
|
var r GroupElementAffine
|
|
r.setGEJ(&rj)
|
|
r.y.normalize()
|
|
|
|
// If R.y is odd, negate k
|
|
if r.y.isOdd() {
|
|
k.negate(&k)
|
|
// Recompute R with negated k
|
|
EcmultGen(&rj, &k)
|
|
r.setGEJ(&rj)
|
|
}
|
|
|
|
// Extract r = X(R)
|
|
r.x.normalize()
|
|
var r32 [32]byte
|
|
r.x.getB32(r32[:])
|
|
copy(sig64[:32], r32[:])
|
|
|
|
// Compute challenge e = TaggedHash("BIP0340/challenge", r || pk || msg)
|
|
var challengeInput []byte
|
|
challengeInput = append(challengeInput, r32[:]...)
|
|
challengeInput = append(challengeInput, pkX[:]...)
|
|
challengeInput = append(challengeInput, msg32...)
|
|
|
|
challengeHash := TaggedHash(bip340ChallengeTag, challengeInput)
|
|
var e Scalar
|
|
e.setB32(challengeHash[:])
|
|
|
|
// Compute s = k + e * sk
|
|
var s Scalar
|
|
s.mul(&e, &sk)
|
|
s.add(&s, &k)
|
|
|
|
// Serialize s
|
|
var s32 [32]byte
|
|
s.getB32(s32[:])
|
|
copy(sig64[32:], s32[:])
|
|
|
|
// Clear sensitive data
|
|
sk.clear()
|
|
k.clear()
|
|
e.clear()
|
|
s.clear()
|
|
memclear(unsafe.Pointer(&nonce32[0]), 32)
|
|
memclear(unsafe.Pointer(&pkX[0]), 32)
|
|
memclear(unsafe.Pointer(&skBytes[0]), 32)
|
|
rj.clear()
|
|
r.clear()
|
|
|
|
return nil
|
|
}
|
|
|
|
// SchnorrVerify verifies a Schnorr signature following BIP-340
|
|
func SchnorrVerify(sig64 []byte, msg32 []byte, xonlyPubkey *XOnlyPubkey) bool {
|
|
if len(sig64) != 64 {
|
|
return false
|
|
}
|
|
if len(msg32) != 32 {
|
|
return false
|
|
}
|
|
if xonlyPubkey == nil {
|
|
return false
|
|
}
|
|
|
|
// Extract r and s from signature
|
|
var r32 [32]byte
|
|
var s32 [32]byte
|
|
copy(r32[:], sig64[:32])
|
|
copy(s32[:], sig64[32:])
|
|
|
|
// Parse r as field element
|
|
var rx FieldElement
|
|
if err := rx.setB32(r32[:]); err != nil {
|
|
return false
|
|
}
|
|
|
|
// Check if r corresponds to a valid point
|
|
var r GroupElementAffine
|
|
if !r.setXOVar(&rx, false) {
|
|
// Try with odd Y
|
|
if !r.setXOVar(&rx, true) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Parse s as scalar
|
|
var s Scalar
|
|
s.setB32(s32[:])
|
|
if s.isZero() {
|
|
return false
|
|
}
|
|
|
|
// Compute challenge e = TaggedHash("BIP0340/challenge", r || pk || msg)
|
|
var challengeInput []byte
|
|
challengeInput = append(challengeInput, r32[:]...)
|
|
challengeInput = append(challengeInput, xonlyPubkey.data[:]...)
|
|
challengeInput = append(challengeInput, msg32...)
|
|
|
|
challengeHash := TaggedHash(bip340ChallengeTag, challengeInput)
|
|
var e Scalar
|
|
e.setB32(challengeHash[:])
|
|
|
|
// Compute R = s*G - e*P
|
|
// First compute s*G
|
|
var sG GroupElementJacobian
|
|
EcmultGen(&sG, &s)
|
|
|
|
// Compute e*P where P is the x-only pubkey
|
|
// We need to reconstruct P with even Y
|
|
var pk GroupElementAffine
|
|
pk.x.setB32(xonlyPubkey.data[:])
|
|
// Always use even Y for x-only pubkey
|
|
if !pk.setXOVar(&pk.x, false) {
|
|
return false
|
|
}
|
|
|
|
var eP GroupElementJacobian
|
|
EcmultConst(&eP, &pk, &e)
|
|
|
|
// Negate eP
|
|
var negEP GroupElementJacobian
|
|
negEP.negate(&eP)
|
|
|
|
// R = sG + (-eP)
|
|
var R GroupElementJacobian
|
|
R.addVar(&sG, &negEP)
|
|
|
|
// Convert R to affine
|
|
var RAff GroupElementAffine
|
|
RAff.setGEJ(&R)
|
|
|
|
if RAff.isInfinity() {
|
|
return false
|
|
}
|
|
|
|
// Check if R.y is even
|
|
RAff.y.normalize()
|
|
if RAff.y.isOdd() {
|
|
// Negate R
|
|
var negR GroupElementAffine
|
|
negR.negate(&RAff)
|
|
RAff = negR
|
|
}
|
|
|
|
// Compare X(R) with r
|
|
RAff.x.normalize()
|
|
var computedR [32]byte
|
|
RAff.x.getB32(computedR[:])
|
|
|
|
for i := 0; i < 32; i++ {
|
|
if computedR[i] != r32[i] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|