]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/ecdsa: add low-level encoding functions for keys
authorFilippo Valsorda <filippo@golang.org>
Tue, 20 May 2025 15:34:57 +0000 (17:34 +0200)
committerGopher Robot <gobot@golang.org>
Wed, 21 May 2025 21:18:13 +0000 (14:18 -0700)
Fixes #63963

Change-Id: I6a6a4656a729b6211171aca46bdc13fed5fc5643
Reviewed-on: https://go-review.googlesource.com/c/go/+/674475
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
api/next/63963.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/crypto/ecdsa/63963.md [new file with mode: 0644]
src/crypto/ecdsa/ecdsa.go
src/crypto/ecdsa/ecdsa_test.go

diff --git a/api/next/63963.txt b/api/next/63963.txt
new file mode 100644 (file)
index 0000000..f64f214
--- /dev/null
@@ -0,0 +1,4 @@
+pkg crypto/ecdsa, func ParseRawPrivateKey(elliptic.Curve, []uint8) (*PrivateKey, error) #63963
+pkg crypto/ecdsa, func ParseUncompressedPublicKey(elliptic.Curve, []uint8) (*PublicKey, error) #63963
+pkg crypto/ecdsa, method (*PrivateKey) Bytes() ([]uint8, error) #63963
+pkg crypto/ecdsa, method (*PublicKey) Bytes() ([]uint8, error) #63963
diff --git a/doc/next/6-stdlib/99-minor/crypto/ecdsa/63963.md b/doc/next/6-stdlib/99-minor/crypto/ecdsa/63963.md
new file mode 100644 (file)
index 0000000..5c329c7
--- /dev/null
@@ -0,0 +1,3 @@
+The new [ParseRawPrivateKey], [ParseUncompressedPublicKey], [PrivateKey.Bytes],
+and [PublicKey.Bytes] functions and methods implement low-level encodings,
+replacing the need to use crypto/elliptic or math/big functions and methods.
index 5e670c50818dd5492869dfa6735d652e6df0398e..9affc1ff7861256be9b2754891f31e51dc1fbcec 100644 (file)
@@ -23,6 +23,7 @@ import (
        "crypto/internal/boring"
        "crypto/internal/boring/bbig"
        "crypto/internal/fips140/ecdsa"
+       "crypto/internal/fips140/nistec"
        "crypto/internal/fips140cache"
        "crypto/internal/fips140hash"
        "crypto/internal/fips140only"
@@ -40,6 +41,18 @@ import (
 // PublicKey represents an ECDSA public key.
 type PublicKey struct {
        elliptic.Curve
+
+       // X, Y are the coordinates of the public key point.
+       //
+       // Modifying the raw coordinates can produce invalid keys, and may
+       // invalidate internal optimizations; moreover, [big.Int] methods are not
+       // suitable for operating on cryptographic values. To encode and decode
+       // PublicKey values, use [PublicKey.Bytes] and [ParseUncompressedPublicKey]
+       // or [x509.MarshalPKIXPublicKey] and [x509.ParsePKIXPublicKey]. For ECDH,
+       // use [crypto/ecdh]. For lower-level elliptic curve operations, use a
+       // third-party module like filippo.io/nistec.
+       //
+       // These fields will be deprecated in Go 1.26.
        X, Y *big.Int
 }
 
@@ -78,9 +91,94 @@ func (pub *PublicKey) Equal(x crypto.PublicKey) bool {
                pub.Curve == xx.Curve
 }
 
+// ParseUncompressedPublicKey parses a public key encoded as an uncompressed
+// point according to SEC 1, Version 2.0, Section 2.3.3 (also known as the X9.62
+// uncompressed format). It returns an error if the point is not in uncompressed
+// form, is not on the curve, or is the point at infinity.
+//
+// curve must be one of [elliptic.P224], [elliptic.P256], [elliptic.P384], or
+// [elliptic.P521], or ParseUncompressedPublicKey returns an error.
+//
+// ParseUncompressedPublicKey accepts the same format as
+// [ecdh.Curve.NewPublicKey] does for NIST curves, but returns a [PublicKey]
+// instead of an [ecdh.PublicKey].
+//
+// Note that public keys are more commonly encoded in DER (or PEM) format, which
+// can be parsed with [x509.ParsePKIXPublicKey] (and [encoding/pem]).
+func ParseUncompressedPublicKey(curve elliptic.Curve, data []byte) (*PublicKey, error) {
+       if len(data) < 1 || data[0] != 4 {
+               return nil, errors.New("ecdsa: invalid uncompressed public key")
+       }
+       switch curve {
+       case elliptic.P224():
+               return parseUncompressedPublicKey(ecdsa.P224(), curve, data)
+       case elliptic.P256():
+               return parseUncompressedPublicKey(ecdsa.P256(), curve, data)
+       case elliptic.P384():
+               return parseUncompressedPublicKey(ecdsa.P384(), curve, data)
+       case elliptic.P521():
+               return parseUncompressedPublicKey(ecdsa.P521(), curve, data)
+       default:
+               return nil, errors.New("ecdsa: curve not supported by ParseUncompressedPublicKey")
+       }
+}
+
+func parseUncompressedPublicKey[P ecdsa.Point[P]](c *ecdsa.Curve[P], curve elliptic.Curve, data []byte) (*PublicKey, error) {
+       k, err := ecdsa.NewPublicKey(c, data)
+       if err != nil {
+               return nil, err
+       }
+       return publicKeyFromFIPS(curve, k)
+}
+
+// Bytes encodes the public key as an uncompressed point according to SEC 1,
+// Version 2.0, Section 2.3.3 (also known as the X9.62 uncompressed format).
+// It returns an error if the public key is invalid.
+//
+// PublicKey.Curve must be one of [elliptic.P224], [elliptic.P256],
+// [elliptic.P384], or [elliptic.P521], or Bytes returns an error.
+//
+// Bytes returns the same format as [ecdh.PublicKey.Bytes] does for NIST curves.
+//
+// Note that public keys are more commonly encoded in DER (or PEM) format, which
+// can be generated with [x509.MarshalPKIXPublicKey] (and [encoding/pem]).
+func (pub *PublicKey) Bytes() ([]byte, error) {
+       switch pub.Curve {
+       case elliptic.P224():
+               return publicKeyBytes(ecdsa.P224(), pub)
+       case elliptic.P256():
+               return publicKeyBytes(ecdsa.P256(), pub)
+       case elliptic.P384():
+               return publicKeyBytes(ecdsa.P384(), pub)
+       case elliptic.P521():
+               return publicKeyBytes(ecdsa.P521(), pub)
+       default:
+               return nil, errors.New("ecdsa: curve not supported by PublicKey.Bytes")
+       }
+}
+
+func publicKeyBytes[P ecdsa.Point[P]](c *ecdsa.Curve[P], pub *PublicKey) ([]byte, error) {
+       k, err := publicKeyToFIPS(c, pub)
+       if err != nil {
+               return nil, err
+       }
+       return k.Bytes(), nil
+}
+
 // PrivateKey represents an ECDSA private key.
 type PrivateKey struct {
        PublicKey
+
+       // D is the private scalar value.
+       //
+       // Modifying the raw value can produce invalid keys, and may
+       // invalidate internal optimizations; moreover, [big.Int] methods are not
+       // suitable for operating on cryptographic values. To encode and decode
+       // PrivateKey values, use [PrivateKey.Bytes] and [ParseRawPrivateKey]
+       // or [x509.MarshalPKCS8PrivateKey] and [x509.ParsePKCS8PrivateKey].
+       // For ECDH, use [crypto/ecdh].
+       //
+       // This field will be deprecated in Go 1.26.
        D *big.Int
 }
 
@@ -134,6 +232,82 @@ func bigIntEqual(a, b *big.Int) bool {
        return subtle.ConstantTimeCompare(a.Bytes(), b.Bytes()) == 1
 }
 
+// ParseRawPrivateKey parses a private key encoded as a fixed-length big-endian
+// integer, according to SEC 1, Version 2.0, Section 2.3.6 (sometimes referred
+// to as the raw format). It returns an error if the value is not reduced modulo
+// the curve's order, or if it's zero.
+//
+// curve must be one of [elliptic.P224], [elliptic.P256], [elliptic.P384], or
+// [elliptic.P521], or ParseRawPrivateKey returns an error.
+//
+// ParseRawPrivateKey accepts the same format as [ecdh.Curve.NewPrivateKey] does
+// for NIST curves, but returns a [PrivateKey] instead of an [ecdh.PrivateKey].
+//
+// Note that private keys are more commonly encoded in ASN.1 or PKCS#8 format,
+// which can be parsed with [x509.ParseECPrivateKey] or
+// [x509.ParsePKCS8PrivateKey] (and [encoding/pem]).
+func ParseRawPrivateKey(curve elliptic.Curve, data []byte) (*PrivateKey, error) {
+       switch curve {
+       case elliptic.P224():
+               return parseRawPrivateKey(ecdsa.P224(), nistec.NewP224Point, curve, data)
+       case elliptic.P256():
+               return parseRawPrivateKey(ecdsa.P256(), nistec.NewP256Point, curve, data)
+       case elliptic.P384():
+               return parseRawPrivateKey(ecdsa.P384(), nistec.NewP384Point, curve, data)
+       case elliptic.P521():
+               return parseRawPrivateKey(ecdsa.P521(), nistec.NewP521Point, curve, data)
+       default:
+               return nil, errors.New("ecdsa: curve not supported by ParseRawPrivateKey")
+       }
+}
+
+func parseRawPrivateKey[P ecdsa.Point[P]](c *ecdsa.Curve[P], newPoint func() P, curve elliptic.Curve, data []byte) (*PrivateKey, error) {
+       q, err := newPoint().ScalarBaseMult(data)
+       if err != nil {
+               return nil, err
+       }
+       k, err := ecdsa.NewPrivateKey(c, data, q.Bytes())
+       if err != nil {
+               return nil, err
+       }
+       return privateKeyFromFIPS(curve, k)
+}
+
+// Bytes encodes the private key as a fixed-length big-endian integer according
+// to SEC 1, Version 2.0, Section 2.3.6 (sometimes referred to as the raw
+// format). It returns an error if the private key is invalid.
+//
+// PrivateKey.Curve must be one of [elliptic.P224], [elliptic.P256],
+// [elliptic.P384], or [elliptic.P521], or Bytes returns an error.
+//
+// Bytes returns the same format as [ecdh.PrivateKey.Bytes] does for NIST curves.
+//
+// Note that private keys are more commonly encoded in ASN.1 or PKCS#8 format,
+// which can be generated with [x509.MarshalECPrivateKey] or
+// [x509.MarshalPKCS8PrivateKey] (and [encoding/pem]).
+func (priv *PrivateKey) Bytes() ([]byte, error) {
+       switch priv.Curve {
+       case elliptic.P224():
+               return privateKeyBytes(ecdsa.P224(), priv)
+       case elliptic.P256():
+               return privateKeyBytes(ecdsa.P256(), priv)
+       case elliptic.P384():
+               return privateKeyBytes(ecdsa.P384(), priv)
+       case elliptic.P521():
+               return privateKeyBytes(ecdsa.P521(), priv)
+       default:
+               return nil, errors.New("ecdsa: curve not supported by PrivateKey.Bytes")
+       }
+}
+
+func privateKeyBytes[P ecdsa.Point[P]](c *ecdsa.Curve[P], priv *PrivateKey) ([]byte, error) {
+       k, err := privateKeyToFIPS(c, priv)
+       if err != nil {
+               return nil, err
+       }
+       return k.Bytes(), nil
+}
+
 // Sign signs a hash (which should be the result of hashing a larger message
 // with opts.HashFunc()) using the private key, priv. If the hash is longer than
 // the bit-length of the private key's curve order, the hash will be truncated
index 84b127dc0ff962cb2c5f1085bd758c5c42d68dfc..87e74f2a0e9d6a2464619347cc49b3e4d0145e96 100644 (file)
@@ -546,6 +546,161 @@ func testRFC6979(t *testing.T, curve elliptic.Curve, D, X, Y, msg, r, s string)
        }
 }
 
