From 9c3f63abd2bbb8e9cc5c67c98a2035acd7ffe21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 30 Mar 2026 13:08:19 +0200 Subject: [PATCH] crypto: add support for BIP-340 --- crypto/bip340/key.go | 31 +++++ crypto/bip340/key_test.go | 71 +++++++++++ crypto/bip340/private.go | 171 ++++++++++++++++++++++++++ crypto/bip340/public.go | 140 +++++++++++++++++++++ crypto/bip340/testvectors/vectors.csv | 20 +++ crypto/bip340/testvectors/vectors.go | 88 +++++++++++++ 6 files changed, 521 insertions(+) create mode 100644 crypto/bip340/key.go create mode 100644 crypto/bip340/key_test.go create mode 100644 crypto/bip340/private.go create mode 100644 crypto/bip340/public.go create mode 100644 crypto/bip340/testvectors/vectors.csv create mode 100644 crypto/bip340/testvectors/vectors.go diff --git a/crypto/bip340/key.go b/crypto/bip340/key.go new file mode 100644 index 0000000..d251b28 --- /dev/null +++ b/crypto/bip340/key.go @@ -0,0 +1,31 @@ +package bip340 + +import ( + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +const ( + // PublicKeyBytesSize is the size, in bytes, of public keys as x-only (BIP-340 format). + PublicKeyBytesSize = 32 + // PrivateKeyBytesSize is the size, in bytes, of private keys in raw bytes. + PrivateKeyBytesSize = secp256k1.PrivKeyBytesLen + // SignatureBytesSize is the size, in bytes, of BIP-340 signatures. + SignatureBytesSize = 64 + + // code waiting for approval: https://github.com/multiformats/multicodec/pull/398 + MultibaseCode = uint64(0x1340) +) + +// GenerateKeyPair generates a new BIP-340 keypair. +// The private key is normalized so the public key always has an even Y coordinate, +// enabling lossless x-only serialization. +func GenerateKeyPair() (*PublicKey, *PrivateKey, error) { + priv, err := secp256k1.GeneratePrivateKey() + if err != nil { + return nil, nil, err + } + if priv.PubKey().Y().Bit(0) != 0 { + priv.Key.Negate() + } + return &PublicKey{k: priv.PubKey()}, &PrivateKey{k: priv}, nil +} diff --git a/crypto/bip340/key_test.go b/crypto/bip340/key_test.go new file mode 100644 index 0000000..6a3e92b --- /dev/null +++ b/crypto/bip340/key_test.go @@ -0,0 +1,71 @@ +package bip340 + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + testsuite "github.com/MetaMask/go-did-it/crypto/_testsuite" + "github.com/MetaMask/go-did-it/crypto/bip340/testvectors" +) + +var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{ + Name: "secp256k1-bip340", + GenerateKeyPair: GenerateKeyPair, + PublicKeyFromBytes: PublicKeyFromBytes, + PublicKeyFromPublicKeyMultibase: PublicKeyFromPublicKeyMultibase, + PrivateKeyFromBytes: PrivateKeyFromBytes, + MultibaseCode: MultibaseCode, + DefaultHash: 0, // BIP-340 passes messages raw to its internal tagged hashes; no pre-hashing. + PublicKeyBytesSize: PublicKeyBytesSize, + PrivateKeyBytesSize: PrivateKeyBytesSize, + SignatureBytesSize: SignatureBytesSize, +} + +func TestSuite(t *testing.T) { + testsuite.TestSuite(t, harness) +} + +func BenchmarkSuite(b *testing.B) { + testsuite.BenchSuite(b, harness) +} + +func TestBIP340Vectors(t *testing.T) { + vectors, err := testvectors.Load() + require.NoError(t, err) + + for _, v := range vectors { + t.Run(vectorName(v), func(t *testing.T) { + // Verification via public API. + pub, err := PublicKeyFromBytes(v.PublicKey) + if err != nil { + require.False(t, v.Valid, "unexpected parse error: %v", err) + return + } + + got := pub.VerifyBytes(v.Message, v.Signature) + require.Equal(t, v.Valid, got, "verification mismatch") + + // Signing (only when secret key and aux_rand are provided). + if v.SecretKey == nil { + return + } + + priv, err := PrivateKeyFromBytes(v.SecretKey) + require.NoError(t, err, "failed to parse private key") + + // Inject the known aux_rand for deterministic comparison against the vector. + gotSig, err := bip340Sign(&priv.k.Key, priv.k.PubKey(), v.Message, v.AuxRand) + require.NoError(t, err, "signing failed") + require.Equal(t, v.Signature, gotSig, "signature mismatch") + }) + } +} + +func vectorName(v testvectors.Vector) string { + if v.Comment != "" { + return fmt.Sprintf("%d-%s", v.Index, v.Comment) + } + return fmt.Sprintf("%d", v.Index) +} diff --git a/crypto/bip340/private.go b/crypto/bip340/private.go new file mode 100644 index 0000000..cc85a9d --- /dev/null +++ b/crypto/bip340/private.go @@ -0,0 +1,171 @@ +package bip340 + +import ( + "crypto/rand" + "crypto/sha256" + "fmt" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + + "github.com/MetaMask/go-did-it/crypto" +) + +var _ crypto.PrivateKeySigningBytes = &PrivateKey{} +var _ crypto.PrivateKeyToBytes = &PrivateKey{} + +type PrivateKey struct { + k *secp256k1.PrivateKey +} + +// PrivateKeyFromBytes parses a 32-byte private key and normalizes it so the +// corresponding public key has an even Y coordinate. +func PrivateKeyFromBytes(b []byte) (*PrivateKey, error) { + if len(b) != PrivateKeyBytesSize { + return nil, fmt.Errorf("bip340: invalid private key size: expected %d bytes, got %d", PrivateKeyBytesSize, len(b)) + } + priv := secp256k1.PrivKeyFromBytes(b) + if priv.PubKey().Y().Bit(0) != 0 { + priv.Key.Negate() + } + return &PrivateKey{k: priv}, nil +} + +func (p *PrivateKey) Equal(other crypto.PrivateKey) bool { + if other, ok := other.(*PrivateKey); ok { + return p.k.PubKey().IsEqual(other.k.PubKey()) + } + return false +} + +func (p *PrivateKey) Public() crypto.PublicKey { + return &PublicKey{k: p.k.PubKey()} +} + +func (p *PrivateKey) ToBytes() []byte { + return p.k.Serialize() +} + +// SignToBytes signs the message using BIP-340 Schnorr and returns a 64-byte signature. +// Signing options are not supported as BIP-340 uses a fixed internal hash function. +func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) { + if len(opts) != 0 { + return nil, fmt.Errorf("bip340: SignToBytes does not support any options") + } + var auxRand [32]byte + if _, err := rand.Read(auxRand[:]); err != nil { + return nil, fmt.Errorf("bip340: failed to generate auxiliary randomness: %w", err) + } + + return bip340Sign(&p.k.Key, p.k.PubKey(), message, auxRand) +} + +// Unwrap returns the underlying secp256k1 private key. +func (p *PrivateKey) Unwrap() *secp256k1.PrivateKey { + return p.k +} + +// bip340Sign implements BIP-340 signing. +// Spec: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#default-signing +// Reference: https://github.com/btcsuite/btcd/blob/3a0df88/btcec/schnorr/signature.go#L393 +// +// Preconditions: pubKey has even Y (enforced at key import/generation in this package), +// meaning d is already the normalized secret scalar — step 5 (negate if odd Y) is a no-op. +func bip340Sign(d *secp256k1.ModNScalar, pubKey *secp256k1.PublicKey, msg []byte, auxRand [32]byte) ([]byte, error) { + // Step 1: let d' = int(d) + // Step 2-3: validity checks (d != 0, d < n) guaranteed by secp256k1.PrivKeyFromBytes. + // Step 4: let P = d'*G — pubKey is already computed and stored. + // Step 5: negate d if has_odd_y(P) — no-op: caller guarantees even Y. + var pubKeyBytes [32]byte + pubKey.X().FillBytes(pubKeyBytes[:]) + + // Serialize d to bytes; zero after use. + var privBytes [32]byte + d.PutBytes(&privBytes) + defer func() { + for i := range privBytes { + privBytes[i] = 0 + } + }() + + // Step 6: let t = bytes(d) XOR tagged_hash("BIP0340/aux", a) + auxHash := taggedHash("BIP0340/aux", auxRand[:]) + var t [32]byte + for i := range t { + t[i] = privBytes[i] ^ auxHash[i] + } + defer func() { + for i := range t { + t[i] = 0 + } + }() + + // Step 7: let rand = tagged_hash("BIP0340/nonce", t || bytes(P) || m) + // Step 8: let k' = int(rand) mod n + nonceHash := taggedHash("BIP0340/nonce", t[:], pubKeyBytes[:], msg) + defer func() { + for i := range nonceHash { + nonceHash[i] = 0 + } + }() + var k secp256k1.ModNScalar + var nonceArr [32]byte + copy(nonceArr[:], nonceHash) + k.SetBytes(&nonceArr) + + // Step 9: fail if k' = 0 + if k.IsZero() { + return nil, fmt.Errorf("bip340: generated nonce is zero") + } + + // Step 10: let R = k'*G + var R secp256k1.JacobianPoint + secp256k1.ScalarBaseMultNonConst(&k, &R) + R.ToAffine() + + // Step 11: let k = k' if has_even_y(R), otherwise k = n - k' + if R.Y.IsOdd() { + k.Negate() + } + + // Step 12: let e = int(tagged_hash("BIP0340/challenge", bytes(R) || bytes(P) || m)) mod n + var rBytes [32]byte + R.X.PutBytes(&rBytes) + commitment := taggedHash("BIP0340/challenge", rBytes[:], pubKeyBytes[:], msg) + var e secp256k1.ModNScalar + var commitmentArr [32]byte + copy(commitmentArr[:], commitment) + e.SetBytes(&commitmentArr) + + // Step 13: let sig = bytes(R.x) || bytes((k + e*d) mod n) + s := new(secp256k1.ModNScalar).Mul2(&e, d).Add(&k) + k.Zero() + var sig [SignatureBytesSize]byte + R.X.PutBytes((*[32]byte)(sig[:32])) + s.PutBytes((*[32]byte)(sig[32:])) + + // Step 14: if Verify(bytes(P), m, sig) fails, abort. + // Required by the spec as a fault-attack guard. + var rField secp256k1.FieldVal + rField.SetByteSlice(sig[:32]) + rField.Normalize() + var sScalar secp256k1.ModNScalar + sScalar.SetByteSlice(sig[32:]) + if !bip340Verify(&rField, &sScalar, pubKey, msg) { + return nil, fmt.Errorf("bip340: produced signature failed verification") + } + + return sig[:], nil +} + +// taggedHash computes the BIP-340 tagged hash: +// SHA256(SHA256(tag) || SHA256(tag) || data...) +func taggedHash(tag string, data ...[]byte) []byte { + tagHash := sha256.Sum256([]byte(tag)) + h := sha256.New() + h.Write(tagHash[:]) + h.Write(tagHash[:]) + for _, d := range data { + h.Write(d) + } + return h.Sum(nil) +} diff --git a/crypto/bip340/public.go b/crypto/bip340/public.go new file mode 100644 index 0000000..0a7f3a7 --- /dev/null +++ b/crypto/bip340/public.go @@ -0,0 +1,140 @@ +package bip340 + +import ( + "fmt" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + + "github.com/MetaMask/go-did-it/crypto" + helpers "github.com/MetaMask/go-did-it/crypto/internal" +) + +var _ crypto.PublicKeySigningBytes = &PublicKey{} +var _ crypto.PublicKeyToBytes = &PublicKey{} + +type PublicKey struct { + k *secp256k1.PublicKey +} + +// PublicKeyFromBytes parses a 32-byte x-only BIP-340 public key. +// The Y coordinate is reconstructed as even (lift_x). +func PublicKeyFromBytes(b []byte) (*PublicKey, error) { + if len(b) != PublicKeyBytesSize { + return nil, fmt.Errorf("bip340: invalid public key size: expected %d bytes, got %d", PublicKeyBytesSize, len(b)) + } + compressed := make([]byte, 33) + compressed[0] = 0x02 + copy(compressed[1:], b) + pub, err := secp256k1.ParsePubKey(compressed) + if err != nil { + return nil, fmt.Errorf("bip340: failed to parse public key: %w", err) + } + return &PublicKey{k: pub}, nil +} + +// PublicKeyFromPublicKeyMultibase decodes the public key from its Multibase form +func PublicKeyFromPublicKeyMultibase(multibase string) (*PublicKey, error) { + code, bytes, err := helpers.PublicKeyMultibaseDecode(multibase) + if err != nil { + return nil, err + } + if code != MultibaseCode { + return nil, fmt.Errorf("invalid code") + } + return PublicKeyFromBytes(bytes) +} + +func (p *PublicKey) Equal(other crypto.PublicKey) bool { + if other, ok := other.(*PublicKey); ok { + return p.k.IsEqual(other.k) + } + return false +} + +// ToBytes returns the 32-byte x-only BIP-340 serialization of the public key. +func (p *PublicKey) ToBytes() []byte { + var buf [PublicKeyBytesSize]byte + p.k.X().FillBytes(buf[:]) + return buf[:] +} + +func (p *PublicKey) ToPublicKeyMultibase() string { + return helpers.PublicKeyMultibaseEncode(MultibaseCode, p.ToBytes()) +} + +// VerifyBytes verifies a 64-byte BIP-340 Schnorr signature. +// Signing options are not supported as BIP-340 uses a fixed internal hash function. +func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.SigningOption) bool { + if len(opts) != 0 { + return false // VerifyBytes does not support any options + } + if len(signature) != SignatureBytesSize { + return false + } + + // Parse r (field element) and s (scalar) from signature. + var r secp256k1.FieldVal + if r.SetByteSlice(signature[:32]) { + return false // r >= field prime + } + r.Normalize() + var s secp256k1.ModNScalar + if s.SetByteSlice(signature[32:]) { + return false // s >= curve order n + } + + return bip340Verify(&r, &s, p.k, message) +} + +// Unwrap returns the underlying secp256k1 public key. +func (p *PublicKey) Unwrap() *secp256k1.PublicKey { + return p.k +} + +// bip340Verify implements BIP-340 verification. +// Spec: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#verification +// Reference: https://github.com/btcsuite/btcd/blob/3a0df88/btcec/schnorr/signature.go#L114 +// +// Inputs: 32-byte public key P (x-only), message m, 64-byte signature (r, s). +// Preconditions: r is normalized, r < p, s < n (enforced by caller). +func bip340Verify(r *secp256k1.FieldVal, s *secp256k1.ModNScalar, pubKey *secp256k1.PublicKey, msgHash []byte) bool { + // Step 1: let P = lift_x(public key) + // Already done: pubKey was parsed via PublicKeyFromBytes which uses lift_x (0x02 prefix). + + // Step 2: let r = int(sig[ 0:32]); fail if r >= p — checked by caller. + // Step 3: let s = int(sig[32:64]); fail if s >= n — checked by caller. + + // Step 4: let e = int(tagged_hash("BIP0340/challenge", bytes(r) || bytes(P) || m)) mod n + var rBytes, pubKeyBytes [32]byte + r.PutBytes(&rBytes) + pubKey.X().FillBytes(pubKeyBytes[:]) + commitment := taggedHash("BIP0340/challenge", rBytes[:], pubKeyBytes[:], msgHash) + var e secp256k1.ModNScalar + var commitmentArr [32]byte + copy(commitmentArr[:], commitment) + e.SetBytes(&commitmentArr) + e.Negate() // negate so we can compute s*G + (-e)*P instead of s*G - e*P + + // Step 5: let R = s*G - e*P + var P, R, sG, eP secp256k1.JacobianPoint + pubKey.AsJacobian(&P) + secp256k1.ScalarBaseMultNonConst(s, &sG) + secp256k1.ScalarMultNonConst(&e, &P, &eP) + secp256k1.AddNonConst(&sG, &eP, &R) + + // Step 6: fail if is_infinite(R) + if R.Z.IsZero() { + return false + } + + R.ToAffine() + + // Step 7: fail if not has_even_y(R) + if R.Y.IsOdd() { + return false + } + + // Step 8: fail if x(R) != r + R.X.Normalize() + return r.Equals(&R.X) +} diff --git a/crypto/bip340/testvectors/vectors.csv b/crypto/bip340/testvectors/vectors.csv new file mode 100644 index 0000000..aa317a3 --- /dev/null +++ b/crypto/bip340/testvectors/vectors.csv @@ -0,0 +1,20 @@ +index,secret key,public key,aux_rand,message,signature,verification result,comment +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE, +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE, +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE, +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE, +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size +15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12) +16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12) +17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12) +18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12) diff --git a/crypto/bip340/testvectors/vectors.go b/crypto/bip340/testvectors/vectors.go new file mode 100644 index 0000000..dbad252 --- /dev/null +++ b/crypto/bip340/testvectors/vectors.go @@ -0,0 +1,88 @@ +// Package testvectors provides the official BIP-340 test vectors. +// Source: https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv +package testvectors + +import ( + "bytes" + "embed" + "encoding/csv" + "encoding/hex" + "fmt" + "strconv" +) + +//go:embed vectors.csv +var vectorFiles embed.FS + +// Vector represents one row from the BIP-340 test vector CSV. +type Vector struct { + Index int + SecretKey []byte // nil if not provided (verify-only rows) + PublicKey []byte + AuxRand [32]byte // all-zero if not provided (verify-only rows) + Message []byte + Signature []byte + Valid bool + Comment string +} + +// Load reads and parses vectors.csv, returning all test vectors. +func Load() ([]Vector, error) { + data, err := vectorFiles.ReadFile("vectors.csv") + if err != nil { + return nil, err + } + + r := csv.NewReader(bytes.NewReader(data)) + rows, err := r.ReadAll() + if err != nil { + return nil, err + } + + var vectors []Vector + for _, row := range rows[1:] { // skip header + index, err := strconv.Atoi(row[0]) + if err != nil { + return nil, err + } + + v := Vector{ + Index: index, + Comment: row[7], + } + + if row[1] != "" { + v.SecretKey, err = hex.DecodeString(row[1]) + if err != nil { + return nil, err + } + } + v.PublicKey, err = hex.DecodeString(row[2]) + if err != nil { + return nil, err + } + if row[3] != "" { + auxRand, err := hex.DecodeString(row[3]) + if err != nil { + return nil, err + } + if len(auxRand) != 32 { + return nil, fmt.Errorf("aux_rand at index %d: expected 32 bytes, got %d", index, len(auxRand)) + } + copy(v.AuxRand[:], auxRand) + } + v.Message, err = hex.DecodeString(row[4]) + if err != nil { + return nil, err + } + v.Signature, err = hex.DecodeString(row[5]) + if err != nil { + return nil, err + } + v.Valid = row[6] == "TRUE" + + vectors = append(vectors, v) + } + + return vectors, nil +}