From a97b136d6697030259345f9a6423e28dd7caed9d Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Sat, 1 Aug 2026 15:09:09 -0500 Subject: [PATCH 01/12] fix a linter issue --- hash/hash_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hash/hash_test.go b/hash/hash_test.go index 09de08d8..d1b49fff 100644 --- a/hash/hash_test.go +++ b/hash/hash_test.go @@ -21,13 +21,14 @@ package hash import ( "crypto/rand" "crypto/sha256" + "crypto/sha3" "crypto/sha512" "encoding/hex" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/crypto/sha3" + xsha3 "golang.org/x/crypto/sha3" ) // Sanity check of SHA3_256 @@ -260,7 +261,7 @@ func TestKeccak(t *testing.T) { value := make([]byte, i) _, err := rand.Read(value) require.NoError(t, err) - k := sha3.NewLegacyKeccak256() + k := xsha3.NewLegacyKeccak256() k.Write(value) expected := k.Sum(nil) From e54f62f8b7900afe8fdb217174b9c92fc46ed0a2 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Sat, 1 Aug 2026 15:11:47 -0500 Subject: [PATCH 02/12] refactor ecdsa and use go-ethereum for secp256k1 operations --- bls12381_utils.go | 4 + ecdsa.go | 508 ++++++++++++--------------------------------- ecdsa_p256.go | 275 ++++++++++++++++++++++++ ecdsa_secp256k1.go | 267 ++++++++++++++++++++++++ ecdsa_test.go | 51 +---- go.mod | 8 +- go.sum | 25 ++- sign.go | 15 +- 8 files changed, 696 insertions(+), 457 deletions(-) create mode 100644 ecdsa_p256.go create mode 100644 ecdsa_secp256k1.go diff --git a/bls12381_utils.go b/bls12381_utils.go index e504a914..aa939c79 100644 --- a/bls12381_utils.go +++ b/bls12381_utils.go @@ -117,6 +117,10 @@ func initBLS12381() { // set a global point to infinity C.E2_set_infty((*C.E2)(&g2PublicKey.point)) g2PublicKey.isIdentity = true + + blsInstance = &blsBLS12381Algo{ + algo: BLSBLS12381, + } } // String returns a hex-encoded representation of the scalar. diff --git a/ecdsa.go b/ecdsa.go index 6a2ae135..9e4e715b 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -21,168 +21,70 @@ package crypto // Elliptic Curve Digital Signature Algorithm is implemented as // defined in FIPS 186-4 (although the hash functions implemented in this package are SHA2 and SHA3). -// Most of the implementation is Go based and is not optimized for performance. - -// This implementation does not include any security against side-channel attacks. +// This implementation is not resistant against side-channel attacks or fault attacks. import ( - "crypto/ecdh" - "crypto/ecdsa" - "crypto/elliptic" + "bytes" "crypto/hkdf" - "crypto/rand" "crypto/sha256" "fmt" "math/big" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/onflow/crypto/hash" ) -const ( - // NIST P256 - SignatureLenECDSAP256 = 64 - PrKeyLenECDSAP256 = 32 - // PubKeyLenECDSAP256 is the size of uncompressed points on P256 - PubKeyLenECDSAP256 = 64 - - // SECG secp256k1 - SignatureLenECDSASecp256k1 = 64 - PrKeyLenECDSASecp256k1 = 32 - // PubKeyLenECDSASecp256k1 is the size of uncompressed points on secp256k1 - PubKeyLenECDSASecp256k1 = 64 -) - -// ecdsaAlgo embeds SignAlgo -type ecdsaAlgo struct { - // elliptic curve - curve elliptic.Curve - // the signing algo and parameters +// ecdsaContext embeds SigningAlgorithm +type ecdsaContext struct { + // the signing algo algo SigningAlgorithm + // curve prime field + curveP *big.Int + // curve order + curveN *big.Int } -// ECDSA contexts for each supported curve -// -// NIST P-256 curve -var p256Instance *ecdsaAlgo +const ecEncodingUncompressed = 0x4 -// SECG secp256k1 curve https://www.secg.org/sec2-v2.pdf -var secp256k1Instance *ecdsaAlgo +func initECDSA() { + // ECDSA with P256 + initECDSAP256() + // ECDSA with secp256k1 + initECDSASecp256k1() +} func bitsToBytes(bits int) int { return (bits + 7) >> 3 } -// signHash returns the signature of the input hash using the private key receiver. -// The signature is the concatenation bytes(r) || bytes(s), -// where `r` and `s` are padded to the curve order size. -// Current implementation of `sign` is randomized, mixing the entropy from the -// the system's crypto/rand, the private key and the hash. -// -// The caller must make sure that the hash is at least the curve order size. -func (sk *prKeyECDSA) signHash(h hash.Hash) (Signature, error) { - r, s, err := ecdsa.Sign(rand.Reader, sk.goPrKey, h) - if err != nil { - return nil, fmt.Errorf("ECDSA sign failed: %w", err) - } - rBytes := r.Bytes() - sBytes := s.Bytes() - nLen := bitsToBytes((sk.alg.curve.Params().N).BitLen()) - signature := make([]byte, 2*nLen) - // pad the signature with zeroes - copy(signature[nLen-len(rBytes):], rBytes) - copy(signature[2*nLen-len(sBytes):], sBytes) - return signature, nil -} - -// Sign signs an array of bytes -// -// The resulting signature is the concatenation bytes(r)||bytes(s), -// where r and s are padded to the curve order size. -// The private key is read only while sha2 and sha3 hashers are -// modified temporarily. -// -// The function returns: -// - (false, errNilHasher) if a hasher is nil -// - (false, invalidHasherSizeError) when the hasher's output size is less than the curve order (currently 32 bytes). -// - (nil, error) if an unexpected error occurs -// - (signature, nil) otherwise -func (sk *prKeyECDSA) Sign(data []byte, alg hash.Hasher) (Signature, error) { - if alg == nil { +func (a *ecdsaContext) checkAlgoAndComputeHash(msg []byte, hasher hash.Hasher) (hash.Hash, error) { + if hasher == nil { return nil, errNilHasher } - // check hasher's size is at least the curve order in bytes - nLen := bitsToBytes((sk.alg.curve.Params().N).BitLen()) - if alg.Size() < nLen { - return nil, invalidHasherSizeErrorf( - "hasher's size should be at least %d, got %d", nLen, alg.Size()) - } - - h := alg.ComputeHash(data) - return sk.signHash(h) -} - -// verifyHash implements ECDSA signature verification -func (pk *pubKeyECDSA) verifyHash(sig Signature, h hash.Hash) (bool, error) { - nLen := bitsToBytes((pk.alg.curve.Params().N).BitLen()) - - if len(sig) != 2*nLen { - return false, nil - } - - var r big.Int - var s big.Int - r.SetBytes(sig[:nLen]) - s.SetBytes(sig[nLen:]) - return ecdsa.Verify(pk.goPubKey, h, &r, &s), nil -} - -// Verify verifies a signature of an input data under the public key. -// -// If the input signature slice has an invalid length or fails to deserialize into valid -// scalars, the function returns false without an error. -// -// Public keys are read only, sha2 and sha3 hashers are -// modified temporarily. -// -// The function returns: -// - (false, errNilHasher) if a hasher is nil -// - (false, invalidHasherSizeError) when the hasher's output size is less than the curve order (currently 32 bytes). -// - (false, error) if an unexpected error occurs -// - (validity, nil) otherwise -func (pk *pubKeyECDSA) Verify(sig Signature, data []byte, alg hash.Hasher) (bool, error) { - if alg == nil { - return false, errNilHasher - } // check hasher's size is at least the curve order in bytes - nLen := bitsToBytes((pk.alg.curve.Params().N).BitLen()) - if alg.Size() < nLen { - return false, invalidHasherSizeErrorf( - "hasher's size should be at least %d, got %d", nLen, alg.Size()) + nLen := bitsToBytes((a.curveN).BitLen()) + if hasher.Size() < nLen { + return nil, invalidHasherSizeErrorf( + "hasher's size should be at least %d, got %d", nLen, hasher.Size()) } - h := alg.ComputeHash(data) - return pk.verifyHash(sig, h) + h := hasher.ComputeHash(msg) + return h, nil } // signatureFormatCheck verifies the format of a serialized signature, // regardless of messages or public keys. // If FormatCheck returns false then the input is not a valid ECDSA // signature and will fail a verification against any message and public key. -func (a *ecdsaAlgo) signatureFormatCheck(sig Signature) bool { - N := a.curve.Params().N +func (a *ecdsaContext) signatureFormatCheck(sig Signature) bool { + N := a.curveN nLen := bitsToBytes(N.BitLen()) if len(sig) != 2*nLen { return false } - var r big.Int - var s big.Int - r.SetBytes(sig[:nLen]) - s.SetBytes(sig[nLen:]) + r, s := readTwoBigInts(sig, nLen) if r.Sign() == 0 || s.Sign() == 0 { return false @@ -199,61 +101,43 @@ func (a *ecdsaAlgo) signatureFormatCheck(sig Signature) bool { var one = new(big.Int).SetInt64(1) -// goecdsaMapKey maps the input seed to a private key -// of the Go crypto/ecdsa library. +// mapToPrivateKey simply maps the input seed to an ECDSA private key // The private scalar `d` satisfies 0 < d < n. -// Returned error is expected to be nil. -func goecdsaMapKey(curve elliptic.Curve, seed []byte) (*ecdsa.PrivateKey, error) { +// +// The function returns: +// - (nil, invalidInputsError) if the curve is not supported +// - (nil, error) if an unexpected error occurs +// - (sk, nil) if key mapping was successful +func (a *ecdsaContext) mapToPrivateKey(seed []byte) (PrivateKey, error) { d := new(big.Int).SetBytes(seed) - n := new(big.Int).Sub(curve.Params().N, one) - d.Mod(d, n) - d.Add(d, one) - return goecdsaPrivateKey(curve, d) // n > d > 0 at this point + NminusOne := new(big.Int).Sub(a.curveN, one) + d.Mod(d, NminusOne) + d.Add(d, one) // n > d > 0 at this point + return a.privateKey(d) } -// goecdsaPrivateKey creates a Go crypto/ecdsa private key using the -// input curve and scalar. -// Input scalar is assumed to be a non-zero integer modulo the curve order `n`. -// Error returns: -// - invalidInputsError if the input curve is unsupported -func goecdsaPrivateKey(curve elliptic.Curve, d *big.Int) (*ecdsa.PrivateKey, error) { - priv := new(ecdsa.PrivateKey) - priv.D = d - priv.PublicKey.Curve = curve - - // compute the crypto/ecdsa public key - if curve == elliptic.P256() { - // Perform the base scalar multiplication using crypto/ecdh, - // because crypto/elliptic deprecated `ScalarBaseMult`. - // - // We build the ecdh.PrivateKey directly from the scalar bytes - // instead of going through `priv.ECDH()`: since Go 1.26, - // ecdsa's `(*PrivateKey).ECDH` serializes the key via `(*PrivateKey).Bytes`, - // which reads the public affine coordinates `X`/`Y`. - // Those are not set yet at this point (we are computing them), - // so that path dereferences nil and panics. - // Constructing the ecdh key from the scalar avoids reading `X`/`Y` - // and works across Go versions. - scalarLen := bitsToBytes(curve.Params().N.BitLen()) - ecdhPriv, err := ecdh.P256().NewPrivateKey(d.FillBytes(make([]byte, scalarLen))) - if err != nil { - // at this point, no error is expected because the function can't be called - // with a zero scalar modulo `n` - return nil, fmt.Errorf("non expected error when creating an ECDH private key: %w", err) - } - // crypto/ecdh serialization uses SEC1 version 2 (https://www.secg.org/sec1-v2.pdf section 2.3.3). - // The bytes returned are `0x04 || X || Y` because the point is guaranteed to be non-infinity - ecdhPubBytes := ecdhPriv.PublicKey().Bytes() - pLen := bitsToBytes(curve.Params().P.BitLen()) - priv.PublicKey.X = new(big.Int).SetBytes(ecdhPubBytes[1 : 1+pLen]) - priv.PublicKey.Y = new(big.Int).SetBytes(ecdhPubBytes[1+pLen:]) - } else if curve == btcec.S256() { - // `ScalarBaseMult` is not deprecated in btcec's type `KoblitzCurve` - priv.PublicKey.X, priv.PublicKey.Y = btcec.S256().ScalarBaseMult(d.Bytes()) - } else { +// privateKey returns an ECDSA private key using the +// input scalar. + +// Input scalar d is assumed to be satisfy 0 < d < n before calling this function. +// +// The function returns: +// - (nil, invalidInputsError) if the curve is not supported +// - (nil, error) if an unexpected error occurs +// - (sk, nil) if key mapping was successful +func (a *ecdsaContext) privateKey(d *big.Int) (PrivateKey, error) { + dBytes := make([]byte, bitsToBytes(a.curveN.BitLen())) + d.FillBytes(dBytes) // dBytes is the big-endian encoding of d padded to the curve order + + // build the private key depending on the curve + switch a.algo { + case ECDSAP256: + return privateKeyECDSAP256(a, dBytes) + case ECDSASecp256k1: + return privateKeyECDSASecp256k1(a, dBytes), nil + default: return nil, invalidInputsErrorf("the curve is not supported") } - return priv, nil } // generatePrivateKey generates a private key for ECDSA @@ -261,7 +145,7 @@ func goecdsaPrivateKey(curve elliptic.Curve, d *big.Int) (*ecdsa.PrivateKey, err // // It is recommended to use a secure crypto RNG to generate the seed. // The seed must have enough entropy. -func (a *ecdsaAlgo) generatePrivateKey(seed []byte) (PrivateKey, error) { +func (a *ecdsaContext) generatePrivateKey(seed []byte) (PrivateKey, error) { if len(seed) < KeyGenSeedMinLen || len(seed) > KeyGenSeedMaxLen { return nil, invalidInputsErrorf("seed byte length should be between %d and %d", KeyGenSeedMinLen, KeyGenSeedMaxLen) @@ -274,7 +158,7 @@ func (a *ecdsaAlgo) generatePrivateKey(seed []byte) (PrivateKey, error) { salt := []byte("") // HKDF salt info := "" // HKDF info // use extra 128 bits to reduce the modular reduction bias - nLen := bitsToBytes((a.curve.Params().N).BitLen()) + nLen := bitsToBytes((a.curveN).BitLen()) okmLength := nLen + (securityBits / 8) // instantiate HKDF and extract okm @@ -284,23 +168,19 @@ func (a *ecdsaAlgo) generatePrivateKey(seed []byte) (PrivateKey, error) { } defer overwrite(okm) // overwrite okm - sk, err := goecdsaMapKey(a.curve, okm) + sk, err := a.mapToPrivateKey(okm) if err != nil { // no error is expected at this point return nil, fmt.Errorf("mapping the private key failed: %w", err) } - return &prKeyECDSA{ - alg: a, - goPrKey: sk, - pubKey: nil, // public key is not constructed - }, nil + return sk, nil } -func (a *ecdsaAlgo) rawDecodePrivateKey(der []byte) (PrivateKey, error) { - n := a.curve.Params().N +func (a *ecdsaContext) rawDecodePrivateKey(der []byte) (PrivateKey, error) { + n := a.curveN nLen := bitsToBytes(n.BitLen()) if len(der) != nLen { - return nil, invalidInputsErrorf("input has incorrect %s key size", a.algo) + return nil, invalidInputsErrorf("input has incorrect %s key size, should be %d", a.algo, nLen) } var d big.Int d.SetBytes(der) @@ -313,20 +193,16 @@ func (a *ecdsaAlgo) rawDecodePrivateKey(der []byte) (PrivateKey, error) { return nil, invalidInputsErrorf("zero private keys are not a valid %s key", a.algo) } - priv, err := goecdsaPrivateKey(a.curve, &d) // n > d > 0 at this point + sk, err := a.privateKey(&d) // n > d > 0 at this point if err != nil { // error is not expected at this point return nil, fmt.Errorf("building the private key failed: %w", err) } - return &prKeyECDSA{ - alg: a, - goPrKey: priv, - pubKey: nil, // public key is not constructed - }, nil + return sk, nil } -func (a *ecdsaAlgo) decodePrivateKey(der []byte) (PrivateKey, error) { +func (a *ecdsaContext) decodePrivateKey(der []byte) (PrivateKey, error) { return a.rawDecodePrivateKey(der) } @@ -335,236 +211,112 @@ func (a *ecdsaAlgo) decodePrivateKey(der []byte) (PrivateKey, error) { // Note that infinity point serialization isn't defined in this package so the input (or output) can never represent an infinity point. // Error Returns: // - invalidInputsError if the input is not a valid serialization of a public key on the given curve. -func (a *ecdsaAlgo) rawDecodePublicKey(der []byte) (PublicKey, error) { - curve := a.curve - p := (curve.Params().P) - pLen := bitsToBytes(p.BitLen()) - if len(der) != 2*pLen { - return nil, invalidInputsErrorf("input has incorrect %s key size, got %d, expects %d", - a.algo, len(der), 2*pLen) - } - var x, y big.Int - x.SetBytes(der[:pLen]) - y.SetBytes(der[pLen:]) - - // check the coordinates are valid field elements - if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 { - return nil, invalidInputsErrorf("at least one coordinate is larger than the field prime for %s", a.algo) - } - +func (a *ecdsaContext) rawDecodePublicKey(input []byte) (PublicKey, error) { // all the curves supported for now have a cofactor equal to 1, - // so that checking the point is on curve is enough. - if curve == elliptic.P256() { - // use crypto/ecdh implementation to perform on curve check - // because crypto/elliptic deprecated `IsOnCurve`. - // ECDH's `NewPublicKey` checks the public key is on curve to avoid falling in small-order groups. - - // crypto/ecdh deserialization uses SEC1 version 2 (https://www.secg.org/sec1-v2.pdf section 2.3.3) - // except for infinity point. - // The bytes serialization for non-zero points is `0x04 || X || Y` - ecdhPubBytes := append([]byte{0x4}, der...) - - _, err := ecdh.P256().NewPublicKey(ecdhPubBytes) - if err != nil { - return nil, invalidInputsErrorf("input is not a point on curve P-256: %w", err) - } - } else if curve == btcec.S256() { - // `IsOnCurve` is not deprecated in btcec's type `KoblitzCurve` - if !btcec.S256().IsOnCurve(&x, &y) { - return nil, invalidInputsErrorf("input is not a point on curve secp256k1") - } - } else { + // so that checking the point is on curve is enough to make sure it is on the correct subgroup + switch a.algo { + case ECDSAP256: + return publicKeyECDSAP256(a, input) + case ECDSASecp256k1: + return publicKeyECDSASecp256k1(a, input) + default: return nil, invalidInputsErrorf("curve is not supported") } - pk := ecdsa.PublicKey{ - Curve: a.curve, - X: &x, - Y: &y, - } - - return &pubKeyECDSA{a, &pk}, nil } -func (a *ecdsaAlgo) decodePublicKey(der []byte) (PublicKey, error) { +func (a *ecdsaContext) decodePublicKey(der []byte) (PublicKey, error) { return a.rawDecodePublicKey(der) } // decodePublicKeyCompressed returns a non-infinity public key given the bytes of a compressed // public key according to X9.62 section 4.3.6. -// The compressed representation uses an extra byte to disambiguate sign. // Note that infinity point serialization isn't defined in this package so the input (or output) // can never represent an infinity point. // Error Returns: // - invalidInputsError if the curve isn't supported or the input isn't a valid key serialization // on the given curve. -func (a *ecdsaAlgo) decodePublicKeyCompressed(pkBytes []byte) (PublicKey, error) { - expectedLen := bitsToBytes(a.curve.Params().BitSize) + 1 - if len(pkBytes) != expectedLen { - return nil, invalidInputsErrorf("input length incompatible, expected %d, got %d", expectedLen, len(pkBytes)) - } - var goPubKey *ecdsa.PublicKey - - if a.curve == elliptic.P256() { - x, y := elliptic.UnmarshalCompressed(a.curve, pkBytes) - if x == nil { - return nil, invalidInputsErrorf("input %x isn't a compressed serialization of a %v key", pkBytes, a.algo.String()) - } - goPubKey = new(ecdsa.PublicKey) - goPubKey.Curve = a.curve - goPubKey.X = x - goPubKey.Y = y - - } else if a.curve == btcec.S256() { - // use `btcec` because elliptic's `UnmarshalCompressed` doesn't work for SEC Koblitz curves - pk, err := btcec.ParsePubKey(pkBytes) - if err != nil { - return nil, invalidInputsErrorf("input %x isn't a compressed serialization of a %v key", pkBytes, a.algo.String()) - } - // convert to a crypto/ecdsa key - goPubKey = pk.ToECDSA() - } else { +func (a *ecdsaContext) decodePublicKeyCompressed(pkBytes []byte) (PublicKey, error) { + switch a.algo { + case ECDSAP256: + return p256DecodePublicKeyCompressed(pkBytes) + case ECDSASecp256k1: + return secp256k1DecodePublicKeyCompressed(pkBytes) + default: return nil, invalidInputsErrorf("the input curve is not supported") } - return &pubKeyECDSA{a, goPubKey}, nil } -// prKeyECDSA is the private key of ECDSA, it implements the interface PrivateKey -type prKeyECDSA struct { - // the signature algo - alg *ecdsaAlgo - // ecdsa private key - goPrKey *ecdsa.PrivateKey - // public key - pubKey *pubKeyECDSA -} - -var _ PrivateKey = (*prKeyECDSA)(nil) - // Algorithm returns the algo related to the private key -func (sk *prKeyECDSA) Algorithm() SigningAlgorithm { - return sk.alg.algo +func (a *ecdsaContext) Algorithm() SigningAlgorithm { + return a.algo } -// Size returns the length of the private key in bytes -func (sk *prKeyECDSA) Size() int { - return bitsToBytes((sk.alg.curve.Params().N).BitLen()) +type prKeyCommonECDSA struct { + // ECDSA context + *ecdsaContext } -// PublicKey returns the public key associated to the private key -func (sk *prKeyECDSA) PublicKey() PublicKey { - // construct the public key once - if sk.pubKey == nil { - sk.pubKey = &pubKeyECDSA{ - alg: sk.alg, - goPubKey: &sk.goPrKey.PublicKey, - } - } - return sk.pubKey +// Size returns the length of the private key in bytes +func (sk *prKeyCommonECDSA) Size() int { + return bitsToBytes((sk.curveN).BitLen()) } -// given a private key (d), returns a raw encoding bytes(d) in big endian -// padded to the private key length -func (sk *prKeyECDSA) rawEncode() []byte { - skBytes := sk.goPrKey.D.Bytes() - nLen := bitsToBytes((sk.alg.curve.Params().N).BitLen()) - skEncoded := make([]byte, nLen) - // pad sk with zeroes - copy(skEncoded[nLen-len(skBytes):], skBytes) - return skEncoded +// prKeyCommonECDSAString returns the string representation of an ECDSA private key. +// It is used by all ECDSA private keys regardless of the curve. +func prKeyCommonECDSAString(sk PrivateKey) string { + return fmt.Sprintf("%#x", sk.Encode()) } -// Encode returns a byte representation of a private key. -// a simple raw byte encoding in big endian is used for all curves -func (sk *prKeyECDSA) Encode() []byte { - return sk.rawEncode() +// pubKeyCommonECDSAString returns the string representation of an ECDSA public key. +// It is used by all ECDSA public keys regardless of the curve. +func pubKeyCommonECDSAString(pk PublicKey) string { + return fmt.Sprintf("%#x", pk.Encode()) } // Equals test the equality of two private keys -func (sk *prKeyECDSA) Equals(other PrivateKey) bool { - // check the key type - otherECDSA, ok := other.(*prKeyECDSA) - if !ok { - return false - } - // check the curve - if sk.alg.curve != otherECDSA.alg.curve { +func prKeyCommonECDSAEquals(sk, other PrivateKey) bool { + // check the algorithm + if sk.Algorithm() != other.Algorithm() { return false } - return sk.goPrKey.D.Cmp(otherECDSA.goPrKey.D) == 0 + // check the scalar + return bytes.Equal(sk.Encode(), other.Encode()) } -// String returns the hex string representation of the key. -func (sk *prKeyECDSA) String() string { - return fmt.Sprintf("%#x", sk.Encode()) -} - -// pubKeyECDSA is the public key of ECDSA, it implements PublicKey -type pubKeyECDSA struct { - // the signature algo - alg *ecdsaAlgo - // public key data - goPubKey *ecdsa.PublicKey -} - -var _ PublicKey = (*pubKeyECDSA)(nil) - -// Algorithm returns the the algo related to the private key -func (pk *pubKeyECDSA) Algorithm() SigningAlgorithm { - return pk.alg.algo +type pubKeyCommonECDSA struct { + // ECDSA context + *ecdsaContext } // Size returns the length of the public key in bytes -func (pk *pubKeyECDSA) Size() int { - return 2 * bitsToBytes((pk.goPubKey.Params().P).BitLen()) -} - -// EncodeCompressed returns a compressed encoding according to X9.62 section 4.3.6. -// This compressed representation uses an extra byte to disambiguate parity. -// The expected input is a public key (x,y). -// -// Receiver point is guaranteed to be on curve and to be non-infinity because -// the package does not allow constructing infinity points or points not on curve. -func (pk *pubKeyECDSA) EncodeCompressed() []byte { - return elliptic.MarshalCompressed(pk.goPubKey.Curve, pk.goPubKey.X, pk.goPubKey.Y) -} - -// `rawEncode` returns a raw uncompressed encoding `bytes(x) || bytes(y)` given a public key (x,y). -// x and y are padded to the field size. -func (pk *pubKeyECDSA) rawEncode() []byte { - xBytes := pk.goPubKey.X.Bytes() - yBytes := pk.goPubKey.Y.Bytes() - Plen := bitsToBytes((pk.alg.curve.Params().P).BitLen()) - pkEncoded := make([]byte, 2*Plen) - // pad the public key coordinates with zeroes - copy(pkEncoded[Plen-len(xBytes):], xBytes) - copy(pkEncoded[2*Plen-len(yBytes):], yBytes) - return pkEncoded -} - -// Encode returns a byte representation of a public key. -// a simple uncompressed raw encoding X||Y is used for all curves -// X and Y are the big endian byte encoding of the x and y coordinates of the public key -func (pk *pubKeyECDSA) Encode() []byte { - return pk.rawEncode() +func (pk *pubKeyCommonECDSA) Size() int { + return 2 * bitsToBytes(pk.curveP.BitLen()) } // Equals test the equality of two private keys -func (pk *pubKeyECDSA) Equals(other PublicKey) bool { - // check the key type - otherECDSA, ok := other.(*pubKeyECDSA) - if !ok { +func pubKeyCommonECDSAEquals(pk, other PublicKey) bool { + // check the algorithm + if pk.Algorithm() != other.Algorithm() { return false } - // check the curve - if pk.alg.curve != otherECDSA.alg.curve { - return false - } - return (pk.goPubKey.X.Cmp(otherECDSA.goPubKey.X) == 0) && - (pk.goPubKey.Y.Cmp(otherECDSA.goPubKey.Y) == 0) + // check the point + return bytes.Equal(pk.Encode(), other.Encode()) } -// String returns the hex string representation of the key. -func (pk *pubKeyECDSA) String() string { - return fmt.Sprintf("%#x", pk.Encode()) +// Helper function to pad two big integers to "size" bytes and concatenate them. +// This helper is needed in serializations in ECDSA implementation. +// It assumes the output buffer has at least 2*size byte-length +func padToSizeAndConcat(output []byte, a, b *big.Int, size int) { + a.FillBytes(output[:size]) + b.FillBytes(output[size:]) +} + +// Helper function to read two big integers of "size" bytes each from a concatenate input buffer. +// This helper is needed when deserializing. +// It assumes the input buffer has at least 2*size byte-length. +func readTwoBigInts(input []byte, size int) (*big.Int, *big.Int) { + a := new(big.Int).SetBytes(input[:size]) + b := new(big.Int).SetBytes(input[size : 2*size]) + return a, b } diff --git a/ecdsa_p256.go b/ecdsa_p256.go new file mode 100644 index 00000000..d50d76dc --- /dev/null +++ b/ecdsa_p256.go @@ -0,0 +1,275 @@ +/* + * Flow Crypto + * + * Copyright Flow Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + + "github.com/onflow/crypto/hash" +) + +// ECDSA implementation on NIST-P256 is based on https://pkg.go.dev/crypto and https://pkg.go.dev/crypto/elliptic +// This implementation is not resistant against side-channel attacks or fault attacks. + +const ( + nLenP256 = 32 + pLenP256 = 32 +) + +var ( + // NIST P256 + SignatureLenECDSAP256 = 2 * nLenP256 + PrKeyLenECDSAP256 = nLenP256 + // PubKeyLenECDSAP256 is the size of uncompressed points on P256 + PubKeyLenECDSAP256 = 2 * pLenP256 +) + +// context of ECDSA on NIST P-256 +var p256Instance *ecdsaContext + +func initECDSAP256() { + curve := elliptic.P256() + p256Instance = &(ecdsaContext{ + curveP: curve.Params().P, + curveN: curve.Params().N, + algo: ECDSAP256, + }) +} + +// prKeyECDSAP256 is the private key of ECDSA on P256, it implements the interface PrivateKey +type prKeyECDSAP256 struct { + // ECDSA generic private key + *prKeyCommonECDSA + // go ecdsa standard lib private key + goPrKey *ecdsa.PrivateKey + // public key + pubKey *pubKeyECDSAP256 +} + +var _ PrivateKey = (*prKeyECDSAP256)(nil) + +// pubKeyECDSAP256 is the public key of ECDSA on P256, it implements PublicKey +type pubKeyECDSAP256 struct { + // ECDSA generic public key + *pubKeyCommonECDSA + // go ecdsa standard lib public key + goPubKey *ecdsa.PublicKey +} + +var _ PublicKey = (*pubKeyECDSAP256)(nil) + +func privateKeyECDSAP256(a *ecdsaContext, dBytes []byte) (*prKeyECDSAP256, error) { + internalSK, err := ecdsa.ParseRawPrivateKey(elliptic.P256(), dBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse raw private key: %w", err) + } + sk := &prKeyECDSAP256{ + prKeyCommonECDSA: &prKeyCommonECDSA{a}, + goPrKey: internalSK, + pubKey: nil, // public key is not constructed + } + return sk, nil +} + +// Sign signs an array of bytes +// +// The resulting signature is the concatenation bytes(r)||bytes(s), +// where r and s are padded to the curve order size. +// The private key is read only while sha2 and sha3 hashers are +// modified temporarily. +// +// The function returns: +// - (false, errNilHasher) if a hasher is nil +// - (false, invalidHasherSizeError) when the hasher's output size is less than the curve order (currently 32 bytes). +// - (nil, error) if an unexpected error occurs +// - (signature, nil) otherwise +func (sk *prKeyECDSAP256) Sign(msg []byte, hasher hash.Hasher) (Signature, error) { + hash, err := sk.checkAlgoAndComputeHash(msg, hasher) + if err != nil { + return nil, err + } + r, s, err := ecdsa.Sign(rand.Reader, sk.goPrKey, hash) + if err != nil { + return nil, fmt.Errorf("ECDSA sign failed: %w", err) + } + + signature := make([]byte, 2*nLenP256) + padToSizeAndConcat(signature, r, s, nLenP256) + return signature, nil +} + +// String returns the hex string representation of the private key +func (sk *prKeyECDSAP256) String() string { + return prKeyCommonECDSAString(sk) +} + +// returns a publicKeyECDSAP256 from (bytes(x) || bytes(y)) bytes +func publicKeyECDSAP256(a *ecdsaContext, XYBytes []byte) (*pubKeyECDSAP256, error) { + // deserialization uses SEC1 version 2 (https://www.secg.org/sec1-v2.pdf section 2.3.3) + // and includes on curve check. + // The bytes serialization for non-infinity points is `0x04 || X || Y` and infinity point should be rejected anyway + parsingBytes := append([]byte{ecEncodingUncompressed}, XYBytes...) + + internalPK, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), parsingBytes) + if err != nil { + return nil, invalidInputsErrorf("input point has invalid coordinates or is not on curve: %w", err) + } + return &pubKeyECDSAP256{ + &pubKeyCommonECDSA{p256Instance}, + internalPK, + }, nil +} + +// String returns the hex string representation of the public key +func (pk *pubKeyECDSAP256) String() string { + return pubKeyCommonECDSAString(pk) +} + +// PublicKey returns the public key associated to the private key +func (sk *prKeyECDSAP256) PublicKey() PublicKey { + // construct the public key once + if sk.pubKey == nil { + sk.pubKey = &pubKeyECDSAP256{ + pubKeyCommonECDSA: &pubKeyCommonECDSA{p256Instance}, + goPubKey: &sk.goPrKey.PublicKey, + } + } + return sk.pubKey +} + +// Verify verifies a signature of an input data under the public key. +// +// If the input signature slice has an invalid length or fails to deserialize into valid +// scalars, the function returns false without an error. +// +// Public keys are read only, sha2 and sha3 hashers are +// modified temporarily. +// +// The function returns: +// - (false, errNilHasher) if a hasher is nil +// - (false, invalidHasherSizeError) when the hasher's output size is less than the curve order (currently 32 bytes). +// - (false, error) if an unexpected error occurs +// - (validity, nil) otherwise +func (pk *pubKeyECDSAP256) Verify(sig Signature, data []byte, alg hash.Hasher) (bool, error) { + h, err := pk.checkAlgoAndComputeHash(data, alg) + if err != nil { + return false, err + } + if len(sig) != SignatureLenECDSAP256 { + return false, nil + } + + r, s := readTwoBigInts(sig, nLenP256) + return ecdsa.Verify(pk.goPubKey, h, r, s), nil +} + +// given a private key (d), returns a raw encoding bytes(d) in big endian +// padded to the private key length +func (sk *prKeyECDSAP256) rawEncode() []byte { + skBytes, err := sk.goPrKey.Bytes() + if err != nil { + // not expected to happen since the private key is generated by this package and should be valid + panic(fmt.Sprintf("failed to encode private key: %v", err)) + } + return skBytes +} + +// Encode returns a byte representation of a private key. +// a simple raw byte encoding in big endian is used for all curves +func (sk *prKeyECDSAP256) Encode() []byte { + return sk.rawEncode() +} + +// Equals test the equality of two private keys +func (sk *prKeyECDSAP256) Equals(other PrivateKey) bool { + return prKeyCommonECDSAEquals(sk, other) +} + +// Equals test the equality of two public keys +func (pk *pubKeyECDSAP256) Equals(other PublicKey) bool { + return pubKeyCommonECDSAEquals(pk, other) +} + +// `rawEncode` returns a raw uncompressed encoding `bytes(x) || bytes(y)` given a public key (x,y). +// x and y are padded to the field size. +func (pk *pubKeyECDSAP256) rawEncode() []byte { + bytes, err := pk.goPubKey.Bytes() + if err != nil { + // not expected to happen since the public keys generated by this package only + // use elliptic.P256 + panic(fmt.Sprintf("unexpected failure to encode public key: %v", err)) + } + return bytes[1:] // remove the uncompressed point prefix +} + +// Encode returns a byte representation of a public key. +// a simple uncompressed raw encoding X||Y is used for all curves +// X and Y are the big endian byte encoding of the x and y coordinates of the public key +func (pk *pubKeyECDSAP256) Encode() []byte { + return pk.rawEncode() +} + +// EncodeCompressed returns a compressed encoding according to X9.62 section 4.3.6. +// This compressed representation uses an extra byte to disambiguate parity. +// The expected input is a public key (x,y). +// +// Receiver point is guaranteed to be on curve and to be non-infinity because +// the package does not allow constructing infinity points or points not on curve. +func (pk *pubKeyECDSAP256) EncodeCompressed() []byte { + bytes := pk.rawEncode() + // read X and Y from the encoding + x, y := readTwoBigInts(bytes, pLenP256) + // use elliptic.MarshalCompressed to get the compressed encoding + return elliptic.MarshalCompressed(elliptic.P256(), x, y) +} + +// p256DecodePublicKeyCompressed returns a non-infinity P-256 public key given the bytes of a compressed +// public key according to X9.62 section 4.3.6. +// Note that infinity point serialization isn't defined in this package so the input (or output) +// can never represent an infinity point. +// Error Returns: +// - invalidInputsError if the input isn't a valid key serialization +// on the given curve. +func p256DecodePublicKeyCompressed(pkBytes []byte) (*pubKeyECDSAP256, error) { + + expectedLen := pLenP256 + 1 + if len(pkBytes) != expectedLen { + return nil, invalidInputsErrorf("incorrect input length, expected %d, got %d", expectedLen, len(pkBytes)) + } + x, y := elliptic.UnmarshalCompressed(elliptic.P256(), pkBytes) + if x == nil || y == nil { + return nil, invalidInputsErrorf("input %x isn't a compressed serialization of a point on P256", pkBytes) + } + uncompressedPointBytes := make([]byte, 2*pLenP256+1) + uncompressedPointBytes[0] = ecEncodingUncompressed + padToSizeAndConcat(uncompressedPointBytes[1:], x, y, pLenP256) + + internalPK, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), uncompressedPointBytes) + if err != nil { + // unexpected error since prior deserialization succeeded + return nil, invalidInputsErrorf("unexpected error: input is not a point on curve P-256: %w", err) + } + return &pubKeyECDSAP256{ + &pubKeyCommonECDSA{p256Instance}, + internalPK, + }, nil +} diff --git a/ecdsa_secp256k1.go b/ecdsa_secp256k1.go new file mode 100644 index 00000000..f23fdded --- /dev/null +++ b/ecdsa_secp256k1.go @@ -0,0 +1,267 @@ +/* + * Flow Crypto + * + * Copyright Flow Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package crypto + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/crypto/secp256k1" + + "github.com/onflow/crypto/hash" +) + +// ECDSA implementation on SECG secp256k1 is based on https://pkg.go.dev/github.com/ethereum/go-ethereum/crypto/secp256k1 + +// This implementation is not resistant against side-channel attacks or fault attacks. + +// curve parameters for SECG secp256k1 https://www.secg.org/sec2-v2.pdf +const ( + secp256k1PHex = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F" + secp256k1NHex = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + + nLenSecp256k1 = 32 + pLenSecp256k1 = 32 +) + +var ( + // SECG secp256k1 + SignatureLenECDSASecp256k1 = 2 * nLenSecp256k1 + PrKeyLenECDSASecp256k1 = nLenSecp256k1 + // PubKeyLenECDSASecp256k1 is the size of uncompressed points on secp256k1 + PubKeyLenECDSASecp256k1 = 2 * pLenSecp256k1 +) + +// context of ECDSA on SECG secp256k1 curve https://www.secg.org/sec2-v2.pdf +var secp256k1Instance *ecdsaContext + +func initECDSASecp256k1() { + curveP, ok := new(big.Int).SetString(secp256k1PHex, 16) + if !ok { + panic("failed to initialize ECDSA with secp256k1 curve") + } + curveN, ok := new(big.Int).SetString(secp256k1NHex, 16) + if !ok { + panic("failed to initialize ECDSA with secp256k1 curve") + } + secp256k1Instance = &(ecdsaContext{ + curveP: curveP, + curveN: curveN, + algo: ECDSASecp256k1, + }) +} + +// prKeyECDSASecp256k1 is the private key of ECDSA on SECG secp256k1, it implements PrivateKey +type prKeyECDSASecp256k1 struct { + // ECDSA generic private key + *prKeyCommonECDSA + // bytes(D) of private scalar D in big endian, padded to the curve order size (32 bytes) + dBytes []byte + // public key + pubKey *pubKeyECDSASecp256k1 +} + +var _ PrivateKey = (*prKeyECDSASecp256k1)(nil) + +// pubKeyECDSASecp256k1 is the public key of ECDSA on SECG secp256k1, it implements PublicKey +type pubKeyECDSASecp256k1 struct { + // ECDSA generic public key + *pubKeyCommonECDSA + // 0x4 || bytes(x) || bytes(y) (65 bytes) where x and y are the coordinates of the public key point, padded to the field size (32 bytes) + pkBytes []byte +} + +var _ PublicKey = (*pubKeyECDSASecp256k1)(nil) + +func privateKeyECDSASecp256k1(a *ecdsaContext, dBytes []byte) *prKeyECDSASecp256k1 { + sk := &prKeyECDSASecp256k1{ + prKeyCommonECDSA: &prKeyCommonECDSA{a}, + dBytes: dBytes, + pubKey: nil, // public key is not constructed + } + return sk +} + +// Sign signs an array of bytes +// +// The resulting signature is the concatenation bytes(r)||bytes(s), +// where r and s are padded to the curve order size. +// The private key is read only while sha2 and sha3 hashers are +// modified temporarily. +// +// The function returns: +// - (false, errNilHasher) if a hasher is nil +// - (false, invalidHasherSizeError) when the hasher's output size is less than the curve order (currently 32 bytes). +// - (nil, error) if an unexpected error occurs +// - (signature, nil) otherwise +func (sk *prKeyECDSASecp256k1) Sign(msg []byte, hasher hash.Hasher) (Signature, error) { + hash, err := sk.checkAlgoAndComputeHash(msg, hasher) + if err != nil { + return nil, err + } + // truncate the hash to the curve order size, as specified in FIPS 186-4 section 6.4 + // and as required by the secp256k1 package signing function + hash = hash[:nLenSecp256k1] + signature, err := secp256k1.Sign(hash, sk.dBytes) + if err != nil { + return nil, fmt.Errorf("failed to sign hash: %w", err) + } + // remove the EC recover byte (last byte) + return signature[:SignatureLenECDSASecp256k1], nil +} + +// String returns the hex string representation of the private key +func (sk *prKeyECDSASecp256k1) String() string { + return prKeyCommonECDSAString(sk) +} + +func publicKeyECDSASecp256k1(a *ecdsaContext, XYBytes []byte) (*pubKeyECDSASecp256k1, error) { + pLen := bitsToBytes(a.curveP.BitLen()) + x, y := readTwoBigInts(XYBytes, pLen) + // `IsOnCurve` is not deprecated in btcec's type `KoblitzCurve` + if !secp256k1.S256().IsOnCurve(x, y) { + return nil, invalidInputsErrorf("input point has invalid coordinates or is not on curve") + } + return &pubKeyECDSASecp256k1{ + &pubKeyCommonECDSA{secp256k1Instance}, + append([]byte{ecEncodingUncompressed}, XYBytes...), + // XYBytes is already the raw uncompressed encoding `bytes(x) || bytes(y)` + }, nil +} + +// String returns the hex string representation of the public key +func (pk *pubKeyECDSASecp256k1) String() string { + return pubKeyCommonECDSAString(pk) +} + +// PublicKey returns the public key associated to the private key +func (sk *prKeyECDSASecp256k1) PublicKey() PublicKey { + // construct the public key once + if sk.pubKey == nil { + x, y := secp256k1.S256().ScalarBaseMult(sk.dBytes) + pkBytes := make([]byte, 1+2*pLenSecp256k1) + pkBytes[0] = ecEncodingUncompressed + // pad x and y to the field size and concatenate them + padToSizeAndConcat(pkBytes[1:], x, y, pLenSecp256k1) + sk.pubKey = &pubKeyECDSASecp256k1{ + pubKeyCommonECDSA: &pubKeyCommonECDSA{secp256k1Instance}, + pkBytes: pkBytes, + } + } + return sk.pubKey +} + +// Verify verifies a signature of an input data under the public key. +// +// If the input signature slice has an invalid length or fails to deserialize into valid +// scalars, the function returns false without an error. +// +// Public keys are read only, sha2 and sha3 hashers are +// modified temporarily. +// +// The function returns: +// - (false, errNilHasher) if a hasher is nil +// - (false, invalidHasherSizeError) when the hasher's output size is less than the curve order (currently 32 bytes). +// - (false, error) if an unexpected error occurs +// - (validity, nil) otherwise +func (pk *pubKeyECDSASecp256k1) Verify(sig Signature, msg []byte, hasher hash.Hasher) (bool, error) { + hash, err := pk.checkAlgoAndComputeHash(msg, hasher) + if err != nil { + return false, err + } + if len(sig) != 2*nLenSecp256k1 { + return false, nil + } + + return secp256k1.VerifySignature(pk.pkBytes, hash, sig), nil +} + +// given a private key (d), returns a raw encoding bytes(d) in big endian +// padded to the private key length +func (sk *prKeyECDSASecp256k1) rawEncode() []byte { + return sk.dBytes +} + +// Encode returns a byte representation of a private key. +// a simple raw byte encoding in big endian is used for all curves +func (sk *prKeyECDSASecp256k1) Encode() []byte { + return sk.rawEncode() +} + +// Equals test the equality of two private keys +func (sk *prKeyECDSASecp256k1) Equals(other PrivateKey) bool { + return prKeyCommonECDSAEquals(sk, other) +} + +// Equals test the equality of two public keys +func (pk *pubKeyECDSASecp256k1) Equals(other PublicKey) bool { + return pubKeyCommonECDSAEquals(pk, other) +} + +// `rawEncode` returns a raw uncompressed encoding `bytes(x) || bytes(y)` given a public key (x,y). +// x and y are padded to the field size. +func (pk *pubKeyECDSASecp256k1) rawEncode() []byte { + // skip the uncompressed encoding byte + return pk.pkBytes[1:] +} + +// Encode returns a byte representation of a public key. +// a simple uncompressed raw encoding X||Y is used for all curves +// X and Y are the big endian byte encoding of the x and y coordinates of the public key +func (pk *pubKeyECDSASecp256k1) Encode() []byte { + return pk.rawEncode() +} + +// EncodeCompressed returns a compressed encoding according to X9.62 section 4.3.6. +// This compressed representation uses an extra byte to disambiguate parity. +// The expected input is a public key (x,y). +// +// Receiver point is guaranteed to be on curve and to be non-infinity because +// the package does not allow constructing infinity points or points not on curve. +func (pk *pubKeyECDSASecp256k1) EncodeCompressed() []byte { + x, y := readTwoBigInts(pk.pkBytes[1:], pLenSecp256k1) + // read X and Y from the encoding + return secp256k1.CompressPubkey(x, y) +} + +// p256DecodePublicKeyCompressed returns a non-infinity P-256 public key given the bytes of a compressed +// public key according to X9.62 section 4.3.6. +// Note that infinity point serialization isn't defined in this package so the input (or output) +// can never represent an infinity point. +// Error Returns: +// - invalidInputsError if the input isn't a valid key serialization +// on the given curve. +func secp256k1DecodePublicKeyCompressed(pkBytes []byte) (*pubKeyECDSASecp256k1, error) { + expectedLen := pLenSecp256k1 + 1 + if len(pkBytes) != expectedLen { + return nil, invalidInputsErrorf("incorrect input length, expected %d, got %d", expectedLen, len(pkBytes)) + } + x, y := secp256k1.DecompressPubkey(pkBytes) + if x == nil || y == nil { + return nil, invalidInputsErrorf("input %x isn't a compressed serialization of a point on secp256k1", pkBytes) + } + uncompressedPkBytes := make([]byte, 1+2*pLenSecp256k1) + uncompressedPkBytes[0] = ecEncodingUncompressed + padToSizeAndConcat(uncompressedPkBytes[1:], x, y, pLenSecp256k1) + + return &pubKeyECDSASecp256k1{ + &pubKeyCommonECDSA{secp256k1Instance}, + uncompressedPkBytes, + }, nil +} diff --git a/ecdsa_test.go b/ecdsa_test.go index 30ba13d4..970e5203 100644 --- a/ecdsa_test.go +++ b/ecdsa_test.go @@ -24,7 +24,6 @@ import ( "crypto/elliptic" crand "crypto/rand" - "math/big" "github.com/btcsuite/btcd/btcec/v2" "github.com/stretchr/testify/assert" @@ -178,7 +177,6 @@ func TestECDSAEncodeDecode(t *testing.T) { pk, err := DecodePublicKey(curve, pkBytes) require.Error(t, err, "point is not on curve") assert.True(t, IsInvalidInputsError(err)) - assert.ErrorContains(t, err, "input is not a point on curve") assert.Nil(t, pk) }) @@ -201,13 +199,11 @@ func TestECDSAEncodeDecode(t *testing.T) { require.NoError(t, err) _, err = DecodePublicKey(curve, invalidPk1) assert.Error(t, err) - assert.ErrorContains(t, err, "at least one coordinate is larger than the field prime for") // invalidpk2 with y >= p invalidPk2, err := hex.DecodeString(invalidPK2s[curve]) require.NoError(t, err) _, err = DecodePublicKey(curve, invalidPk2) assert.Error(t, err) - assert.ErrorContains(t, err, "at least one coordinate is larger than the field prime for") }) } } @@ -270,52 +266,7 @@ func TestECDSAPublicKeyComputation(t *testing.T) { } } -// TestGoECDSAP256PrivateKeyConstruction exercises `goecdsaPrivateKey` for P-256 directly. -// -// This is a regression test for a panic that surfaced on Go 1.26: -// when constructing the key, the public affine coordinates `X`/`Y` are still nil -// (we are in the middle of computing them via base scalar multiplication). -// Since Go 1.26, `(*ecdsa.PrivateKey).ECDH` serializes the key through -// `(*PrivateKey).Bytes`, which reads `X`/`Y` and therefore panicked on the nil deref. -// The fix builds the ecdh key from the scalar bytes instead. -// -// The test asserts the construction does not panic and that the computed public key -// matches a known test vector, so it guards both the crash and the correctness of the -// scalar-based code path. -func TestGoECDSAP256PrivateKeyConstruction(t *testing.T) { - // scalar / expected public key pair, identical to the P-256 vector in - // TestECDSAPublicKeyComputation - const skHex = "6e37a39c31a05181bf77919ace790efd0bdbcaf42b5a52871fc112fceb918c95" - const xHex = "78a80dfe190a6068be8ddf05644c32d2540402ffc682442f6a9eeb96125d8681" - const yHex = "3789f92cf4afabf719aaba79ecec54b27e33a188f83158f6dd15ecb231b49808" - - skBytes, err := hex.DecodeString(skHex) - require.NoError(t, err) - d := new(big.Int).SetBytes(skBytes) - - // the call panicked on Go 1.26 before the fix - require.NotPanics(t, func() { - priv, err := goecdsaPrivateKey(elliptic.P256(), d) - require.NoError(t, err) - require.NotNil(t, priv) - - // the scalar must be preserved - assert.Equal(t, 0, priv.D.Cmp(d)) - - // the computed public affine coordinates must match the known vector - expectedX, ok := new(big.Int).SetString(xHex, 16) - require.True(t, ok) - expectedY, ok := new(big.Int).SetString(yHex, 16) - require.True(t, ok) - assert.Equal(t, 0, priv.PublicKey.X.Cmp(expectedX)) - assert.Equal(t, 0, priv.PublicKey.Y.Cmp(expectedY)) - - // the computed point must be on the curve - assert.True(t, elliptic.P256().IsOnCurve(priv.PublicKey.X, priv.PublicKey.Y)) - }) -} - -func TestSignatureFormatCheck(t *testing.T) { +func TestECDSASignatureFormatCheck(t *testing.T) { for _, curve := range ecdsaCurves { t.Run("valid signature", func(t *testing.T) { diff --git a/go.mod b/go.mod index 344580d9..4e27765d 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ go 1.26.0 require ( github.com/btcsuite/btcd/btcec/v2 v2.3.4 + github.com/ethereum/go-ethereum v1.17.5 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.36.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.54.0 gonum.org/v1/gonum v0.16.0 pgregory.net/rapid v0.4.7 ) @@ -14,9 +15,8 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/kr/pretty v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sys v0.47.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 16ac77ab..3e14abed 100644 --- a/go.sum +++ b/go.sum @@ -6,24 +6,27 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/ethereum/go-ethereum v1.17.5 h1:o9BIXs2Q/3cPHVxw49n+Zjn2i6rB9TOXatev46duOC4= +github.com/ethereum/go-ethereum v1.17.5/go.mod h1:vz2YvG7RewA4sFHTgzLyW+WmFG1N4jfk/hgXQVhhn9c= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/sign.go b/sign.go index 9d5000fe..3d368879 100644 --- a/sign.go +++ b/sign.go @@ -19,11 +19,8 @@ package crypto import ( - "crypto/elliptic" "fmt" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/onflow/crypto/hash" ) @@ -83,20 +80,10 @@ func newSigner(algo SigningAlgorithm) (signer, error) { // Initialize the context of all algos func init() { // ECDSA - p256Instance = &(ecdsaAlgo{ - curve: elliptic.P256(), - algo: ECDSAP256, - }) - secp256k1Instance = &(ecdsaAlgo{ - curve: btcec.S256(), - algo: ECDSASecp256k1, - }) + initECDSA() // BLS initBLS12381() - blsInstance = &blsBLS12381Algo{ - algo: BLSBLS12381, - } } // SignatureFormatCheck verifies the format of a serialized signature, From 16c5655f1b2aa2cf43689321203ee4914531e935 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Mon, 3 Aug 2026 13:59:20 -0500 Subject: [PATCH 03/12] towards isolaing ECDSA secp256k1 in cgo mode only --- ecdsa_secp256k1.go | 2 ++ no_cgo.go | 29 +++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/ecdsa_secp256k1.go b/ecdsa_secp256k1.go index f23fdded..a2c75fd6 100644 --- a/ecdsa_secp256k1.go +++ b/ecdsa_secp256k1.go @@ -1,3 +1,5 @@ +//go:build cgo && !no_cgo + /* * Flow Crypto * diff --git a/no_cgo.go b/no_cgo.go index fd95f65c..aae3d51a 100644 --- a/no_cgo.go +++ b/no_cgo.go @@ -15,9 +15,12 @@ import ( ) const ( - SignatureLenBLSBLS12381 = 0 - PubKeyLenBLSBLS12381 = 0 - PrKeyLenBLSBLS12381 = 0 + SignatureLenBLSBLS12381 = 0 + PubKeyLenBLSBLS12381 = 0 + PrKeyLenBLSBLS12381 = 0 + SignatureLenECDSASecp256k1 = 0 + PrKeyLenECDSASecp256k1 = 0 + PubKeyLenECDSASecp256k1 = 0 ) func initBLS12381() {} @@ -30,7 +33,6 @@ type blsBLS12381Algo struct { algo SigningAlgorithm } -// BLS context on the BLS 12-381 curve var blsInstance *blsBLS12381Algo func (a *blsBLS12381Algo) generatePrivateKey(ikm []byte) (PrivateKey, error) { @@ -192,3 +194,22 @@ func IsNotBLSKeyError(err error) bool { func IsInvalidSignatureError(err error) bool { panic(withFeature("BLS multi-sig")) } + +func initECDSASecp256k1() { + panic(withFeature("ECDSA SECP256k1")) +} + +type pubKeyECDSASecp256k1 struct{} +type prKeyECDSASecp256k1 struct{} + +func secp256k1DecodePublicKeyCompressed(pkBytes []byte) (*pubKeyECDSASecp256k1, error) { + panic(withFeature("ECDSA SECP256k1")) +} + +func privateKeyECDSASecp256k1(a *ecdsaContext, dBytes []byte) *prKeyECDSASecp256k1 { + panic(withFeature("ECDSA SECP256k1")) +} + +func publicKeyECDSASecp256k1(a *ecdsaContext, XYBytes []byte) (*pubKeyECDSASecp256k1, error) { + panic(withFeature("ECDSA SECP256k1")) +} From 975a8e0b26435812a8b4b0367c1aeb24eb802fe1 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Mon, 3 Aug 2026 14:00:40 -0500 Subject: [PATCH 04/12] revert linter exception --- .golangci.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d73fb864..ca737b81 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -10,17 +10,6 @@ linters: - linters: - govet text: "unsafeptr" # disable flagging unsafeptr usage - # ecdsa.go wraps crypto/ecdsa, whose Sign/Verify still consume the raw - # PrivateKey.D and PublicKey.X/Y fields. - # Go 1.26 deprecated direct access to those fields, but the recommended - # replacement API (ecdsa.ParseRawPrivateKey / ParseUncompressedPublicKey / - # (*PrivateKey).Bytes / (*PublicKey).Bytes) supports only the NIST curves and - # rejects secp256k1, which this package supports via btcec's custom - # elliptic.Curve, so the package has to keep using the low-level fields. - - path: (^|/)ecdsa(_test)?\.go$ - linters: - - staticcheck - text: "SA1019" formatters: exclusions: paths: From 430b1ea81d53f7aa7e3f38bedd4d6c444ab1ab66 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Mon, 3 Aug 2026 14:38:07 -0500 Subject: [PATCH 05/12] remove non-cgo mode --- .github/workflows/ci.yml | 11 +- Makefile | 8 -- README.md | 28 +---- bls.go | 2 - bls12381_utils.go | 2 - bls12381_utils_test.go | 2 - bls_crossBLST_test.go | 2 - bls_multisig.go | 2 - bls_test.go | 2 - bls_thresholdsign.go | 2 - bls_thresholdsign_test.go | 2 - dkg_feldmanvss.go | 2 - dkg_feldmanvssq.go | 2 - dkg_jointfeldman.go | 2 - dkg_test.go | 2 - ecdsa_secp256k1.go | 2 - no_cgo.go | 215 -------------------------------------- no_cgo_test.go | 46 -------- spock.go | 2 - spock_test.go | 2 - 20 files changed, 6 insertions(+), 332 deletions(-) delete mode 100644 no_cgo.go delete mode 100644 no_cgo_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4f66357..53141423 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: cache: true - name: run Go tidy run: make go-tidy - - name: Run golangci-lint with cgo + - name: Run golangci-lint env: CGO_ENABLED: 1 uses: golangci/golangci-lint-action@v8 @@ -42,15 +42,6 @@ jobs: version: ${{ env.LINT_VERSION }} # https://github.com/golangci/golangci-lint-action/issues/244 skip-cache: true - - name: Run golangci-lint without cgo - env: - CGO_ENABLED: 0 - uses: golangci/golangci-lint-action@v8 - with: - version: ${{ env.LINT_VERSION }} - args: --build-tags no_cgo - # https://github.com/golangci/golangci-lint-action/issues/244 - skip-cache: true - name: Run Go Fix run: make go-fix - name: Run incorrect builds diff --git a/Makefile b/Makefile index 1fdd5cd5..aba22695 100644 --- a/Makefile +++ b/Makefile @@ -96,15 +96,7 @@ go-lint: go-tidy go-fix test: # root package CGO_ENABLED=1 CGO_CFLAGS=$(ADX_FLAG) go test -coverprofile=$(COVER_PROFILE) $(RACE_FLAG) $(if $(JSON_OUTPUT),-json,) $(if $(VERBOSE),-v,) -#root package without cgo - CGO_ENABLED=0 go test -tags=no_cgo -coverprofile=$(COVER_PROFILE) $(RACE_FLAG) $(if $(JSON_OUTPUT),-json,) $(if $(VERBOSE),-v,) # sub packages go test -coverprofile=$(COVER_PROFILE) $(RACE_FLAG) $(if $(JSON_OUTPUT),-json,) $(if $(VERBOSE),-v,) ./hash go test -coverprofile=$(COVER_PROFILE) $(RACE_FLAG) $(if $(JSON_OUTPUT),-json,) $(if $(VERBOSE),-v,) ./random -# test incorrect builds and make sure they fail -.PHONY: incorrect_builds -incorrect_builds: -# both tests should fail - ! CGO_ENABLED=0 go test - ! CGO_ENABLED=1 CGO_CFLAGS=$(ADX_FLAG) go test -tags=no_cgo diff --git a/README.md b/README.md index 0c78ecac..4b912d94 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,6 @@ import "github.com/onflow/crypto" Building your project with Flow crypto and enabling all the supported algorithms requires using cgo to compile the C code underneath. If cgo isn't enabled by default, the `CGO_ENABLED` environment variable should be set to `1`. -It is also possible to build without cgo (`CGO_ENABLED=0`) but this would disable some primitives (the ones related to BLS). - -### Build with cgo - -Building with cgo is required to support all the algorithms of the module, including the algorithms based on the BLS12-381 curve. If the test or target application crashes with a "Caught SIGILL" exception, rebuild with `CGO_CFLAGS` set to `"-O2 -D__BLST_PORTABLE__"` to disable non-portable code. The runtime error can happen if the CPU doesn't support certain instructions. @@ -53,19 +48,6 @@ GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 go build When using the `go mod vendor` command in your project, [a known issue](https://github.com/golang/go/issues/26366) with the Go vendoring tool prevents cgo dependencies from being copied into your vendor directory. This results in build errors related to the Flow crypto package. External vendoring tools that do copy the entire package files can be used instead of the Go command to resolve the issue. - -### Build without cgo - -It is possible to build without cgo but this requires disabling all primitives based on the BLS12-381 curve (BLS signature, BLS threshold signature, BLS-based DKG, BLS-based SPoCK). -Refer to [algorithms](#algorithms) and [protocols](#protocols) to check the supported features. -Calling any of the non-supported primitives would panic. -In order to avoid accidental builds that result in unwanted crashes, disabling cgo must be confirmed with the `no_cgo` build tag. - -``` -CGO_ENABLED=0 go build -tags=no_cgo -``` - - ## Algorithms ### Hashing and MAC: @@ -86,7 +68,7 @@ All signature schemes use the generic interfaces of `PrivateKey` and `PublicKey` * ephemeral key is derived from the private key, hash and the system entropy (based on https://golang.org/pkg/crypto/ecdsa/). * supports NIST P-256 (secp256r1) and secp256k1 curves. - * BLS (requires cgo) + * BLS * supports [BLS12-381](https://electriccoin.co/blog/new-snark-curve/) curve. * is implementing the minimal-signature-size variant: signatures in G1 and public keys in G2. @@ -114,7 +96,7 @@ All signature schemes use the generic interfaces of `PrivateKey` and `PublicKey` ### Threshold Signature - * BLS-based threshold signature (requires cgo) + * BLS-based threshold signature * [non interactive](https://www.iacr.org/archive/pkc2003/25670031/25670031.pdf) threshold signature reconstruction. * supports only BLS 12-381 curve with the same features above. * (t+1) signatures are required to reconstruct the threshold signature. @@ -126,16 +108,16 @@ All signature schemes use the generic interfaces of `PrivateKey` and `PublicKey` All supported Distributed Key Generation protocols are [discrete log based](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.50.2737&rep=rep1&type=pdf) and are implemented for the same BLS setup on the BLS 12-381 curve. The protocols generate key sets for the BLS-based threshold signature. - * Feldman VSS (requires cgo) + * Feldman VSS * simple verifiable secret sharing with a single dealer. * the library does not implement the communication channels between participants. The caller should implement the methods `PrivateSend` (1-to-1 messaging) and `Broadcast` (1-to-n messaging) * 1-to-1 messaging must be a private channel, the caller must make sure the channel preserves confidentialiy and authenticates the sender. * 1-to-n broadcasting is a reliable broadcast, where honest senders are able to reach all honest receivers, and where all honest receivers end up with the same received messages. The channel should also authenticate the broadcaster. * It is recommended that both communication channels are unique per protocol instance. This could be achieved by prepending the messages to send/broadcast by a unique protocol instance ID. - * Feldman VSS Qual (requires cgo) + * Feldman VSS Qual * an extension of the simple Feldman VSS. * implements a complaint mechanism to qualify/disqualify the dealer. - * Joint Feldman (Pedersen) (requires cgo) + * Joint Feldman (Pedersen) * distributed generation. * based on parallel instances of Feldman VSS Qual, each with a different dealer. * same assumptions about the communication channels as in Feldman VSS. diff --git a/bls.go b/bls.go index e371c60e..e80465cf 100644 --- a/bls.go +++ b/bls.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/bls12381_utils.go b/bls12381_utils.go index aa939c79..784f5389 100644 --- a/bls12381_utils.go +++ b/bls12381_utils.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/bls12381_utils_test.go b/bls12381_utils_test.go index f392be05..35a37025 100644 --- a/bls12381_utils_test.go +++ b/bls12381_utils_test.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/bls_crossBLST_test.go b/bls_crossBLST_test.go index b5f47f60..a0d04b60 100644 --- a/bls_crossBLST_test.go +++ b/bls_crossBLST_test.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/bls_multisig.go b/bls_multisig.go index dbb1625e..03f55e6e 100644 --- a/bls_multisig.go +++ b/bls_multisig.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/bls_test.go b/bls_test.go index f7e7893d..4fbf4fe4 100644 --- a/bls_test.go +++ b/bls_test.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/bls_thresholdsign.go b/bls_thresholdsign.go index c7de6761..f790df7f 100644 --- a/bls_thresholdsign.go +++ b/bls_thresholdsign.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/bls_thresholdsign_test.go b/bls_thresholdsign_test.go index a69155bf..f8c310f8 100644 --- a/bls_thresholdsign_test.go +++ b/bls_thresholdsign_test.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/dkg_feldmanvss.go b/dkg_feldmanvss.go index 696e9050..959da868 100644 --- a/dkg_feldmanvss.go +++ b/dkg_feldmanvss.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/dkg_feldmanvssq.go b/dkg_feldmanvssq.go index 61e81049..bc1bf72a 100644 --- a/dkg_feldmanvssq.go +++ b/dkg_feldmanvssq.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/dkg_jointfeldman.go b/dkg_jointfeldman.go index 3f82f5b6..14e05c6c 100644 --- a/dkg_jointfeldman.go +++ b/dkg_jointfeldman.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/dkg_test.go b/dkg_test.go index e225d5b9..7d28c546 100644 --- a/dkg_test.go +++ b/dkg_test.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/ecdsa_secp256k1.go b/ecdsa_secp256k1.go index a2c75fd6..f23fdded 100644 --- a/ecdsa_secp256k1.go +++ b/ecdsa_secp256k1.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/no_cgo.go b/no_cgo.go deleted file mode 100644 index aae3d51a..00000000 --- a/no_cgo.go +++ /dev/null @@ -1,215 +0,0 @@ -//go:build !cgo && no_cgo - -package crypto - -// This file enables the build of the library when cgo is disabled, i.e when the environment -// variable `CGO_ENABLED` is set to `0`. -// The build without cgo succeeds but disables all algorithms working with -// the BLS12-381 curve (BLS signature, BLS threshold signature, BLS-based DKG and BLS-SPoCK). -// Any call to any of these algorithms would panic. - -import ( - "fmt" - - "github.com/onflow/crypto/hash" -) - -const ( - SignatureLenBLSBLS12381 = 0 - PubKeyLenBLSBLS12381 = 0 - PrKeyLenBLSBLS12381 = 0 - SignatureLenECDSASecp256k1 = 0 - PrKeyLenECDSASecp256k1 = 0 - PubKeyLenECDSASecp256k1 = 0 -) - -func initBLS12381() {} - -func withFeature(feature string) string { - return fmt.Sprintf("%s is only supported with cgo, rebuild with CGO_ENABLED=1\n", feature) -} - -type blsBLS12381Algo struct { - algo SigningAlgorithm -} - -var blsInstance *blsBLS12381Algo - -func (a *blsBLS12381Algo) generatePrivateKey(ikm []byte) (PrivateKey, error) { - panic(withFeature("BLS signature")) -} - -func (a *blsBLS12381Algo) decodePrivateKey(privateKeyBytes []byte) (PrivateKey, error) { - panic(withFeature("BLS signature")) -} - -func (a *blsBLS12381Algo) decodePublicKey(publicKeyBytes []byte) (PublicKey, error) { - panic(withFeature("BLS signature")) -} - -func (a *blsBLS12381Algo) decodePublicKeyCompressed(publicKeyBytes []byte) (PublicKey, error) { - panic(withFeature("BLS signature")) -} - -func NewExpandMsgXOFKMAC128(domainTag string) hash.Hasher { - panic(withFeature("BLS hasher")) -} - -func IsBLSSignatureIdentity(s Signature) bool { - panic(withFeature("BLS signature")) -} - -func BLSInvalidSignature() Signature { - panic(withFeature("BLS signature")) -} - -func isG2Compressed() bool { - panic(withFeature("BLS12-381 curve")) -} - -func NewBLSThresholdSignatureParticipant( - groupPublicKey PublicKey, - sharePublicKeys []PublicKey, - threshold int, - myIndex int, - myPrivateKey PrivateKey, - message []byte, - dsTag string, -) (ThresholdSignatureParticipant, error) { - panic(withFeature("BLS threshold signature")) -} - -func NewBLSThresholdSignatureInspector( - groupPublicKey PublicKey, - sharePublicKeys []PublicKey, - threshold int, - message []byte, - dsTag string, -) (ThresholdSignatureInspector, error) { - panic(withFeature("BLS threshold signature")) -} - -func BLSReconstructThresholdSignature(size int, threshold int, - shares []Signature, signers []int) (Signature, error) { - _ = duplicatedSignerErrorf("") - _ = notEnoughSharesErrorf("") - panic(withFeature("BLS threshold signature")) -} - -func EnoughShares(threshold int, sharesNumber int) (bool, error) { - panic(withFeature("BLS threshold signature")) -} - -func BLSThresholdKeyGen(size int, threshold int, seed []byte) ([]PrivateKey, - []PublicKey, PublicKey, error) { - panic(withFeature("BLS threshold signature")) -} - -func NewFeldmanVSS(size int, threshold int, myIndex int, - processor DKGProcessor, dealerIndex int) (DKGState, error) { - _, _ = newDKGCommon(size, threshold, myIndex, - processor, dealerIndex) - panic(withFeature("BLS-DKG")) -} - -func NewFeldmanVSSQual(size int, threshold int, myIndex int, - processor DKGProcessor, dealerIndex int) (DKGState, error) { - _ = dkgFailureErrorf("") - _ = dkgInvalidStateTransitionErrorf("") - panic(withFeature("BLS-DKG")) -} - -func NewJointFeldman(size int, threshold int, myIndex int, - processor DKGProcessor) (DKGState, error) { - _ = feldmanVSSShare | feldmanVSSVerifVec | feldmanVSSComplaint | feldmanVSSComplaintAnswer - panic(withFeature("BLS-DKG")) -} - -func SPOCKProve(sk PrivateKey, data []byte, kmac hash.Hasher) (Signature, error) { - panic(withFeature("BLS-SPoCK")) -} - -func SPOCKVerifyAgainstData(pk PublicKey, proof Signature, data []byte, kmac hash.Hasher) (bool, error) { - panic(withFeature("BLS-SPoCK")) -} - -func SPOCKVerify(pk1 PublicKey, proof1 Signature, pk2 PublicKey, proof2 Signature) (bool, error) { - panic(withFeature("BLS-SPoCK")) -} - -func BLSGeneratePOP(sk PrivateKey) (Signature, error) { - panic(withFeature("BLS multi-sig")) -} - -func BLSVerifyPOP(pk PublicKey, s Signature) (bool, error) { - panic(withFeature("BLS multi-sig")) -} - -func AggregateBLSSignatures(sigs []Signature) (Signature, error) { - panic(withFeature("BLS multi-sig")) -} - -func AggregateBLSPrivateKeys(keys []PrivateKey) (PrivateKey, error) { - panic(withFeature("BLS multi-sig")) -} - -func AggregateBLSPublicKeys(keys []PublicKey) (PublicKey, error) { - panic(withFeature("BLS multi-sig")) -} - -func IdentityBLSPublicKey() PublicKey { - panic(withFeature("BLS multi-sig")) -} - -func RemoveBLSPublicKeys(aggKey PublicKey, keysToRemove []PublicKey) (PublicKey, error) { - panic(withFeature("BLS multi-sig")) -} - -func VerifyBLSSignatureOneMessage( - pks []PublicKey, s Signature, message []byte, kmac hash.Hasher, -) (bool, error) { - panic(withFeature("BLS multi-sig")) -} - -func VerifyBLSSignatureManyMessages( - pks []PublicKey, s Signature, messages [][]byte, kmac []hash.Hasher, -) (bool, error) { - panic(withFeature("BLS multi-sig")) -} - -func BatchVerifyBLSSignaturesOneMessage( - pks []PublicKey, sigs []Signature, message []byte, kmac hash.Hasher, -) ([]bool, error) { - panic(withFeature("BLS multi-sig")) -} - -func IsBLSAggregateEmptyListError(err error) bool { - panic(withFeature("BLS multi-sig")) -} - -func IsNotBLSKeyError(err error) bool { - panic(withFeature("BLS multi-sig")) -} - -func IsInvalidSignatureError(err error) bool { - panic(withFeature("BLS multi-sig")) -} - -func initECDSASecp256k1() { - panic(withFeature("ECDSA SECP256k1")) -} - -type pubKeyECDSASecp256k1 struct{} -type prKeyECDSASecp256k1 struct{} - -func secp256k1DecodePublicKeyCompressed(pkBytes []byte) (*pubKeyECDSASecp256k1, error) { - panic(withFeature("ECDSA SECP256k1")) -} - -func privateKeyECDSASecp256k1(a *ecdsaContext, dBytes []byte) *prKeyECDSASecp256k1 { - panic(withFeature("ECDSA SECP256k1")) -} - -func publicKeyECDSASecp256k1(a *ecdsaContext, XYBytes []byte) (*pubKeyECDSASecp256k1, error) { - panic(withFeature("ECDSA SECP256k1")) -} diff --git a/no_cgo_test.go b/no_cgo_test.go deleted file mode 100644 index efc32a29..00000000 --- a/no_cgo_test.go +++ /dev/null @@ -1,46 +0,0 @@ -//go:build !cgo && no_cgo - -package crypto_test - -import ( - "testing" - - "github.com/onflow/crypto" - "github.com/stretchr/testify/assert" -) - -// Test all public functions requiring cgo. -// These functions must panic if built without cgo. -func TestNoRelicPanic(t *testing.T) { - assert.Panics(t, func() { _, _ = crypto.GeneratePrivateKey(crypto.BLSBLS12381, nil) }) - assert.Panics(t, func() { _, _ = crypto.DecodePrivateKey(crypto.BLSBLS12381, nil) }) - assert.Panics(t, func() { _, _ = crypto.DecodePublicKey(crypto.BLSBLS12381, nil) }) - assert.Panics(t, func() { _, _ = crypto.DecodePublicKeyCompressed(crypto.BLSBLS12381, nil) }) - assert.Panics(t, func() { _ = crypto.NewExpandMsgXOFKMAC128("") }) - assert.Panics(t, func() { _ = crypto.BLSInvalidSignature() }) - assert.Panics(t, func() { _, _ = crypto.BLSGeneratePOP(nil) }) - assert.Panics(t, func() { _, _ = crypto.BLSVerifyPOP(nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.AggregateBLSSignatures(nil) }) - assert.Panics(t, func() { _, _ = crypto.AggregateBLSPrivateKeys(nil) }) - assert.Panics(t, func() { _, _ = crypto.AggregateBLSPublicKeys(nil) }) - assert.Panics(t, func() { _ = crypto.IdentityBLSPublicKey() }) - assert.Panics(t, func() { _ = crypto.IsBLSAggregateEmptyListError(nil) }) - assert.Panics(t, func() { _ = crypto.IsInvalidSignatureError(nil) }) - assert.Panics(t, func() { _ = crypto.IsNotBLSKeyError(nil) }) - assert.Panics(t, func() { _ = crypto.IsBLSSignatureIdentity(nil) }) - assert.Panics(t, func() { _, _ = crypto.RemoveBLSPublicKeys(nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.VerifyBLSSignatureOneMessage(nil, nil, nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.VerifyBLSSignatureManyMessages(nil, nil, nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.BatchVerifyBLSSignaturesOneMessage(nil, nil, nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.SPOCKProve(nil, nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.SPOCKVerify(nil, nil, nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.SPOCKVerifyAgainstData(nil, nil, nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.NewBLSThresholdSignatureParticipant(nil, nil, 0, 0, nil, nil, "") }) - assert.Panics(t, func() { _, _ = crypto.NewBLSThresholdSignatureInspector(nil, nil, 0, nil, "") }) - assert.Panics(t, func() { _, _ = crypto.BLSReconstructThresholdSignature(0, 0, nil, nil) }) - assert.Panics(t, func() { _, _ = crypto.EnoughShares(0, 0) }) - assert.Panics(t, func() { _, _, _, _ = crypto.BLSThresholdKeyGen(0, 0, nil) }) - assert.Panics(t, func() { _, _ = crypto.NewFeldmanVSS(0, 0, 0, nil, 0) }) - assert.Panics(t, func() { _, _ = crypto.NewFeldmanVSSQual(0, 0, 0, nil, 0) }) - assert.Panics(t, func() { _, _ = crypto.NewJointFeldman(0, 0, 0, nil) }) -} diff --git a/spock.go b/spock.go index 96f1a593..5927c9be 100644 --- a/spock.go +++ b/spock.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * diff --git a/spock_test.go b/spock_test.go index 1bf8d808..9efccf30 100644 --- a/spock_test.go +++ b/spock_test.go @@ -1,5 +1,3 @@ -//go:build cgo && !no_cgo - /* * Flow Crypto * From 0a2e4c0f1ec4f8370d483449d76ef90eca4349e8 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Mon, 3 Aug 2026 14:53:20 -0500 Subject: [PATCH 06/12] remove non-cgo workflow --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53141423..0ebd67f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,6 @@ jobs: skip-cache: true - name: Run Go Fix run: make go-fix - - name: Run incorrect builds - run: | - echo "::remove-matcher owner=go::" - make incorrect_builds c-code: strategy: From c574d6b7b7b61b29033faa08177a1bbd40a7bcf8 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Mon, 3 Aug 2026 19:06:27 -0500 Subject: [PATCH 07/12] ecdsa improvements --- ecdsa.go | 11 ++++------- ecdsa_p256.go | 2 +- ecdsa_secp256k1.go | 22 ++++++++++++++++------ sign.go | 6 +++++- sign_test_utils.go | 12 ++++++++++++ 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/ecdsa.go b/ecdsa.go index 9e4e715b..a0b71211 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -62,10 +62,10 @@ func (a *ecdsaContext) checkAlgoAndComputeHash(msg []byte, hasher hash.Hasher) ( } // check hasher's size is at least the curve order in bytes - nLen := bitsToBytes((a.curveN).BitLen()) - if hasher.Size() < nLen { + nLen := (a.curveN).BitLen() + if (hasher.Size() << 3) < nLen { return nil, invalidHasherSizeErrorf( - "hasher's size should be at least %d, got %d", nLen, hasher.Size()) + "hasher's bit-size should be at least %d, got %d", nLen, hasher.Size()<<3) } h := hasher.ComputeHash(msg) @@ -118,7 +118,7 @@ func (a *ecdsaContext) mapToPrivateKey(seed []byte) (PrivateKey, error) { // privateKey returns an ECDSA private key using the // input scalar. - +// // Input scalar d is assumed to be satisfy 0 < d < n before calling this function. // // The function returns: @@ -212,8 +212,6 @@ func (a *ecdsaContext) decodePrivateKey(der []byte) (PrivateKey, error) { // Error Returns: // - invalidInputsError if the input is not a valid serialization of a public key on the given curve. func (a *ecdsaContext) rawDecodePublicKey(input []byte) (PublicKey, error) { - // all the curves supported for now have a cofactor equal to 1, - // so that checking the point is on curve is enough to make sure it is on the correct subgroup switch a.algo { case ECDSAP256: return publicKeyECDSAP256(a, input) @@ -222,7 +220,6 @@ func (a *ecdsaContext) rawDecodePublicKey(input []byte) (PublicKey, error) { default: return nil, invalidInputsErrorf("curve is not supported") } - } func (a *ecdsaContext) decodePublicKey(der []byte) (PublicKey, error) { diff --git a/ecdsa_p256.go b/ecdsa_p256.go index d50d76dc..0e5159ba 100644 --- a/ecdsa_p256.go +++ b/ecdsa_p256.go @@ -85,7 +85,7 @@ func privateKeyECDSAP256(a *ecdsaContext, dBytes []byte) (*prKeyECDSAP256, error sk := &prKeyECDSAP256{ prKeyCommonECDSA: &prKeyCommonECDSA{a}, goPrKey: internalSK, - pubKey: nil, // public key is not constructed + pubKey: nil, // public key is not constructed yet } return sk, nil } diff --git a/ecdsa_secp256k1.go b/ecdsa_secp256k1.go index f23fdded..669eab99 100644 --- a/ecdsa_secp256k1.go +++ b/ecdsa_secp256k1.go @@ -93,7 +93,7 @@ func privateKeyECDSASecp256k1(a *ecdsaContext, dBytes []byte) *prKeyECDSASecp256 sk := &prKeyECDSASecp256k1{ prKeyCommonECDSA: &prKeyCommonECDSA{a}, dBytes: dBytes, - pubKey: nil, // public key is not constructed + pubKey: nil, // public key is not constructed yet } return sk } @@ -115,8 +115,8 @@ func (sk *prKeyECDSASecp256k1) Sign(msg []byte, hasher hash.Hasher) (Signature, if err != nil { return nil, err } - // truncate the hash to the curve order size, as specified in FIPS 186-4 section 6.4 - // and as required by the secp256k1 package signing function + // truncate the hash to the curve order size, as specified in FIPS 186-4 section 6.4 (nLenSecp256k1 here is a multiple of 8 bits). + // Moreover, the secp256k1 package requires the message hash to equal nLenSecp256k1 hash = hash[:nLenSecp256k1] signature, err := secp256k1.Sign(hash, sk.dBytes) if err != nil { @@ -133,8 +133,16 @@ func (sk *prKeyECDSASecp256k1) String() string { func publicKeyECDSASecp256k1(a *ecdsaContext, XYBytes []byte) (*pubKeyECDSASecp256k1, error) { pLen := bitsToBytes(a.curveP.BitLen()) + + if len(XYBytes) != 2*pLen { + return nil, invalidInputsErrorf("input has incorrect %s key size, got %d, expects %d", + a.algo, len(XYBytes), 2*pLen) + } + x, y := readTwoBigInts(XYBytes, pLen) + // `IsOnCurve` is not deprecated in btcec's type `KoblitzCurve` + // `IsOnCurve` includes checks for x

= len(names) { + return "UNKNOWN" + } + return names[f] } // Signature is a generic type, regardless of the signature scheme diff --git a/sign_test_utils.go b/sign_test_utils.go index b0f2a7ae..ebbd5150 100644 --- a/sign_test_utils.go +++ b/sign_test_utils.go @@ -243,6 +243,12 @@ func testEncodeDecode(t *testing.T, salg SigningAlgorithm) { assert.True(t, IsInvalidInputsError(err)) assert.Nil(t, sk) + bytes = make([]byte, skLens[salg]-1) + sk, err = DecodePrivateKey(salg, bytes) + require.Error(t, err) + assert.True(t, IsInvalidInputsError(err)) + assert.Nil(t, sk) + // public key pkLens := make(map[SigningAlgorithm]int) pkLens[ECDSAP256] = PubKeyLenECDSAP256 @@ -254,6 +260,12 @@ func testEncodeDecode(t *testing.T, salg SigningAlgorithm) { require.Error(t, err) assert.True(t, IsInvalidInputsError(err)) assert.Nil(t, pk) + + bytes = make([]byte, pkLens[salg]-1) + pk, err = DecodePublicKey(salg, bytes) + require.Error(t, err) + assert.True(t, IsInvalidInputsError(err)) + assert.Nil(t, pk) }) }) } From 2d4a72c2909c0d68ec14bc890ee8649f91f7ddc3 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Tue, 4 Aug 2026 15:05:54 -0500 Subject: [PATCH 08/12] address malleability breaking change --- ecdsa.go | 40 +++++++++++++++++++++++++ ecdsa_p256.go | 13 +++++--- ecdsa_secp256k1.go | 22 +++++++++----- ecdsa_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++++-- go.mod | 2 -- go.sum | 5 ---- 6 files changed, 135 insertions(+), 21 deletions(-) diff --git a/ecdsa.go b/ecdsa.go index a0b71211..d38e7c33 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -41,6 +41,8 @@ type ecdsaContext struct { curveP *big.Int // curve order curveN *big.Int + // curve order minus 1 divided by 2 (used for signature malleability annalysis) + curveNdiv2 *big.Int } const ecEncodingUncompressed = 0x4 @@ -100,6 +102,7 @@ func (a *ecdsaContext) signatureFormatCheck(sig Signature) bool { } var one = new(big.Int).SetInt64(1) +var two = new(big.Int).SetInt64(2) // mapToPrivateKey simply maps the input seed to an ECDSA private key // The private scalar `d` satisfies 0 < d < n. @@ -317,3 +320,40 @@ func readTwoBigInts(input []byte, size int) (*big.Int, *big.Int) { b := new(big.Int).SetBytes(input[size : 2*size]) return a, b } + +// isLowS returns true if the signature's S is in the lower range (S <= (n-1)/2). +func (a *ecdsaContext) isLowS(s *big.Int) bool { + return a.curveNdiv2.Cmp(s) >= 0 +} + +// signatureNormalizeS returns a new signature with S normalized to low S. +// This is needed when the signature verification requires low S to avoid signature malleability, while the package allows high S signatures to be accepted. +func (a *ecdsaContext) signatureNormalizeS(sig []byte) []byte { + // read S + nLen := bitsToBytes(a.curveN.BitLen()) + s := new(big.Int).SetBytes(sig[nLen:]) + if a.isLowS(s) { + return sig // no need to flip S + } + // compute N-S + sComplement := new(big.Int).Sub(a.curveN, s) + // write it into a new signature + newSig := make([]byte, len(sig)) + copy(newSig, sig[:nLen]) // copy R + sComplement.FillBytes(newSig[nLen:]) // write S complement + return newSig +} + +// Test function only to flip S in a signature. It is used for testing signature malleability +func (a *ecdsaContext) signatureFlipS(sig []byte) []byte { + // read S + nLen := bitsToBytes(a.curveN.BitLen()) + s := new(big.Int).SetBytes(sig[nLen:]) + // compute N-S + sComplement := new(big.Int).Sub(a.curveN, s) + // write it into a new signature + newSig := make([]byte, len(sig)) + copy(newSig, sig[:nLen]) // copy R + sComplement.FillBytes(newSig[nLen:]) // write S complement + return newSig +} diff --git a/ecdsa_p256.go b/ecdsa_p256.go index 0e5159ba..87f7b41f 100644 --- a/ecdsa_p256.go +++ b/ecdsa_p256.go @@ -23,6 +23,7 @@ import ( "crypto/elliptic" "crypto/rand" "fmt" + "math/big" "github.com/onflow/crypto/hash" ) @@ -35,7 +36,7 @@ const ( pLenP256 = 32 ) -var ( +const ( // NIST P256 SignatureLenECDSAP256 = 2 * nLenP256 PrKeyLenECDSAP256 = nLenP256 @@ -48,10 +49,14 @@ var p256Instance *ecdsaContext func initECDSAP256() { curve := elliptic.P256() + n := curve.Params().N + nMinus1 := new(big.Int).Sub(n, one) + p256Instance = &(ecdsaContext{ - curveP: curve.Params().P, - curveN: curve.Params().N, - algo: ECDSAP256, + curveP: curve.Params().P, + curveN: n, + curveNdiv2: new(big.Int).Div(nMinus1, two), // (N-1)/2 + algo: ECDSAP256, }) } diff --git a/ecdsa_secp256k1.go b/ecdsa_secp256k1.go index 669eab99..a6478bad 100644 --- a/ecdsa_secp256k1.go +++ b/ecdsa_secp256k1.go @@ -38,9 +38,11 @@ const ( nLenSecp256k1 = 32 pLenSecp256k1 = 32 + + secp256k1Ndiv2Hex = "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0" ) -var ( +const ( // SECG secp256k1 SignatureLenECDSASecp256k1 = 2 * nLenSecp256k1 PrKeyLenECDSASecp256k1 = nLenSecp256k1 @@ -60,10 +62,15 @@ func initECDSASecp256k1() { if !ok { panic("failed to initialize ECDSA with secp256k1 curve") } + curveNdiv2, ok := new(big.Int).SetString(secp256k1Ndiv2Hex, 16) + if !ok { + panic("failed to initialize ECDSA with secp256k1 curve") + } secp256k1Instance = &(ecdsaContext{ - curveP: curveP, - curveN: curveN, - algo: ECDSASecp256k1, + curveP: curveP, + curveN: curveN, + curveNdiv2: curveNdiv2, + algo: ECDSASecp256k1, }) } @@ -141,7 +148,6 @@ func publicKeyECDSASecp256k1(a *ecdsaContext, XYBytes []byte) (*pubKeyECDSASecp2 x, y := readTwoBigInts(XYBytes, pLen) - // `IsOnCurve` is not deprecated in btcec's type `KoblitzCurve` // `IsOnCurve` includes checks for x