+func TestParseAndBytesRoundTrip(t *testing.T) {
+       testAllCurves(t, testParseAndBytesRoundTrip)
+}
+
+func testParseAndBytesRoundTrip(t *testing.T, curve elliptic.Curve) {
+       if strings.HasSuffix(t.Name(), "/Generic") {
+               t.Skip("these methods don't support generic curves")
+       }
+       priv, _ := GenerateKey(curve, rand.Reader)
+
+       b, err := priv.PublicKey.Bytes()
+       if err != nil {
+               t.Fatalf("failed to serialize private key's public key: %v", err)
+       }
+       if b[0] != 4 {
+               t.Fatalf("public key bytes doesn't start with 0x04 (uncompressed format)")
+       }
+       p, err := ParseUncompressedPublicKey(curve, b)
+       if err != nil {
+               t.Fatalf("failed to parse private key's public key: %v", err)
+       }
+       if !priv.PublicKey.Equal(p) {
+               t.Errorf("parsed private key's public key doesn't match original")
+       }
+
+       bk, err := priv.Bytes()
+       if err != nil {
+               t.Fatalf("failed to serialize private key: %v", err)
+       }
+       k, err := ParseRawPrivateKey(curve, bk)
+       if err != nil {
+               t.Fatalf("failed to parse private key: %v", err)
+       }
+       if !priv.Equal(k) {
+               t.Errorf("parsed private key doesn't match original")
+       }
+
+       if curve != elliptic.P224() {
+               privECDH, err := priv.ECDH()
+               if err != nil {
+                       t.Fatalf("failed to convert private key to ECDH: %v", err)
+               }
+
+               pp, err := privECDH.Curve().NewPublicKey(b)
+               if err != nil {
+                       t.Fatalf("failed to parse with ECDH: %v", err)
+               }
+               if !privECDH.PublicKey().Equal(pp) {
+                       t.Errorf("parsed ECDH public key doesn't match original")
+               }
+               if !bytes.Equal(b, pp.Bytes()) {
+                       t.Errorf("encoded ECDH public key doesn't match Bytes")
+               }
+
+               kk, err := privECDH.Curve().NewPrivateKey(bk)
+               if err != nil {
+                       t.Fatalf("failed to parse with ECDH: %v", err)
+               }
+               if !privECDH.Equal(kk) {
+                       t.Errorf("parsed ECDH private key doesn't match original")
+               }
+               if !bytes.Equal(bk, kk.Bytes()) {
+                       t.Errorf("encoded ECDH private key doesn't match Bytes")
+               }
+       }
+}
+
+func TestInvalidPublicKeys(t *testing.T) {
+       testAllCurves(t, testInvalidPublicKeys)
+}
+
+func testInvalidPublicKeys(t *testing.T, curve elliptic.Curve) {
+       t.Run("Infinity", func(t *testing.T) {
+               k := &PublicKey{Curve: curve, X: big.NewInt(0), Y: big.NewInt(0)}
+               if _, err := k.Bytes(); err == nil {
+                       t.Errorf("PublicKey.Bytes accepted infinity")
+               }
+
+               b := []byte{0}
+               if _, err := ParseUncompressedPublicKey(curve, b); err == nil {
+                       t.Errorf("ParseUncompressedPublicKey accepted infinity")
+               }
+               b = make([]byte, 1+2*(curve.Params().BitSize+7)/8)
+               b[0] = 4
+               if _, err := ParseUncompressedPublicKey(curve, b); err == nil {
+                       t.Errorf("ParseUncompressedPublicKey accepted infinity")
+               }
+       })
+       t.Run("NotOnCurve", func(t *testing.T) {
+               k, _ := GenerateKey(curve, rand.Reader)
+               k.X = k.X.Add(k.X, big.NewInt(1))
+               if _, err := k.Bytes(); err == nil {
+                       t.Errorf("PublicKey.Bytes accepted not on curve")
+               }
+
+               b := make([]byte, 1+2*(curve.Params().BitSize+7)/8)
+               b[0] = 4
+               k.X.FillBytes(b[1 : 1+len(b)/2])
+               k.Y.FillBytes(b[1+len(b)/2:])
+               if _, err := ParseUncompressedPublicKey(curve, b); err == nil {
+                       t.Errorf("ParseUncompressedPublicKey accepted not on curve")
+               }
+       })
+       t.Run("Compressed", func(t *testing.T) {
+               k, _ := GenerateKey(curve, rand.Reader)
+               b := elliptic.MarshalCompressed(curve, k.X, k.Y)
+               if _, err := ParseUncompressedPublicKey(curve, b); err == nil {
+                       t.Errorf("ParseUncompressedPublicKey accepted compressed key")
+               }
+       })
+}
+
+func TestInvalidPrivateKeys(t *testing.T) {
+       testAllCurves(t, testInvalidPrivateKeys)
+}
+
+func testInvalidPrivateKeys(t *testing.T, curve elliptic.Curve) {
+       t.Run("Zero", func(t *testing.T) {
+               k := &PrivateKey{PublicKey{curve, big.NewInt(0), big.NewInt(0)}, big.NewInt(0)}
+               if _, err := k.Bytes(); err == nil {
+                       t.Errorf("PrivateKey.Bytes accepted zero key")
+               }
+
+               b := make([]byte, (curve.Params().BitSize+7)/8)
+               if _, err := ParseRawPrivateKey(curve, b); err == nil {
+                       t.Errorf("ParseRawPrivateKey accepted zero key")
+               }
+       })
+       t.Run("Overflow", func(t *testing.T) {
+               d := new(big.Int).Add(curve.Params().N, big.NewInt(5))
+               x, y := curve.ScalarBaseMult(d.Bytes())
+               k := &PrivateKey{PublicKey{curve, x, y}, d}
+               if _, err := k.Bytes(); err == nil {
+                       t.Errorf("PrivateKey.Bytes accepted overflow key")
+               }
+
+               b := make([]byte, (curve.Params().BitSize+7)/8)
+               k.D.FillBytes(b)
+               if _, err := ParseRawPrivateKey(curve, b); err == nil {
+                       t.Errorf("ParseRawPrivateKey accepted overflow key")
+               }
+       })
+       t.Run("Length", func(t *testing.T) {
+               b := []byte{1, 2, 3}
+               if _, err := ParseRawPrivateKey(curve, b); err == nil {
+                       t.Errorf("ParseRawPrivateKey accepted short key")
+               }
+
+               b = append(b, make([]byte, (curve.Params().BitSize+7)/8)...)
+               if _, err := ParseRawPrivateKey(curve, b); err == nil {
+                       t.Errorf("ParseRawPrivateKey accepted long key")
+               }
+       })
+}
+
 func benchmarkAllCurves(b *testing.B, f func(*testing.B, elliptic.Curve)) {
        tests := []struct {
                name  string