]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/internal/fips140test: add ML-KEM ACVP tests
authorDaniel McCarney <daniel@binaryparadox.net>
Wed, 18 Dec 2024 18:26:20 +0000 (13:26 -0500)
committerFilippo Valsorda <filippo@golang.org>
Fri, 10 Jan 2025 12:37:57 +0000 (04:37 -0800)
Adds ACVP test coverage for ML-KEM based on the NIST spec:

  https://pages.nist.gov/ACVP/draft-celi-acvp-ml-kem.html

Notably we need to update the BoringSSL module version because the
acvptool was only recently updated to support testing ML-KEM.

A few non-test updates are also required for the
crypto/internal/fips140/mlkem package:

* For keyGen tests a new ExpandedBytes768() function is added that
  converts a DecapsualtionKey768 struct into the expanded NIST
  serialization. The existing Bytes() function returns the
  key's seed, while ACVP testing requires the more cumbersome format.
* For decap tests a new TestingOnlyNewDecapsulationKey768()
  constructor is added to produce a DecapsulationKey768 struct from the
  expanded FIPS 203 serialization provided by the ACVP test vector. The
  pre-existing NewDecapsulationKey768() function expects a seed as
  input.

The generate1024.go helper is updated to translate the above changes to
the generated mlkem1024.go implementation.

Both of these new functions are exclusively for ACVP usage and so not
present in the public mlkem API. End users should always prefer to work
with seeds.

Updates #69642

Change-Id: I79784f8a8db00a2ddefdcece4b8de50b033c8f69
Reviewed-on: https://go-review.googlesource.com/c/go/+/637439
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

src/crypto/internal/fips140/mlkem/generate1024.go
src/crypto/internal/fips140/mlkem/mlkem1024.go
src/crypto/internal/fips140/mlkem/mlkem768.go
src/crypto/internal/fips140test/acvp_capabilities.json
src/crypto/internal/fips140test/acvp_test.config.json
src/crypto/internal/fips140test/acvp_test.go

