]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/internal/fips/ecdsa: add CAST, PCT, DRBG, and FIPS 186-5 references
authorFilippo Valsorda <filippo@golang.org>
Sat, 16 Nov 2024 16:44:17 +0000 (17:44 +0100)
committerGopher Robot <gobot@golang.org>
Tue, 19 Nov 2024 23:02:22 +0000 (23:02 +0000)
The previous CL focused on moving the implementation as-is, while this
makes it FIPS-compliant.

For #69536

Change-Id: I75fa56c7e13ba20246bacf9fda4599c9f25a1c63
Reviewed-on: https://go-review.googlesource.com/c/go/+/628678
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Russ Cox <rsc@golang.org>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
src/crypto/ecdsa/ecdsa.go
src/crypto/internal/fips/ecdsa/cast.go [new file with mode: 0644]
src/crypto/internal/fips/ecdsa/ecdsa.go
src/crypto/internal/fipstest/cast_test.go

index 5ceef823a57e3689e6002bf84c0b2b7d4e5ef442..4621cc9780c03410c9060222e0cc3e68776296e2 100644 (file)
@@ -3,7 +3,7 @@
 // license that can be found in the LICENSE file.
 
 // Package ecdsa implements the Elliptic Curve Digital Signature Algorithm, as
-// defined in FIPS 186-4 and SEC 1, Version 2.0.
+// defined in FIPS 186-5 and SEC 1, Version 2.0.
 //
 // Signatures generated by this package are not deterministic, but entropy is
 // mixed with the private key and the message, achieving the same level of
 // [elliptic.P256], [elliptic.P384], or [elliptic.P521] is used.
 package ecdsa
 