Date: Tue, 4 Aug 2026 15:11:24 -0500 Subject: [PATCH 09/12] cleanup --- ecdsa_secp256k1.go | 4 +++- ecdsa_test.go | 28 ---------------------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/ecdsa_secp256k1.go b/ecdsa_secp256k1.go index a6478bad..de8811c0 100644 --- a/ecdsa_secp256k1.go +++ b/ecdsa_secp256k1.go @@ -202,7 +202,9 @@ func (pk *pubKeyECDSASecp256k1) Verify(sig Signature, msg []byte, hasher hash.Ha if len(sig) != 2*nLenSecp256k1 { return false, nil } - // normalize the signature to low S. This is required because the secp256k1 package does not accept high S signatures. + // normalize the signature to low S. + // This is required because the secp256k1 package does not accept high S signatures while the package allows them. + // Rejecting high S signatures would be a breaking change with prior versions. newSig := secp256k1Instance.signatureNormalizeS(sig) // truncate the hash to the curve order size, as specified in FIPS 186-4 section 6.4 (nLenSecp256k1 here is a multiple of 8 bits). diff --git a/ecdsa_test.go b/ecdsa_test.go index 41f5ca06..5c26f49c 100644 --- a/ecdsa_test.go +++ b/ecdsa_test.go @@ -25,7 +25,6 @@ import ( crand "crypto/rand" - "github.com/ethereum/go-ethereum/crypto/secp256k1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -346,33 +345,6 @@ func TestECDSASignatureFormatCheck(t *testing.T) { } } -func TestEllipticUnmarshalSecp256k1(t *testing.T) { - testVectors := []string{ - "028b10bf56476bf7da39a3286e29df389177a2fa0fca2d73348ff78887515d8da1", // IsOnCurve for elliptic returns false - "03d39427f07f680d202fe8504306eb29041aceaf4b628c2c69b0ec248155443166", // odd, IsOnCurve for elliptic returns false - "0267d1942a6cbe4daec242ea7e01c6cdb82dadb6e7077092deb55c845bf851433e", // arith of sqrt in elliptic doesn't match secp256k1 - "0345d45eda6d087918b041453a96303b78c478dce89a4ae9b3c933a018888c5e06", // odd, arith of sqrt in elliptic doesn't match secp256k1 - } - - for _, testVector := range testVectors { - // get the compressed bytes - publicBytes, err := hex.DecodeString(testVector) - require.NoError(t, err) - - // decompress, check that those are perfectly valid Secp256k1 public keys - retrieved, err := DecodePublicKeyCompressed(ECDSASecp256k1, publicBytes) - require.NoError(t, err) - - // check the compression is canonical by re-compressing to the same bytes - require.Equal(t, retrieved.EncodeCompressed(), publicBytes) - - // check that elliptic fails at decompressing them - x, y := secp256k1.DecompressPubkey(publicBytes) - require.Nil(t, x) - require.Nil(t, y) - } -} - func BenchmarkECDSADecode(b *testing.B) { // random message seed := make([]byte, 50) From 7390c14a572cc852a7d8247eb3eb3c9a7d48b09f Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Wed, 5 Aug 2026 13:25:05 -0500 Subject: [PATCH 10/12] check S range before complementing --- ecdsa.go | 32 +++++++++---- ecdsa_secp256k1.go | 5 +- ecdsa_test.go | 115 +++++++++++++++++++++++++++------------------ 3 files changed, 94 insertions(+), 58 deletions(-) diff --git a/ecdsa.go b/ecdsa.go index d38e7c33..e7cf8cb0 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -321,27 +321,39 @@ func readTwoBigInts(input []byte, size int) (*big.Int, *big.Int) { return a, b } -// isLowS returns true if the signature's S is in the lower range (S <= (n-1)/2). +// isLowS returns true if the signature's S is in the lower range (S <= (n-1)/2) func (a *ecdsaContext) isLowS(s *big.Int) bool { return a.curveNdiv2.Cmp(s) >= 0 } -// signatureNormalizeS returns a new signature with S normalized to low S. -// This is needed when the signature verification requires low S to avoid signature malleability, while the package allows high S signatures to be accepted. -func (a *ecdsaContext) signatureNormalizeS(sig []byte) []byte { +// signatureNormalizeS returns a signature with S normalized to low S. +// (same slice is returned if S is already normalized) +// It assumes len(sig) == 2*nLen where nLen is the byte-length of the curve order. +// This is needed when the underlying signature verification requires S to be in the lower range (to avoid signature malleability). In this package, verification allows high S signatures to be accepted. +// The function checks that S is in the correct range [0, n-1] before normalizing it. If S is not in the correct range, the function returns a false boolean. (S will be checked against 0 in the verification function - check against N is inlcuded here) +// returns: +// - newSig, true if S is in the valid range and was normalized to low S +// - nil, false if S was not in the correct range +func (a *ecdsaContext) signatureNormalizeS(sig []byte) ([]byte, bool) { // read S nLen := bitsToBytes(a.curveN.BitLen()) - s := new(big.Int).SetBytes(sig[nLen:]) - if a.isLowS(s) { - return sig // no need to flip S + s := new(big.Int).SetBytes(sig[nLen:]) // S >= 0 + if a.isLowS(s) { // S <= (n-1)/2 + return sig, true // S is in the valid range and no need to flip it } - // compute N-S - sComplement := new(big.Int).Sub(a.curveN, s) + + if a.curveN.Cmp(s) <= 0 { // S >= n, invalid signature + return nil, false + } + + // In the remaining case, (n-1)/2 < S < n and it is safe to flip + // i.e n-s is guaranteed to be in the range [1, (n-1)/2] + sComplement := new(big.Int).Sub(a.curveN, s) // n-S // write it into a new signature newSig := make([]byte, len(sig)) copy(newSig, sig[:nLen]) // copy R sComplement.FillBytes(newSig[nLen:]) // write S complement - return newSig + return newSig, true } // Test function only to flip S in a signature. It is used for testing signature malleability diff --git a/ecdsa_secp256k1.go b/ecdsa_secp256k1.go index de8811c0..027bef74 100644 --- a/ecdsa_secp256k1.go +++ b/ecdsa_secp256k1.go @@ -205,7 +205,10 @@ func (pk *pubKeyECDSASecp256k1) Verify(sig Signature, msg []byte, hasher hash.Ha // normalize the signature to low S. // This is required because the secp256k1 package does not accept high S signatures while the package allows them. // Rejecting high S signatures would be a breaking change with prior versions. - newSig := secp256k1Instance.signatureNormalizeS(sig) + newSig, validS := secp256k1Instance.signatureNormalizeS(sig) + if !validS { + return false, nil // S value is invalid, return early + } // truncate the hash to the curve order size, as specified in FIPS 186-4 section 6.4 (nLenSecp256k1 here is a multiple of 8 bits). // Moreover, the secp256k1 package requires the message hash to equal nLenSecp256k1 diff --git a/ecdsa_test.go b/ecdsa_test.go index 5c26f49c..10162374 100644 --- a/ecdsa_test.go +++ b/ecdsa_test.go @@ -419,58 +419,79 @@ func TestECDSAHighAndLowS(t *testing.T) { ECDSASecp256k1: secp256k1Instance, } - for _, curve := range ecdsaCurves { - t.Run(curve.String(), func(t *testing.T) { - // generate a key and sign a random message - seed := make([]byte, KeyGenSeedMinLen) - _, err := crand.Read(seed) - require.NoError(t, err) - sk, err := GeneratePrivateKey(curve, seed) - require.NoError(t, err) - - msg := make([]byte, 10) - _, err = crand.Read(msg) - require.NoError(t, err) - - halg := hash.NewSHA3_256() - sig, err := sk.Sign(msg, halg) - require.NoError(t, err) - - // extract S and test the first case of S (can be low or high S) - _, s := readTwoBigInts(sig, ecdsaSigLen[curve]/2) - isLowS := ecdsaContexts[curve].isLowS(s) - - t.Run(fmt.Sprintf("low S equals %v", isLowS), func(t *testing.T) { - // the format check must accept both forms - wellFormed, err := SignatureFormatCheck(curve, sig) + t.Run("lowS and HighS pass", func(t *testing.T) { + for _, curve := range ecdsaCurves { + t.Run(curve.String(), func(t *testing.T) { + // generate a key and sign a random message + seed := make([]byte, KeyGenSeedMinLen) + _, err := crand.Read(seed) require.NoError(t, err) - assert.True(t, wellFormed) - - // verification must accept the first form (can be low or high S) - valid, err := sk.PublicKey().Verify(sig, msg, halg) + sk, err := GeneratePrivateKey(curve, seed) require.NoError(t, err) - assert.True(t, valid) - }) - - // flip S to N-S to check the other case (can be low or high S) - t.Run(fmt.Sprintf("low S equals %v", !isLowS), func(t *testing.T) { - newSig := ecdsaContexts[curve].signatureFlipS(sig) - - // sanity check - _, newS := readTwoBigInts(newSig, ecdsaSigLen[curve]/2) - newIsLowS := ecdsaContexts[curve].isLowS(newS) - require.Equal(t, !newIsLowS, isLowS, "S didn't flip") // this test is correct because S cannot equal N-S since N is odd - // the format check must accept both forms - wellFormed, err := SignatureFormatCheck(curve, newSig) + msg := make([]byte, 10) + _, err = crand.Read(msg) require.NoError(t, err) - assert.True(t, wellFormed) - // verification must accept the second form (can be low or high S) - valid, err := sk.PublicKey().Verify(newSig, msg, halg) + halg := hash.NewSHA3_256() + sig, err := sk.Sign(msg, halg) require.NoError(t, err) - assert.True(t, valid) + + // extract S and test the first case of S (can be low or high S) + _, s := readTwoBigInts(sig, ecdsaSigLen[curve]/2) + isLowS := ecdsaContexts[curve].isLowS(s) + + t.Run(fmt.Sprintf("low S equals %v", isLowS), func(t *testing.T) { + // the format check must accept both forms + wellFormed, err := SignatureFormatCheck(curve, sig) + require.NoError(t, err) + assert.True(t, wellFormed) + + // verification must accept the first form (can be low or high S) + valid, err := sk.PublicKey().Verify(sig, msg, halg) + require.NoError(t, err) + assert.True(t, valid) + }) + + // flip S to N-S to check the other case (can be low or high S) + t.Run(fmt.Sprintf("low S equals %v", !isLowS), func(t *testing.T) { + newSig := ecdsaContexts[curve].signatureFlipS(sig) + + // sanity check + _, newS := readTwoBigInts(newSig, ecdsaSigLen[curve]/2) + newIsLowS := ecdsaContexts[curve].isLowS(newS) + require.Equal(t, !newIsLowS, isLowS, "S didn't flip") // this test is correct because S cannot equal N-S since N is odd + + // the format check must accept both forms + wellFormed, err := SignatureFormatCheck(curve, newSig) + require.NoError(t, err) + assert.True(t, wellFormed) + + // verification must accept the second form (can be low or high S) + valid, err := sk.PublicKey().Verify(newSig, msg, halg) + require.NoError(t, err) + assert.True(t, valid) + }) }) - }) - } + } + }) + + // signatureNormalizeS must reject values S >= N + t.Run("check signatureNormalizeS", func(t *testing.T) { + for _, curve := range ecdsaCurves { + t.Run(curve.String(), func(t *testing.T) { + nLen := ecdsaSigLen[curve] / 2 + badSig := make([]byte, ecdsaSigLen[curve]) + // set all S bytes to 0xFF which makes S larger than N. + // R value does not matter in the function + for i := nLen; i < len(badSig); i++ { + badSig[i] = 0xFF + } + + newSig, validS := ecdsaContexts[curve].signatureNormalizeS(badSig) + assert.False(t, validS) + assert.Nil(t, newSig) + }) + } + }) } From ab90dbfecf36dee3ee8537e9bbcb69fb356379a3 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Wed, 5 Aug 2026 17:01:04 -0500 Subject: [PATCH 11/12] downgrade go-eth to match flow-go version --- go.mod | 4 ++-- go.sum | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index e5240a48..fb560979 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/onflow/crypto go 1.26.0 require ( - github.com/ethereum/go-ethereum v1.17.5 + github.com/ethereum/go-ethereum v1.16.8 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.54.0 @@ -15,6 +15,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.47.0 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8ad6fa05..29594720 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,19 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ethereum/go-ethereum v1.17.5 h1:o9BIXs2Q/3cPHVxw49n+Zjn2i6rB9TOXatev46duOC4= -github.com/ethereum/go-ethereum v1.17.5/go.mod h1:vz2YvG7RewA4sFHTgzLyW+WmFG1N4jfk/hgXQVhhn9c= +github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9igY7law= +github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -25,8 +28,8 @@ golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 638803944530756298ae1681641695492fec1c39 Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef Date: Fri, 7 Aug 2026 15:44:57 -0500 Subject: [PATCH 12/12] fix pub key decoding after downgrading go-eth crypto version and improve tests --- README.md | 2 +- blst_src/README.md | 2 +- ecdsa.go | 10 +++++---- ecdsa_p256.go | 4 +++- ecdsa_secp256k1.go | 38 ++++++++++++++++++++----------- ecdsa_test.go | 48 ++++++++++++++++++++++++++-------------- internal/blst/non_cgo.go | 3 --- 7 files changed, 68 insertions(+), 39 deletions(-) delete mode 100644 internal/blst/non_cgo.go diff --git a/README.md b/README.md index 4b912d94..65cd03e9 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ All signature schemes use the generic interfaces of `PrivateKey` and `PublicKey` * ECDSA * public keys are compressed or uncompressed. - * ephemeral key is derived from the private key, hash and the system entropy (based on https://golang.org/pkg/crypto/ecdsa/). * supports NIST P-256 (secp256r1) and secp256k1 curves. + * For NIST P-256, ephemeral key is derived from the private key, hash and the system entropy (based on https://golang.org/pkg/crypto/ecdsa/). For secp256k1, ephemeral key is deterministically formed following RFC 6979 (based on github.com/ethereum/go-ethereum/crypto/secp256k1) * BLS * supports [BLS12-381](https://electriccoin.co/blog/new-snark-curve/) curve. diff --git a/blst_src/README.md b/blst_src/README.md index 48abe90e..1f4085f1 100644 --- a/blst_src/README.md +++ b/blst_src/README.md @@ -21,7 +21,7 @@ The folder contains: To upgrade the BLST version: - [ ] audit all BLST updates, with focus on `/src`: https://github.com/supranational/blst/compare/v0.3.14... - [ ] delete all files in this folder `./blst_src/` but `blst_src.c` and `README.md`. -- [ ] delete all files in `./internal/blst/` but `non_cgo.go`. +- [ ] delete all files in `./internal/blst/`. - [ ] open BLST repository on the new version. - [ ] copy all `.c` and `.h` files from `/src/` into `./blst_src/`. - [ ] delete newly copied `./blst_src/server.c`. diff --git a/ecdsa.go b/ecdsa.go index e7cf8cb0..0d3af994 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -41,7 +41,7 @@ type ecdsaContext struct { curveP *big.Int // curve order curveN *big.Int - // curve order minus 1 divided by 2 (used for signature malleability annalysis) + // curve order minus 1 divided by 2 (used for signature malleability analysis) curveNdiv2 *big.Int } @@ -122,7 +122,7 @@ func (a *ecdsaContext) mapToPrivateKey(seed []byte) (PrivateKey, error) { // privateKey returns an ECDSA private key using the // input scalar. // -// Input scalar d is assumed to be satisfy 0 < d < n before calling this function. +// Input scalar d is assumed to satisfy 0 < d < n before calling this function. // // The function returns: // - (nil, invalidInputsError) if the curve is not supported @@ -217,7 +217,7 @@ func (a *ecdsaContext) decodePrivateKey(der []byte) (PrivateKey, error) { func (a *ecdsaContext) rawDecodePublicKey(input []byte) (PublicKey, error) { switch a.algo { case ECDSAP256: - return publicKeyECDSAP256(a, input) + return publicKeyECDSAP256(input) case ECDSASecp256k1: return publicKeyECDSASecp256k1(a, input) default: @@ -330,7 +330,9 @@ func (a *ecdsaContext) isLowS(s *big.Int) bool { // (same slice is returned if S is already normalized) // It assumes len(sig) == 2*nLen where nLen is the byte-length of the curve order. // This is needed when the underlying signature verification requires S to be in the lower range (to avoid signature malleability). In this package, verification allows high S signatures to be accepted. -// The function checks that S is in the correct range [0, n-1] before normalizing it. If S is not in the correct range, the function returns a false boolean. (S will be checked against 0 in the verification function - check against N is inlcuded here) +// The function checks that S is in the correct range [0, n-1] before normalizing it. +// If S is not in the correct range, the function returns a false boolean. +// (S will be checked against 0 in the verification function - check against N is inlcuded here) // returns: // - newSig, true if S is in the valid range and was normalized to low S // - nil, false if S was not in the correct range diff --git a/ecdsa_p256.go b/ecdsa_p256.go index 87f7b41f..d2838985 100644 --- a/ecdsa_p256.go +++ b/ecdsa_p256.go @@ -82,6 +82,7 @@ type pubKeyECDSAP256 struct { var _ PublicKey = (*pubKeyECDSAP256)(nil) +// Input scalar d is assumed to satisfy 0 < d < n before calling this function. func privateKeyECDSAP256(a *ecdsaContext, dBytes []byte) (*prKeyECDSAP256, error) { internalSK, err := ecdsa.ParseRawPrivateKey(elliptic.P256(), dBytes) if err != nil { @@ -128,12 +129,13 @@ func (sk *prKeyECDSAP256) String() string { } // returns a publicKeyECDSAP256 from (bytes(x) || bytes(y)) bytes -func publicKeyECDSAP256(a *ecdsaContext, XYBytes []byte) (*pubKeyECDSAP256, error) { +func publicKeyECDSAP256(XYBytes []byte) (*pubKeyECDSAP256, error) { // deserialization uses SEC1 version 2 (https://www.secg.org/sec1-v2.pdf section 2.3.3) // and includes on curve check. // The bytes serialization for non-infinity points is `0x04 || X || Y` and infinity point should be rejected anyway parsingBytes := append([]byte{ecEncodingUncompressed}, XYBytes...) + // ParseUncompressedPublicKey includes x

= 0 || y.Cmp(a.curveP) >= 0 { + return nil, invalidInputsErrorf("at least one coordinate is larger than the field prime for %s", a.algo) + } + + // `IsOnCurve` includes checks for x

= p + ECDSASecp256k1, "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F0000000000000000000000000000000000000000000000000000000000000000", + onflowCryptoErr, + }, { + ECDSAP256, "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000", + goCryptoErr, + }, { + // y >= p + ECDSASecp256k1, "0000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + onflowCryptoErr, + }, { + ECDSAP256, "0000000000000000000000000000000000000000000000000000000000000000FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", + goCryptoErr, + }, } - invalidPK2s := map[SigningAlgorithm]string{ - ECDSASecp256k1: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F0000000000000000000000000000000000000000000000000000000000000000", - ECDSAP256: "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000", + + for _, invalidPK := range invalidPKs { + pkBytes, err := hex.DecodeString(invalidPK.pk) + require.NoError(t, err) + pk, err := DecodePublicKey(invalidPK.signin, pkBytes) + require.Error(t, err) + assert.True(t, IsInvalidInputsError(err)) + assert.ErrorContains(t, err, invalidPK.errorMsg) + assert.Nil(t, pk) } - // invalidpk1 with x >= p - invalidPk1, err := hex.DecodeString(invalidPK1s[curve]) - require.NoError(t, err) - _, err = DecodePublicKey(curve, invalidPk1) - assert.Error(t, err) - // invalidpk2 with y >= p - invalidPk2, err := hex.DecodeString(invalidPK2s[curve]) - require.NoError(t, err) - _, err = DecodePublicKey(curve, invalidPk2) - assert.Error(t, err) }) } } diff --git a/internal/blst/non_cgo.go b/internal/blst/non_cgo.go deleted file mode 100644 index 324387c6..00000000 --- a/internal/blst/non_cgo.go +++ /dev/null @@ -1,3 +0,0 @@ -//go:build !cgo && no_cgo - -package blst