index e002bf1414ae966b8e8506db2cfffcb760a40cb9..9e38ad00df99e0fa356b682d34ea0407531ade01 100644 (file)
@@ -22,6 +22,7 @@ var replacements = map[string]string{
 
        "CiphertextSize768":       "CiphertextSize1024",
        "EncapsulationKeySize768": "EncapsulationKeySize1024",
+       "decapsulationKeySize768": "decapsulationKeySize1024",
 
        "encryptionKey": "encryptionKey1024",
        "decryptionKey": "decryptionKey1024",
@@ -33,9 +34,11 @@ var replacements = map[string]string{
        "kemEncaps":  "kemEncaps1024",
        "pkeEncrypt": "pkeEncrypt1024",
 
-       "DecapsulationKey768":    "DecapsulationKey1024",
-       "NewDecapsulationKey768": "NewDecapsulationKey1024",
-       "newKeyFromSeed":         "newKeyFromSeed1024",
+       "DecapsulationKey768":               "DecapsulationKey1024",
+       "NewDecapsulationKey768":            "NewDecapsulationKey1024",
+       "TestingOnlyNewDecapsulationKey768": "TestingOnlyNewDecapsulationKey1024",
+       "newKeyFromSeed":                    "newKeyFromSeed1024",
+       "TestingOnlyExpandedBytes768":       "TestingOnlyExpandedBytes1024",
 
        "kemDecaps":  "kemDecaps1024",
        "pkeDecrypt": "pkeDecrypt1024",
index c924c382933c5f19bda70a8512ce78cc80a79fb8..034bf3b5d6682d3164d4290437529ad2eae5d590 100644 (file)
@@ -3,6 +3,7 @@
 package mlkem
 
 import (
+       "bytes"
        "crypto/internal/fips140"
        "crypto/internal/fips140/drbg"
        "crypto/internal/fips140/sha3"
@@ -33,6 +34,32 @@ func (dk *DecapsulationKey1024) Bytes() []byte {
        return b[:]
 }
 
+// TestingOnlyExpandedBytes1024 returns the decapsulation key as a byte slice
+// using the full expanded NIST encoding.
+//
+// This should only be used for ACVP testing. For all other purposes prefer
+// the Bytes method that returns the (much smaller) seed.
+func TestingOnlyExpandedBytes1024(dk *DecapsulationKey1024) []byte {
+       b := make([]byte, 0, decapsulationKeySize1024)
+
+       // ByteEncode₁₂(s)
+       for i := range dk.s {
+               b = polyByteEncode(b, dk.s[i])
+       }
+
+       // ByteEncode₁₂(t) || ρ
+       for i := range dk.t {
+               b = polyByteEncode(b, dk.t[i])
+       }
+       b = append(b, dk.ρ[:]...)
+
+       // H(ek) || z
+       b = append(b, dk.h[:]...)
+       b = append(b, dk.z[:]...)
+
+       return b
+}
+
 // EncapsulationKey returns the public encapsulation key necessary to produce
 // ciphertexts.
 func (dk *DecapsulationKey1024) EncapsulationKey() *EncapsulationKey1024 {
@@ -130,6 +157,53 @@ func newKeyFromSeed1024(dk *DecapsulationKey1024, seed []byte) (*DecapsulationKe
        return dk, nil
 }
 
+// TestingOnlyNewDecapsulationKey1024 parses a decapsulation key from its expanded NIST format.
+//
+// Bytes() must not be called on the returned key, as it will not produce the
+// original seed.
+//
+// This function should only be used for ACVP testing. Prefer NewDecapsulationKey1024 for all
+// other purposes.
+func TestingOnlyNewDecapsulationKey1024(b []byte) (*DecapsulationKey1024, error) {
+       if len(b) != decapsulationKeySize1024 {
+               return nil, errors.New("mlkem: invalid NIST decapsulation key length")
+       }
+
+       dk := &DecapsulationKey1024{}
+       for i := range dk.s {
+               var err error
+               dk.s[i], err = polyByteDecode[nttElement](b[:encodingSize12])
+               if err != nil {
+                       return nil, errors.New("mlkem: invalid secret key encoding")
+               }
+               b = b[encodingSize12:]
+       }
+
+       ek, err := NewEncapsulationKey1024(b[:EncapsulationKeySize1024])
+       if err != nil {
+               return nil, err
+       }
+       dk.ρ = ek.ρ
+       dk.h = ek.h
+       dk.encryptionKey1024 = ek.encryptionKey1024
+       b = b[EncapsulationKeySize1024:]
+
+       if !bytes.Equal(dk.h[:], b[:32]) {
+               return nil, errors.New("mlkem: inconsistent H(ek) in encoded bytes")
+       }
+       b = b[32:]
+
+       copy(dk.z[:], b)
+
+       // Generate a random d value for use in Bytes(). This is a safety mechanism
+       // that avoids returning a broken key vs a random key if this function is
+       // called in contravention of the TestingOnlyNewDecapsulationKey1024 function
+       // comment advising against it.
+       drbg.Read(dk.d[:])
+
+       return dk, nil
+}
+
 // kemKeyGen1024 generates a decapsulation key.
 //
 // It implements ML-KEM.KeyGen_internal according to FIPS 203, Algorithm 16, and
index 2c1cb5c33fcdd110ea13a894dca1a10e59418a62..77043830d4d962e2162011e09311f482a370210e 100644 (file)
@@ -24,6 +24,7 @@ package mlkem
 //go:generate go run generate1024.go -input mlkem768.go -output mlkem1024.go
 
 import (
+       "bytes"
        "crypto/internal/fips140"
        "crypto/internal/fips140/drbg"
        "crypto/internal/fips140/sha3"
@@ -57,6 +58,7 @@ const (
 
        CiphertextSize768       = k*encodingSize10 + encodingSize4
        EncapsulationKeySize768 = k*encodingSize12 + 32
+       decapsulationKeySize768 = k*encodingSize12 + EncapsulationKeySize768 + 32 + 32
 )
 
 // ML-KEM-1024 parameters.
@@ -65,6 +67,7 @@ const (
 
        CiphertextSize1024       = k1024*encodingSize11 + encodingSize5
        EncapsulationKeySize1024 = k1024*encodingSize12 + 32
+       decapsulationKeySize1024 = k1024*encodingSize12 + EncapsulationKeySize1024 + 32 + 32
 )
 
 // A DecapsulationKey768 is the secret key used to decapsulate a shared key from a
@@ -90,6 +93,32 @@ func (dk *DecapsulationKey768) Bytes() []byte {
        return b[:]
 }
 
+// TestingOnlyExpandedBytes768 returns the decapsulation key as a byte slice
+// using the full expanded NIST encoding.
+//
+// This should only be used for ACVP testing. For all other purposes prefer
+// the Bytes method that returns the (much smaller) seed.
+func TestingOnlyExpandedBytes768(dk *DecapsulationKey768) []byte {
+       b := make([]byte, 0, decapsulationKeySize768)
+
+       // ByteEncode₁₂(s)
+       for i := range dk.s {
+               b = polyByteEncode(b, dk.s[i])
+       }
+
+       // ByteEncode₁₂(t) || ρ
+       for i := range dk.t {
+               b = polyByteEncode(b, dk.t[i])
+       }
+       b = append(b, dk.ρ[:]...)
+
+       // H(ek) || z
+       b = append(b, dk.h[:]...)
+       b = append(b, dk.z[:]...)
+
+       return b
+}
+
 // EncapsulationKey returns the public encapsulation key necessary to produce
 // ciphertexts.
 func (dk *DecapsulationKey768) EncapsulationKey() *EncapsulationKey768 {
@@ -187,6 +216,53 @@ func newKeyFromSeed(dk *DecapsulationKey768, seed []byte) (*DecapsulationKey768,
        return dk, nil
 }
 
+// TestingOnlyNewDecapsulationKey768 parses a decapsulation key from its expanded NIST format.
+//
+// Bytes() must not be called on the returned key, as it will not produce the
+// original seed.
+//
+// This function should only be used for ACVP testing. Prefer NewDecapsulationKey768 for all
+// other purposes.
+func TestingOnlyNewDecapsulationKey768(b []byte) (*DecapsulationKey768, error) {
+       if len(b) != decapsulationKeySize768 {
+               return nil, errors.New("mlkem: invalid NIST decapsulation key length")
+       }
+
+       dk := &DecapsulationKey768{}
+       for i := range dk.s {
+               var err error
+               dk.s[i], err = polyByteDecode[nttElement](b[:encodingSize12])
+               if err != nil {
+                       return nil, errors.New("mlkem: invalid secret key encoding")
+               }
+               b = b[encodingSize12:]
+       }
+
+       ek, err := NewEncapsulationKey768(b[:EncapsulationKeySize768])
+       if err != nil {
+               return nil, err
+       }
+       dk.ρ = ek.ρ
+       dk.h = ek.h
+       dk.encryptionKey = ek.encryptionKey
+       b = b[EncapsulationKeySize768:]
+
+       if !bytes.Equal(dk.h[:], b[:32]) {
+               return nil, errors.New("mlkem: inconsistent H(ek) in encoded bytes")
+       }
+       b = b[32:]
+
+       copy(dk.z[:], b)
+
+       // Generate a random d value for use in Bytes(). This is a safety mechanism
+       // that avoids returning a broken key vs a random key if this function is
+       // called in contravention of the TestingOnlyNewDecapsulationKey768 function
+       // comment advising against it.
+       drbg.Read(dk.d[:])
+
+       return dk, nil
+}
+
 // kemKeyGen generates a decapsulation key.
 //
 // It implements ML-KEM.KeyGen_internal according to FIPS 203, Algorithm 16, and
index 6502a98db12dcb7b285b51f233299002a2c376a1..38ce3a39c4b30fb3c29f5d8968a55ddd605383ac 100644 (file)
@@ -23,5 +23,8 @@
   {"algorithm":"HMAC-SHA3-384","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":384,"min":32}],"revision":"1.0"},
   {"algorithm":"HMAC-SHA3-512","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":512,"min":32}],"revision":"1.0"},
 
-  {"algorithm":"PBKDF","capabilities":[{"iterationCount":[{"min":1,"max":10000,"increment":1}],"keyLen":[{"min":112,"max":4096,"increment":8}],"passwordLen":[{"min":8,"max":64,"increment":1}],"saltLen":[{"min":128,"max":512,"increment":8}],"hmacAlg":["SHA2-224","SHA2-256","SHA2-384","SHA2-512","SHA2-512/224","SHA2-512/256","SHA3-224","SHA3-256","SHA3-384","SHA3-512"]}],"revision":"1.0"}
-]
\ No newline at end of file
+  {"algorithm":"PBKDF","capabilities":[{"iterationCount":[{"min":1,"max":10000,"increment":1}],"keyLen":[{"min":112,"max":4096,"increment":8}],"passwordLen":[{"min":8,"max":64,"increment":1}],"saltLen":[{"min":128,"max":512,"increment":8}],"hmacAlg":["SHA2-224","SHA2-256","SHA2-384","SHA2-512","SHA2-512/224","SHA2-512/256","SHA3-224","SHA3-256","SHA3-384","SHA3-512"]}],"revision":"1.0"},
+
+  {"algorithm":"ML-KEM","mode":"keyGen","revision":"FIPS203","parameterSets":["ML-KEM-768","ML-KEM-1024"]},
+  {"algorithm":"ML-KEM","mode":"encapDecap","revision":"FIPS203","parameterSets":["ML-KEM-768","ML-KEM-1024"],"functions":["encapsulation","decapsulation"]}
+]
index 49ab51d0d2a55235e01d7727244a4b9f8d56a81d..f62743f0c5d1aab8b0d29980c9964fe2442505fc 100644 (file)
@@ -23,5 +23,7 @@
   {"Wrapper": "go", "In": "vectors/HMAC-SHA3-384.bz2", "Out": "expected/HMAC-SHA3-384.bz2"},
   {"Wrapper": "go", "In": "vectors/HMAC-SHA3-512.bz2", "Out": "expected/HMAC-SHA3-512.bz2"},
 
