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: 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..e7cf8cb0 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -21,168 +21,72 @@ 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 + // curve order minus 1 divided by 2 (used for signature malleability annalysis) + curveNdiv2 *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 := (a.curveN).BitLen() + if (hasher.Size() << 3) < nLen { + return nil, invalidHasherSizeErrorf( + "hasher's bit-size should be at least %d, got %d", nLen, hasher.Size()<<3) } - 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 @@ -198,62 +102,45 @@ func (a *ecdsaAlgo) signatureFormatCheck(sig Signature) bool { } var one = new(big.Int).SetInt64(1) +var two = new(big.Int).SetInt64(2) -// 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 +148,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 +161,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 +171,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 +196,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 +214,158 @@ 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) - } - - // 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 { +func (a *ecdsaContext) rawDecodePublicKey(input []byte) (PublicKey, error) { + 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 { +func prKeyCommonECDSAEquals(sk, other PrivateKey) bool { + // check the algorithm + if sk.Algorithm() != other.Algorithm() { return false } - // check the curve - if sk.alg.curve != otherECDSA.alg.curve { - return false - } - return sk.goPrKey.D.Cmp(otherECDSA.goPrKey.D) == 0 -} - -// String returns the hex string representation of the key. -func (sk *prKeyECDSA) String() string { - return fmt.Sprintf("%#x", sk.Encode()) + // check the scalar + return bytes.Equal(sk.Encode(), other.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 +type pubKeyCommonECDSA struct { + // ECDSA context + *ecdsaContext } -var _ PublicKey = (*pubKeyECDSA)(nil) - -// Algorithm returns the the algo related to the private key -func (pk *pubKeyECDSA) Algorithm() SigningAlgorithm { - return pk.alg.algo +// Size returns the length of the public key in bytes +func (pk *pubKeyCommonECDSA) Size() int { + return 2 * bitsToBytes(pk.curveP.BitLen()) } -// Size returns the length of the public key in bytes -func (pk *pubKeyECDSA) Size() int { - return 2 * bitsToBytes((pk.goPubKey.Params().P).BitLen()) +// Equals test the equality of two private keys +func pubKeyCommonECDSAEquals(pk, other PublicKey) bool { + // check the algorithm + if pk.Algorithm() != other.Algorithm() { + return false + } + // check the point + return bytes.Equal(pk.Encode(), other.Encode()) } -// 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) +// 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:]) } -// `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 +// 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 } -// 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() +// 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 } -// 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 { - return false +// 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:]) // 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 } - // check the curve - if pk.alg.curve != otherECDSA.alg.curve { - return false + + if a.curveN.Cmp(s) <= 0 { // S >= n, invalid signature + return nil, false } - return (pk.goPubKey.X.Cmp(otherECDSA.goPubKey.X) == 0) && - (pk.goPubKey.Y.Cmp(otherECDSA.goPubKey.Y) == 0) + + // 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, true } -// String returns the hex string representation of the key. -func (pk *pubKeyECDSA) String() string { - return fmt.Sprintf("%#x", pk.Encode()) +// 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 new file mode 100644 index 00000000..87f7b41f --- /dev/null +++ b/ecdsa_p256.go @@ -0,0 +1,280 @@ +/* + * 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" + "math/big" + + "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 +) + +const ( + // 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() + n := curve.Params().N + nMinus1 := new(big.Int).Sub(n, one) + + p256Instance = &(ecdsaContext{ + curveP: curve.Params().P, + curveN: n, + curveNdiv2: new(big.Int).Div(nMinus1, two), // (N-1)/2 + 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 yet + } + 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..4cec7ca2 --- /dev/null +++ b/ecdsa_secp256k1.go @@ -0,0 +1,292 @@ +//go:build cgo && !no_cgo + +/* + * 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 + + secp256k1Ndiv2Hex = "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0" +) + +const ( + // 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") + } + curveNdiv2, ok := new(big.Int).SetString(secp256k1Ndiv2Hex, 16) + if !ok { + panic("failed to initialize ECDSA with secp256k1 curve") + } + secp256k1Instance = &(ecdsaContext{ + curveP: curveP, + curveN: curveN, + curveNdiv2: curveNdiv2, + 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 yet + } + 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 (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 { + 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()) + + 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` includes checks for x

= 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 +265,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) { @@ -395,33 +345,6 @@ func TestSignatureFormatCheck(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 := elliptic.UnmarshalCompressed(btcec.S256(), publicBytes) - require.Nil(t, x) - require.Nil(t, y) - } -} - func BenchmarkECDSADecode(b *testing.B) { // random message seed := make([]byte, 50) @@ -483,3 +406,92 @@ func TestECDSAKeyGenerationBreakingChange(t *testing.T) { assert.Equal(t, test.expectedSK, sk.String()) } } + +// TestECDSAHighAndLowS checks that both signature malleability forms are accepted. +// +// For a valid signature (r,s), the pair (r,n-s) is also a valid signature of the same +// message under the same key. The package signature verification accepts both forms and should keep doing so. +// Rejecting the high-s form would be a breaking change for the applications using this package. +func TestECDSAHighAndLowS(t *testing.T) { + + var ecdsaContexts = map[SigningAlgorithm]*ecdsaContext{ + ECDSAP256: p256Instance, + ECDSASecp256k1: secp256k1Instance, + } + + 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) + 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) + 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) + }) + } + }) +} diff --git a/go.mod b/go.mod index 344580d9..e5240a48 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,18 @@ module github.com/onflow/crypto 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 ) 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..8ad6fa05 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,27 @@ -github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= -github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 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/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/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) 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")) +} diff --git a/sign.go b/sign.go index 9d5000fe..b6de3cbd 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" ) @@ -48,7 +45,11 @@ const ( // String returns the string representation of this signing algorithm. func (f SigningAlgorithm) String() string { - return [...]string{"UNKNOWN", "BLS_BLS12381", "ECDSA_P256", "ECDSA_secp256k1"}[f] + names := [...]string{"UNKNOWN", "BLS_BLS12381", "ECDSA_P256", "ECDSA_secp256k1"} + if f < 0 || int(f) >= len(names) { + return "UNKNOWN" + } + return names[f] } // Signature is a generic type, regardless of the signature scheme @@ -83,20 +84,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, 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) }) }) }