diff --git a/src/crypto/ecdsa/ecdsa.go b/src/crypto/ecdsa/ecdsa.go index 5ceef823a57..4621cc9780c 100644 --- a/src/crypto/ecdsa/ecdsa.go +++ b/src/crypto/ecdsa/ecdsa.go @@ -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 @@ -14,15 +14,6 @@ // [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 index 00000000000..ed6d632c1c1 --- /dev/null +++ b/src/crypto/internal/fips/ecdsa/cast.go @@ -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 + }) +}) diff --git a/src/crypto/internal/fips/ecdsa/ecdsa.go b/src/crypto/internal/fips/ecdsa/ecdsa.go index 6b0a12df35b..ee30fcbcb41 100644 --- a/src/crypto/internal/fips/ecdsa/ecdsa.go +++ b/src/crypto/internal/fips/ecdsa/ecdsa.go @@ -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 diff --git a/src/crypto/internal/fipstest/cast_test.go b/src/crypto/internal/fipstest/cast_test.go index f6620945f40..8f47443b5bb 100644 --- a/src/crypto/internal/fipstest/cast_test.go +++ b/src/crypto/internal/fipstest/cast_test.go @@ -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") }