-  {"Wrapper": "go", "In": "vectors/PBKDF.bz2", "Out": "expected/PBKDF.bz2"}
+  {"Wrapper": "go", "In": "vectors/PBKDF.bz2", "Out": "expected/PBKDF.bz2"},
+
+  {"Wrapper": "go", "In": "vectors/ML-KEM.bz2", "Out": "expected/ML-KEM.bz2"}
 ]
\ No newline at end of file
index 139655ecf6038993a1b3b99009253c64be06b3ca..70c2b7e718ddf4212f05ba5af46e620fb3eed7da 100644 (file)
@@ -24,6 +24,7 @@ import (
        "crypto/internal/cryptotest"
        "crypto/internal/fips140"
        "crypto/internal/fips140/hmac"
+       "crypto/internal/fips140/mlkem"
        "crypto/internal/fips140/pbkdf2"
        "crypto/internal/fips140/sha256"
        "crypto/internal/fips140/sha3"
@@ -75,6 +76,8 @@ var (
        //   https://pages.nist.gov/ACVP/draft-fussell-acvp-mac.html#section-7
        // PBKDF2 algorithm capabilities:
        //   https://pages.nist.gov/ACVP/draft-celi-acvp-pbkdf.html#section-7.3
+       // ML-KEM algorithm capabilities:
+       //   https://pages.nist.gov/ACVP/draft-celi-acvp-ml-kem.html#section-7.3
        //go:embed acvp_capabilities.json
        capabilitiesJson []byte
 
@@ -118,6 +121,13 @@ var (
                "HMAC-SHA3-512":     cmdHmacAft(func() fips140.Hash { return sha3.New512() }),
 
                "PBKDF": cmdPbkdf(),
+
+               "ML-KEM-768/keyGen":  cmdMlKem768KeyGenAft(),
+               "ML-KEM-768/encap":   cmdMlKem768EncapAft(),
+               "ML-KEM-768/decap":   cmdMlKem768DecapAft(),
+               "ML-KEM-1024/keyGen": cmdMlKem1024KeyGenAft(),
+               "ML-KEM-1024/encap":  cmdMlKem1024EncapAft(),
+               "ML-KEM-1024/decap":  cmdMlKem1024DecapAft(),
        }
 )
 
@@ -404,14 +414,138 @@ func lookupHash(name string) (func() fips140.Hash, error) {
        return h, nil
 }
 
+func cmdMlKem768KeyGenAft() command {
+       return command{
+               requiredArgs: 1, // Seed
+               handler: func(args [][]byte) ([][]byte, error) {
+                       seed := args[0]
+
+                       dk, err := mlkem.NewDecapsulationKey768(seed)
+                       if err != nil {
+                               return nil, fmt.Errorf("generating ML-KEM 768 decapsulation key: %w", err)
+                       }
+
+                       // Important: we must return the full encoding of dk, not the seed.
+                       return [][]byte{dk.EncapsulationKey().Bytes(), mlkem.TestingOnlyExpandedBytes768(dk)}, nil
+               },
+       }
+}
+
+func cmdMlKem768EncapAft() command {
+       return command{
+               requiredArgs: 2, // Public key, entropy
+               handler: func(args [][]byte) ([][]byte, error) {
+                       pk := args[0]
+                       entropy := args[1]
+
+                       ek, err := mlkem.NewEncapsulationKey768(pk)
+                       if err != nil {
+                               return nil, fmt.Errorf("generating ML-KEM 768 encapsulation key: %w", err)
+                       }
+
+                       if len(entropy) != 32 {
+                               return nil, fmt.Errorf("wrong entropy length: got %d, want 32", len(entropy))
+                       }
+
+                       sharedKey, ct := ek.EncapsulateInternal((*[32]byte)(entropy[:32]))
+
+                       return [][]byte{ct, sharedKey}, nil
+               },
+       }
+}
+
+func cmdMlKem768DecapAft() command {
+       return command{
+               requiredArgs: 2, // Private key, ciphertext
+               handler: func(args [][]byte) ([][]byte, error) {
+                       pk := args[0]
+                       ct := args[1]
+
+                       dk, err := mlkem.TestingOnlyNewDecapsulationKey768(pk)
+                       if err != nil {
+                               return nil, fmt.Errorf("generating ML-KEM 768 decapsulation key: %w", err)
+                       }
+
+                       sharedKey, err := dk.Decapsulate(ct)
+                       if err != nil {
+                               return nil, fmt.Errorf("decapsulating ML-KEM 768 ciphertext: %w", err)
+                       }
+
+                       return [][]byte{sharedKey}, nil
+               },
+       }
+}
+
+func cmdMlKem1024KeyGenAft() command {
+       return command{
+               requiredArgs: 1, // Seed
+               handler: func(args [][]byte) ([][]byte, error) {
+                       seed := args[0]
+
+                       dk, err := mlkem.NewDecapsulationKey1024(seed)
+                       if err != nil {
+                               return nil, fmt.Errorf("generating ML-KEM 1024 decapsulation key: %w", err)
+                       }
+
+                       // Important: we must return the full encoding of dk, not the seed.
+                       return [][]byte{dk.EncapsulationKey().Bytes(), mlkem.TestingOnlyExpandedBytes1024(dk)}, nil
+               },
+       }
+}
+
+func cmdMlKem1024EncapAft() command {
+       return command{
+               requiredArgs: 2, // Public key, entropy
+               handler: func(args [][]byte) ([][]byte, error) {
+                       pk := args[0]
+                       entropy := args[1]
+
+                       ek, err := mlkem.NewEncapsulationKey1024(pk)
+                       if err != nil {
+                               return nil, fmt.Errorf("generating ML-KEM 1024 encapsulation key: %w", err)
+                       }
+
+                       if len(entropy) != 32 {
+                               return nil, fmt.Errorf("wrong entropy length: got %d, want 32", len(entropy))
+                       }
+
+                       sharedKey, ct := ek.EncapsulateInternal((*[32]byte)(entropy[:32]))
+
+                       return [][]byte{ct, sharedKey}, nil
+               },
+       }
+}
+
+func cmdMlKem1024DecapAft() command {
+       return command{
+               requiredArgs: 2, // Private key, ciphertext
+               handler: func(args [][]byte) ([][]byte, error) {
+                       pk := args[0]
+                       ct := args[1]
+
+                       dk, err := mlkem.TestingOnlyNewDecapsulationKey1024(pk)
+                       if err != nil {
+                               return nil, fmt.Errorf("generating ML-KEM 1024 decapsulation key: %w", err)
+                       }
+
+                       sharedKey, err := dk.Decapsulate(ct)
+                       if err != nil {
+                               return nil, fmt.Errorf("decapsulating ML-KEM 1024 ciphertext: %w", err)
+                       }
+
+                       return [][]byte{sharedKey}, nil
+               },
+       }
+}
+
 func TestACVP(t *testing.T) {
        testenv.SkipIfShortAndSlow(t)
 
        const (
                bsslModule    = "boringssl.googlesource.com/boringssl.git"
-               bsslVersion   = "v0.0.0-20241015160643-2587c4974dbe"
+               bsslVersion   = "v0.0.0-20241218033850-ca3146c56300"
                goAcvpModule  = "github.com/cpu/go-acvp"
-               goAcvpVersion = "v0.0.0-20241011151719-6e0509dcb7ce"
+               goAcvpVersion = "v0.0.0-20250102201911-6839fc40f9f8"
        )
 
        // In crypto/tls/bogo_shim_test.go the test is skipped if run on a builder with runtime.GOOS == "windows"