Răsfoiți Sursa

cert: Sign version 3 certificates with deterministic ML-DSA-87, so extending a certificate twice gives the same certificate

JackDoan 6 zile în urmă
părinte
comite
2ecf2f738e
2 a modificat fișierele cu 48 adăugiri și 3 ștergeri
  1. 7 3
      cert/cert_v3.go
  2. 41 0
      cert/cert_v3_test.go

+ 7 - 3
cert/cert_v3.go

@@ -21,7 +21,8 @@ import (
 // it with peers that only speak v2. The v2 form keeps its own classical CA signature and fingerprint.
 //
 // Hosts carry an ML-KEM-1024 encapsulation key and CAs an ML-DSA-87 public key. The issuing CA signs every field
-// with ML-DSA-87, including the classical signature, so the classical signature can not be swapped out either.
+// with ML-DSA-87, including the classical signature, so the classical signature can not be swapped out either. The
+// ML-DSA-87 signature is deterministic, so a certificate extended twice is the same certificate.
 //
 // Wherever a v3 certificate deals in keys they are composite: the classical key followed by the post-quantum one.
 // PublicKey returns classical || post-quantum, and VerifyPrivateKey and signing take the private keys the same way.
@@ -508,13 +509,16 @@ func ExtendToV3(c Certificate, pqPublicKey []byte, signer Certificate, pqSign Si
 	return c3, nil
 }
 
-// SignPQ returns a SignerLambda that signs certificates with the ML-DSA-87 private key seed.
+// SignPQ returns a SignerLambda that signs certificates with the ML-DSA-87 private key seed. Signatures are
+// deterministic (FIPS 204 section 3.4), so signing the same certificate twice gives the same bytes and the same
+// fingerprint, and extending a certificate is idempotent. The hedged variant guards signers an attacker can fault
+// physically, which a CA key in a file is not; the Ed25519 signature beside it is deterministic too.
 func SignPQ(seed []byte) (SignerLambda, error) {
 	sk, err := mldsa.NewPrivateKey(mldsa.MLDSA87(), seed)
 	if err != nil {
 		return nil, err
 	}
 	return func(b []byte) ([]byte, error) {
-		return sk.Sign(nil, b, &mldsa.Options{Context: pqSignatureContext})
+		return sk.SignDeterministic(b, &mldsa.Options{Context: pqSignatureContext})
 	}, nil
 }

+ 41 - 0
cert/cert_v3_test.go

@@ -389,6 +389,47 @@ func TestExtendToV3(t *testing.T) {
 	}
 }
 
+// ML-DSA-87 signing is deterministic, so extending a certificate twice gives the same bytes and the same fingerprint:
+// pq-extend is idempotent, and a CA extended again is not a second v3 CA to the pool.
+func TestExtendToV3_Deterministic(t *testing.T) {
+	for _, curve := range testCurves {
+		t.Run(curve.String(), func(t *testing.T) {
+			ca2, _, caKey2, _ := NewTestCaCert(Version2, curve, time.Time{}, time.Time{}, nil, nil, nil)
+			host2, _, _, _ := NewTestCert(Version2, curve, ca2, caKey2, "host", time.Time{}, time.Time{}, []netip.Prefix{netip.MustParsePrefix("10.1.0.1/16")}, nil, nil)
+			sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
+			require.NoError(t, err)
+			signPQ, err := SignPQ(sk.Bytes())
+			require.NoError(t, err)
+			dk, err := mlkem.GenerateKey1024()
+			require.NoError(t, err)
+
+			extend := func(c Certificate, pq []byte, signer Certificate) ([]byte, string, Certificate) {
+				c3, err := ExtendToV3(c, pq, signer, signPQ)
+				require.NoError(t, err)
+				b, err := c3.Marshal()
+				require.NoError(t, err)
+				fp, err := c3.Fingerprint()
+				require.NoError(t, err)
+				return b, fp, c3
+			}
+
+			caBytesA, caFpA, caA := extend(ca2, sk.PublicKey().Bytes(), nil)
+			caBytesB, caFpB, caB := extend(ca2, sk.PublicKey().Bytes(), nil)
+			assert.Equal(t, caBytesA, caBytesB)
+			assert.Equal(t, caFpA, caFpB)
+
+			hostBytesA, hostFpA, _ := extend(host2, dk.EncapsulationKey().Bytes(), caA)
+			hostBytesB, hostFpB, _ := extend(host2, dk.EncapsulationKey().Bytes(), caB)
+			assert.Equal(t, hostBytesA, hostBytesB)
+			assert.Equal(t, hostFpA, hostFpB)
+
+			pool := NewCAPool()
+			require.NoError(t, pool.AddCA(caA))
+			require.NoError(t, pool.AddCA(caB))
+		})
+	}
+}
+
 func TestCertificateV3_KeyPEM(t *testing.T) {
 	for _, curve := range testCurves {
 		t.Run(curve.String(), func(t *testing.T) {