-// [FIPS 186-4] references ANSI X9.62-2005 for the bulk of the ECDSA algorithm.
-// That standard is not freely available, which is a problem in an open source
-// implementation, because not only the implementer, but also any maintainer,
-// contributor, reviewer, auditor, and learner needs access to it. Instead, this
-// package references and follows the equivalent [SEC 1, Version 2.0].
-//
-// [FIPS 186-4]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
-// [SEC 1, Version 2.0]: https://www.secg.org/sec1-v2.pdf
-
 import (
        "crypto"
        "crypto/aes"
@@ -234,6 +225,9 @@ func SignASN1(rand io.Reader, priv *PrivateKey, hash []byte) ([]byte, error) {
 }
 
 func signFIPS[P ecdsa.Point[P]](c *ecdsa.Curve[P], priv *PrivateKey, csprng io.Reader, hash []byte) ([]byte, error) {
+       // privateKeyToFIPS is very slow in FIPS mode because it performs a
+       // Sign+Verify cycle per FIPS 140-3 IG 10.3.A. We should find a way to cache
+       // it or attach it to the PrivateKey.
        k, err := privateKeyToFIPS(c, priv)
        if err != nil {
                return nil, err
diff --git a/src/crypto/internal/fips/ecdsa/cast.go b/src/crypto/internal/fips/ecdsa/cast.go
new file mode 100644 (file)
index 0000000..ed6d632
--- /dev/null
@@ -0,0 +1,69 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ecdsa
+
+import (
+       "bytes"
+       "crypto/internal/fips"
+       "errors"
+       "sync"
+)
+
+var fipsSelfTest = sync.OnceFunc(func() {
+       fips.CAST("ECDSA P-256 SHA2-256", func() error {
+               // https://www.rfc-editor.org/rfc/rfc9500.html#section-2.3
+               k := &PrivateKey{
+                       pub: PublicKey{
+                               curve: p256,
+                               q: []byte{
+                                       0x04,
+                                       0x42, 0x25, 0x48, 0xF8, 0x8F, 0xB7, 0x82, 0xFF,
+                                       0xB5, 0xEC, 0xA3, 0x74, 0x44, 0x52, 0xC7, 0x2A,
+                                       0x1E, 0x55, 0x8F, 0xBD, 0x6F, 0x73, 0xBE, 0x5E,
+                                       0x48, 0xE9, 0x32, 0x32, 0xCC, 0x45, 0xC5, 0xB1,
+                                       0x6C, 0x4C, 0xD1, 0x0C, 0x4C, 0xB8, 0xD5, 0xB8,
+                                       0xA1, 0x71, 0x39, 0xE9, 0x48, 0x82, 0xC8, 0x99,
+                                       0x25, 0x72, 0x99, 0x34, 0x25, 0xF4, 0x14, 0x19,
+                                       0xAB, 0x7E, 0x90, 0xA4, 0x2A, 0x49, 0x42, 0x72},
+                       },
+                       d: []byte{
+                               0xE6, 0xCB, 0x5B, 0xDD, 0x80, 0xAA, 0x45, 0xAE,
+                               0x9C, 0x95, 0xE8, 0xC1, 0x54, 0x76, 0x67, 0x9F,
+                               0xFE, 0xC9, 0x53, 0xC1, 0x68, 0x51, 0xE7, 0x11,
+                               0xE7, 0x43, 0x93, 0x95, 0x89, 0xC6, 0x4F, 0xC1,
+                       },
+               }
+               hash := []byte{
+                       0x32, 0xb3, 0xda, 0x19, 0xf9, 0x44, 0xb0, 0x48,
+                       0x66, 0xd9, 0x31, 0xa6, 0x6c, 0x30, 0xb9, 0x4a,
+                       0xe7, 0x28, 0xcc, 0xe7, 0x00, 0xe4, 0xb0, 0xa5,
+                       0xb4, 0xfe, 0xae, 0x8f, 0x43, 0x8f, 0xde, 0xc2,
+               }
+               want := &Signature{
+                       R: []byte{
+                               0x6b, 0x17, 0xd1, 0xf2, 0xe1, 0x2c, 0x42, 0x47,
+                               0xf8, 0xbc, 0xe6, 0xe5, 0x63, 0xa4, 0x40, 0xf2,
+                               0x77, 0x03, 0x7d, 0x81, 0x2d, 0xeb, 0x33, 0xa0,
+                               0xf4, 0xa1, 0x39, 0x45, 0xd8, 0x98, 0xc2, 0x96,
+                       }, S: []byte{
+                               0x2d, 0x4f, 0x31, 0xc9, 0x07, 0x35, 0x74, 0x2b,
+                               0x82, 0x04, 0x19, 0xa0, 0x97, 0xb1, 0x4a, 0x75,
+                               0x99, 0xf7, 0x8a, 0x54, 0xf8, 0x3b, 0xe9, 0x89,
+                               0x50, 0x26, 0x35, 0x32, 0x52, 0x0f, 0x73, 0xfc,
+                       },
+               }
+               got, err := sign(P256(), k, testingOnlyFixedRandomPoint, hash)
+               if err != nil {
+                       return err
+               }
+               if err := verify(P256(), &k.pub, hash, got); err != nil {
+                       return err
+               }
+               if !bytes.Equal(got.R, want.R) || !bytes.Equal(got.S, want.S) {
+                       return errors.New("unexpected result")
+               }
+               return nil
+       })
+})
index 6b0a12df35bc2952263c7236a39c18c25791ec51..ee30fcbcb41bee4feae6479cafa4a0e870043f47 100644 (file)
@@ -6,7 +6,9 @@ package ecdsa
 
 import (
        "bytes"
+       "crypto/internal/fips"
        "crypto/internal/fips/bigmod"
+       "crypto/internal/fips/drbg"
        "crypto/internal/fips/nistec"
        "errors"
        "io"
@@ -154,7 +156,8 @@ var p521Order = []byte{0x01, 0xff,
        0xbb, 0x6f, 0xb7, 0x1e, 0x91, 0x38, 0x64, 0x09}
 
 func NewPrivateKey[P Point[P]](c *Curve[P], D, Q []byte) (*PrivateKey, error) {
-       _, err := c.newPoint().SetBytes(Q)
+       fips.RecordApproved()
+       pub, err := NewPublicKey(c, Q)
        if err != nil {
                return nil, err
        }
@@ -162,49 +165,85 @@ func NewPrivateKey[P Point[P]](c *Curve[P], D, Q []byte) (*PrivateKey, error) {
        if err != nil {
                return nil, err
        }
-       return &PrivateKey{
-               pub: PublicKey{
-                       curve: c.curve,
-                       q:     Q,
-               },
-               d: d.Bytes(c.N),
-       }, nil
+       priv := &PrivateKey{pub: *pub, d: d.Bytes(c.N)}
+       fips.CAST("ECDSA PCT", func() error { return fipsPCT(c, priv) })
+       return priv, nil
 }
 
 func NewPublicKey[P Point[P]](c *Curve[P], Q []byte) (*PublicKey, error) {
+       // SetBytes checks that Q is a valid point on the curve, and that its
+       // coordinates are reduced modulo p, fulfilling the requirements of SP
+       // 800-89, Section 5.3.2.
        _, err := c.newPoint().SetBytes(Q)
        if err != nil {
                return nil, err
        }
-       return &PublicKey{
-               curve: c.curve,
-               q:     Q,
-       }, nil
+       return &PublicKey{curve: c.curve, q: Q}, nil
 }
 
 // GenerateKey generates a new ECDSA private key pair for the specified curve.
+//
+// In FIPS mode, rand is ignored.
 func GenerateKey[P Point[P]](c *Curve[P], rand io.Reader) (*PrivateKey, error) {
+       fips.RecordApproved()
        k, Q, err := randomPoint(c, rand)
        if err != nil {
                return nil, err
        }
-       return &PrivateKey{
+       priv := &PrivateKey{
                pub: PublicKey{
                        curve: c.curve,
                        q:     Q.Bytes(),
                },
                d: k.Bytes(c.N),
-       }, nil
+       }
+       fips.CAST("ECDSA PCT", func() error { return fipsPCT(c, priv) })
+       return priv, nil
 }
 
-// randomPoint returns a random scalar and the corresponding point using the
-// procedure given in FIPS 186-4, Appendix B.5.2 (rejection sampling).
+func fipsPCT[P Point[P]](c *Curve[P], k *PrivateKey) error {
+       hash := []byte{
+               0x32, 0xb3, 0xda, 0x19, 0xf9, 0x44, 0xb0, 0x48,
+               0x66, 0xd9, 0x31, 0xa6, 0x6c, 0x30, 0xb9, 0x4a,
+               0xe7, 0x28, 0xcc, 0xe7, 0x00, 0xe4, 0xb0, 0xa5,
+               0xb4, 0xfe, 0xae, 0x8f, 0x43, 0x8f, 0xde, 0xc2,
+       }
+       sig, err := Sign(c, k, nil, hash)
+       if err != nil {
+               return err
+       }
+       return Verify(c, &k.pub, hash, sig)
+}
+
+type testingOnlyFixedRandomPointReader struct{}
+
+func (testingOnlyFixedRandomPointReader) Read(p []byte) (int, error) { panic("not implemented") }
+
+// testingOnlyFixedRandomPoint is a signal to Sign that it should use a fixed
+// random point to enable the CAST. This is extremely dangerous because if it
+// were used with a production private key it would leak the private key.
+// We use this rather than a global variable to avoid accidentally using it in
+// production code. Still wish we didn't have to do this.
+var testingOnlyFixedRandomPoint = testingOnlyFixedRandomPointReader{}
+
+// randomPoint returns a random scalar and the corresponding point using a
+// procedure equivalent to FIPS 186-5, Appendix A.2.2 (ECDSA Key Pair Generation
+// by Rejection Sampling) and to Appendix A.2.2 (Per-Message Secret Number
+// Generation of Private Keys by Rejection Sampling) followed by Step 5 of
+// Section 6.4.1.
 func randomPoint[P Point[P]](c *Curve[P], rand io.Reader) (k *bigmod.Nat, p P, err error) {
-       k = bigmod.NewNat()
        for {
                b := make([]byte, c.N.Size())
-               if _, err = io.ReadFull(rand, b); err != nil {
-                       return
+               if fips.Enabled {
+                       if rand == testingOnlyFixedRandomPoint {
+                               b[len(b)-1] = 1
+                       } else {
+                               drbg.Read(b)
+                       }
+               } else {
+                       if _, err := io.ReadFull(rand, b); err != nil {
+                               return nil, nil, err
+                       }
                }
 
                // Mask off any excess bits to increase the chance of hitting a value in
@@ -220,21 +259,20 @@ func randomPoint[P Point[P]](c *Curve[P], rand io.Reader) (k *bigmod.Nat, p P, e
                        b[0] >>= excess
                }
 
-               // FIPS 186-4 makes us check k <= N - 2 and then add one.
-               // Checking 0 < k <= N - 1 is strictly equivalent.
-               // None of this matters anyway because the chance of selecting
-               // zero is cryptographically negligible.
-               if _, err = k.SetBytes(b, c.N); err == nil && k.IsZero() == 0 {
-                       break
+               // FIPS 186-5, Appendix A.4.2 makes us check x <= N - 2 and then return
+               // x + 1. Note that it follows that 0 < x + 1 < N. Instead, SetBytes
+               // checks that k < N, and we explicitly check 0 != k. Since k can't be
+               // negative, this is strictly equivalent. None of this matters anyway
+               // because the chance of selecting zero is cryptographically negligible.
+               if k, err := bigmod.NewNat().SetBytes(b, c.N); err == nil && k.IsZero() == 0 {
+                       p, err := c.newPoint().ScalarBaseMult(k.Bytes(c.N))
+                       return k, p, err
                }
 
                if testingOnlyRejectionSamplingLooped != nil {
                        testingOnlyRejectionSamplingLooped()
                }
        }
-
-       p, err = c.newPoint().ScalarBaseMult(k.Bytes(c.N))
-       return
 }
 
 // testingOnlyRejectionSamplingLooped is called when rejection sampling in
@@ -251,16 +289,18 @@ type Signature struct {
 // 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 to that length.
 //
-// The signature is randomized.
+// The signature is randomized. If FIPS mode is enabled, csprng is ignored.
 func Sign[P Point[P]](c *Curve[P], priv *PrivateKey, csprng io.Reader, hash []byte) (*Signature, error) {
        if priv.pub.curve != c.curve {
                return nil, errors.New("ecdsa: private key does not match curve")
        }
+       fips.RecordApproved()
+       fipsSelfTest()
        return sign(c, priv, csprng, hash)
 }
 
 func signGeneric[P Point[P]](c *Curve[P], priv *PrivateKey, csprng io.Reader, hash []byte) (*Signature, error) {
-       // SEC 1, Version 2.0, Section 4.1.3
+       // FIPS 186-5, Section 6.4.1
 
        k, R, err := randomPoint(c, csprng)
        if err != nil {
@@ -326,7 +366,7 @@ func inverse[P Point[P]](c *Curve[P], kInv, k *bigmod.Nat) {
 }
 
 // hashToNat sets e to the left-most bits of hash, according to
-// SEC 1, Section 4.1.3, point 5 and Section 4.1.4, point 3.
+// FIPS 186-5, Section 6.4.1, point 2 and Section 6.4.2, point 3.
 func hashToNat[P Point[P]](c *Curve[P], e *bigmod.Nat, hash []byte) {
        // ECDSA asks us to take the left-most log2(N) bits of hash, and use them as
        // an integer modulo N. This is the absolute worst of all worlds: we still
@@ -361,17 +401,19 @@ func Verify[P Point[P]](c *Curve[P], pub *PublicKey, hash []byte, sig *Signature
        if pub.curve != c.curve {
                return errors.New("ecdsa: public key does not match curve")
        }
+       fips.RecordApproved()
+       fipsSelfTest()
        return verify(c, pub, hash, sig)
 }
 
 func verifyGeneric[P Point[P]](c *Curve[P], pub *PublicKey, hash []byte, sig *Signature) error {
+       // FIPS 186-5, Section 6.4.2
+
        Q, err := c.newPoint().SetBytes(pub.q)
        if err != nil {
                return err
        }
 
-       // SEC 1, Version 2.0, Section 4.1.4
-
        r, err := bigmod.NewNat().SetBytes(sig.R, c.N)
        if err != nil {
                return err
index f6620945f4090e49126b295ac61a060de3383e7e..8f47443b5bb353a940b4f85c7bf5890daf286187 100644 (file)
@@ -18,6 +18,7 @@ import (
        _ "crypto/internal/fips/aes/gcm"
        _ "crypto/internal/fips/drbg"
        "crypto/internal/fips/ecdh"
+       "crypto/internal/fips/ecdsa"
        _ "crypto/internal/fips/hkdf"
        _ "crypto/internal/fips/hmac"
        "crypto/internal/fips/mlkem"
@@ -71,6 +72,7 @@ func findAllCASTs(t *testing.T) map[string]struct{} {
 func TestConditionals(t *testing.T) {
        mlkem.GenerateKey768()
        ecdh.GenerateKeyP256(rand.Reader)
+       ecdsa.GenerateKey(ecdsa.P256(), rand.Reader)
        t.Log("completed successfully")
 }