Support ML-DSA in libssl Bug: 497880128 Change-Id: I2026f4eb43ef0ec671babb0daf46a1d9e09053c3 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/93807 Auto-Submit: Nick Harper <nharper@chromium.org> Commit-Queue: Nick Harper <nharper@chromium.org> Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/go.mod b/go.mod index 8d0e6cd..b185cca 100644 --- a/go.mod +++ b/go.mod
@@ -1,12 +1,12 @@ module boringssl.googlesource.com/boringssl.git -go 1.24.0 - -toolchain go1.24.8 +go 1.25.0 require ( cloud.google.com/go/storage v1.59.2 filippo.io/edwards25519 v1.2.0 + // TODO(crbug.com/505771670): Replace with crypto/mldsa once that exists. + filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e github.com/hexops/gotextdiff v1.0.3 golang.org/x/crypto v0.43.0 golang.org/x/net v0.46.0
diff --git a/go.sum b/go.sum index 8877b88..3ca048b 100644 --- a/go.sum +++ b/go.sum
@@ -22,6 +22,8 @@ cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e h1:VsUbObBMxXlc23Eb9VeeJYE4jvTs87qa5RqSN2U5FJU= +filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e/go.mod h1:32qQ5yj3R24Eu03iWFWchdC3OB653wPvoepWejkefbY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h index 182cef2..453318b 100644 --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h
@@ -1154,6 +1154,9 @@ #define SSL_SIGN_RSA_PSS_RSAE_SHA384 0x0805 #define SSL_SIGN_RSA_PSS_RSAE_SHA512 0x0806 #define SSL_SIGN_ED25519 0x0807 +#define SSL_SIGN_ML_DSA_44 0x0904 +#define SSL_SIGN_ML_DSA_65 0x0905 +#define SSL_SIGN_ML_DSA_87 0x0906 // SSL_SIGN_RSA_PKCS1_SHA256_LEGACY is a backport of RSASSA-PKCS1-v1_5 with // SHA-256 to TLS 1.3. It is disabled by default and only defined for client
diff --git a/ssl/extensions.cc b/ssl/extensions.cc index 5292268..50ce434 100644 --- a/ssl/extensions.cc +++ b/ssl/extensions.cc
@@ -309,6 +309,11 @@ // algorithms for signing. static const uint16_t kSignSignatureAlgorithms[] = { // List our preferred algorithms first. + SSL_SIGN_ML_DSA_44, + SSL_SIGN_ML_DSA_65, + SSL_SIGN_ML_DSA_87, + + // Classical signature algorithms. SSL_SIGN_ED25519, SSL_SIGN_ECDSA_SECP256R1_SHA256, SSL_SIGN_RSA_PSS_RSAE_SHA256,
diff --git a/ssl/ssl_privkey.cc b/ssl/ssl_privkey.cc index 190eed4..0262233 100644 --- a/ssl/ssl_privkey.cc +++ b/ssl/ssl_privkey.cc
@@ -34,8 +34,16 @@ BSSL_NAMESPACE_BEGIN bool ssl_is_key_type_supported(int key_type) { - return key_type == EVP_PKEY_RSA || key_type == EVP_PKEY_EC || - key_type == EVP_PKEY_ED25519; + switch (key_type) { + case EVP_PKEY_RSA: + case EVP_PKEY_EC: + case EVP_PKEY_ED25519: + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + return true; + } + return false; } typedef struct { @@ -99,6 +107,16 @@ {SSL_SIGN_ED25519, EVP_PKEY_ED25519, NID_undef, nullptr, /*is_rsa_pss=*/false, /*tls12_ok=*/true, /*tls13_ok=*/true, /*client_only=*/false}, + + {SSL_SIGN_ML_DSA_44, EVP_PKEY_ML_DSA_44, NID_undef, nullptr, + /*is_rsa_pss=*/false, /*tls12_ok=*/false, /*tls13_ok=*/true, + /*client_only=*/false}, + {SSL_SIGN_ML_DSA_65, EVP_PKEY_ML_DSA_65, NID_undef, nullptr, + /*is_rsa_pss=*/false, /*tls12_ok=*/false, /*tls13_ok=*/true, + /*client_only=*/false}, + {SSL_SIGN_ML_DSA_87, EVP_PKEY_ML_DSA_87, NID_undef, nullptr, + /*is_rsa_pss=*/false, /*tls12_ok=*/false, /*tls13_ok=*/true, + /*client_only=*/false}, }; static const SSL_SIGNATURE_ALGORITHM *get_signature_algorithm(uint16_t sigalg) { @@ -116,8 +134,9 @@ // code elimination, but for now we just specify every algorithm that might be // reachable from libssl. const EVP_PKEY_ALG *const algs[] = { - EVP_pkey_rsa(), EVP_pkey_ec_p256(), EVP_pkey_ec_p384(), - EVP_pkey_ec_p521(), EVP_pkey_ed25519(), + EVP_pkey_rsa(), EVP_pkey_ec_p256(), EVP_pkey_ec_p384(), + EVP_pkey_ec_p521(), EVP_pkey_ed25519(), EVP_pkey_ml_dsa_44(), + EVP_pkey_ml_dsa_65(), EVP_pkey_ml_dsa_87(), }; return bssl::UniquePtr<EVP_PKEY>(EVP_PKEY_from_subject_public_key_info( spki.data(), spki.size(), algs, std::size(algs))); @@ -481,6 +500,9 @@ {SSL_SIGN_RSA_PSS_RSAE_SHA384, "rsa_pss_rsae_sha384"}, {SSL_SIGN_RSA_PSS_RSAE_SHA512, "rsa_pss_rsae_sha512"}, {SSL_SIGN_ED25519, "ed25519"}, + {SSL_SIGN_ML_DSA_44, "mldsa44"}, + {SSL_SIGN_ML_DSA_65, "mldsa65"}, + {SSL_SIGN_ML_DSA_87, "mldsa87"}, }; const char *SSL_get_signature_algorithm_name(uint16_t sigalg, @@ -645,6 +667,9 @@ {EVP_PKEY_EC, NID_sha384, SSL_SIGN_ECDSA_SECP384R1_SHA384}, {EVP_PKEY_EC, NID_sha512, SSL_SIGN_ECDSA_SECP521R1_SHA512}, {EVP_PKEY_ED25519, NID_undef, SSL_SIGN_ED25519}, + {EVP_PKEY_ML_DSA_44, NID_undef, SSL_SIGN_ML_DSA_44}, + {EVP_PKEY_ML_DSA_65, NID_undef, SSL_SIGN_ML_DSA_65}, + {EVP_PKEY_ML_DSA_87, NID_undef, SSL_SIGN_ML_DSA_87}, }; static bool parse_sigalg_pairs(Array<uint16_t> *out, const int *values,
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc index 49adcaf..7a6da95 100644 --- a/ssl/ssl_test.cc +++ b/ssl/ssl_test.cc
@@ -36,8 +36,10 @@ #include <openssl/crypto.h> #include <openssl/curve25519.h> #include <openssl/err.h> +#include <openssl/evp.h> #include <openssl/hmac.h> #include <openssl/hpke.h> +#include <openssl/nid.h> #include <openssl/pem.h> #include <openssl/rand.h> #include <openssl/sha.h> @@ -6888,6 +6890,7 @@ {{1, 2, 3}, false, {}}, {{NID_sha256, EVP_PKEY_ED25519}, false, {}}, {{NID_sha256, EVP_PKEY_RSA, NID_sha256, EVP_PKEY_RSA}, false, {}}, + {{NID_sha256, EVP_PKEY_ML_DSA_44}, false, {}}, {{NID_sha256, EVP_PKEY_RSA}, true, {SSL_SIGN_RSA_PKCS1_SHA256}}, {{NID_sha512, EVP_PKEY_RSA}, true, {SSL_SIGN_RSA_PKCS1_SHA512}}, @@ -6896,6 +6899,9 @@ {{NID_undef, EVP_PKEY_ED25519, NID_sha384, EVP_PKEY_EC}, true, {SSL_SIGN_ED25519, SSL_SIGN_ECDSA_SECP384R1_SHA384}}, + {{NID_undef, EVP_PKEY_ML_DSA_44}, true, {SSL_SIGN_ML_DSA_44}}, + {{NID_undef, EVP_PKEY_ML_DSA_65}, true, {SSL_SIGN_ML_DSA_65}}, + {{NID_undef, EVP_PKEY_ML_DSA_87}, true, {SSL_SIGN_ML_DSA_87}}, }; UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method())); @@ -6952,6 +6958,9 @@ {SSL_SIGN_ECDSA_SECP256R1_SHA256, SSL_SIGN_RSA_PSS_RSAE_SHA256}}, {"RSA-PSS+SHA256", true, {SSL_SIGN_RSA_PSS_RSAE_SHA256}}, {"PSS+SHA256", true, {SSL_SIGN_RSA_PSS_RSAE_SHA256}}, + {"mldsa44:mldsa65:mldsa87", + true, + {SSL_SIGN_ML_DSA_44, SSL_SIGN_ML_DSA_65, SSL_SIGN_ML_DSA_87}}, }; UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
diff --git a/ssl/test/runner/certs.go b/ssl/test/runner/certs.go index 34c9f5f..06f8ee7 100644 --- a/ssl/test/runner/certs.go +++ b/ssl/test/runner/certs.go
@@ -32,6 +32,7 @@ "sync/atomic" "time" + "filippo.io/mldsa" "golang.org/x/crypto/cryptobyte" cbasn1 "golang.org/x/crypto/cryptobyte/asn1" ) @@ -47,6 +48,10 @@ oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} oidPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} + oidMLDSA44 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 17} + oidMLDSA65 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 18} + oidMLDSA87 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 19} + oidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} oidMGF1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 8} @@ -69,6 +74,9 @@ X509SignRSAWithSHA256 X509SignECDSAWithSHA256 X509SignEd25519 + X509SignMLDSA44 + X509SignMLDSA65 + X509SignMLDSA87 ) func (alg X509SignatureAlgorithm) Marshal(bb *cryptobyte.Builder) error { @@ -86,12 +94,37 @@ bb.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { algID.AddASN1ObjectIdentifier(oidEd25519) }) + case X509SignMLDSA44: + bb.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { + algID.AddASN1ObjectIdentifier(oidMLDSA44) + }) + case X509SignMLDSA65: + bb.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { + algID.AddASN1ObjectIdentifier(oidMLDSA65) + }) + case X509SignMLDSA87: + bb.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { + algID.AddASN1ObjectIdentifier(oidMLDSA87) + }) default: return errors.New("unknown algorithm") } return nil } +func mldsaX509SignatureAlgorithm(key *mldsa.PrivateKey) (X509SignatureAlgorithm, error) { + params := key.PublicKey().Parameters() + switch params { + case mldsa.MLDSA44(): + return X509SignMLDSA44, nil + case mldsa.MLDSA65(): + return X509SignMLDSA65, nil + case mldsa.MLDSA87(): + return X509SignMLDSA87, nil + } + return 0, fmt.Errorf("unsupported ML-DSA Parameters: %v", params) +} + func chooseDefaultX509SignatureAlgorithm(signer crypto.Signer) (X509SignatureAlgorithm, error) { if _, ok := signer.(*rsa.PrivateKey); ok { return X509SignRSAWithSHA256, nil @@ -102,6 +135,9 @@ if _, ok := signer.(ed25519.PrivateKey); ok { return X509SignEd25519, nil } + if mldsaKey, ok := signer.(*mldsa.PrivateKey); ok { + return mldsaX509SignatureAlgorithm(mldsaKey) + } return 0, fmt.Errorf("unsupported key type: %T", signer) } @@ -128,17 +164,20 @@ default: return nil, errors.New("unknown algorithm") } + } else if mldsaKey, ok := signer.(*mldsa.PrivateKey); ok { + params := mldsaKey.PublicKey().Parameters() + algMatchesParams := (alg == X509SignMLDSA44 && params == mldsa.MLDSA44()) || + (alg == X509SignMLDSA65 && params == mldsa.MLDSA65()) || + (alg == X509SignMLDSA87 && params == mldsa.MLDSA87()) + if !algMatchesParams { + return nil, errors.New("unknown algorithm") + } + opts = crypto.Hash(0) } else { return nil, fmt.Errorf("unsupported key type: %T", signer) } - digest := in - if hash := opts.HashFunc(); hash != crypto.Hash(0) { - h := hash.New() - h.Write(in) - digest = h.Sum(nil) - } - return signer.Sign(rand.Reader, digest, opts) + return crypto.SignMessage(signer, rand.Reader, in, opts) } func addX509Time(bb *cryptobyte.Builder, t time.Time) { @@ -160,6 +199,24 @@ }) } +func addMLDSASubjectPublicKeyInfo(bb *cryptobyte.Builder, key *mldsa.PublicKey) { + bb.AddASN1(cbasn1.SEQUENCE, func(spki *cryptobyte.Builder) { + spki.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { + switch key.Parameters() { + case mldsa.MLDSA44(): + algID.AddASN1ObjectIdentifier(oidMLDSA44) + case mldsa.MLDSA65(): + algID.AddASN1ObjectIdentifier(oidMLDSA65) + case mldsa.MLDSA87(): + algID.AddASN1ObjectIdentifier(oidMLDSA87) + default: + panic("Unknown ML-DSA parameter set") + } + }) + spki.AddASN1BitString(key.Bytes()) + }) +} + func addRSAPSSSubjectPublicKeyInfo(bb *cryptobyte.Builder, key *rsa.PublicKey) { bb.AddASN1(cbasn1.SEQUENCE, func(spki *cryptobyte.Builder) { spki.AddASN1(cbasn1.SEQUENCE, func(algID *cryptobyte.Builder) { @@ -264,7 +321,11 @@ return } tbs.AddBytes(subjectDER) - if subject.EncodeSPKIAsRSAPSS { + if mldsaKey, ok := subject.PrivateKey.Public().(*mldsa.PublicKey); ok { + // TODO(crbug.com/505771670): Once crypto/x509 supports ML-DSA, remove + // this and use x509.MarshalPKIXPublicKey (below) directly. + addMLDSASubjectPublicKeyInfo(tbs, mldsaKey) + } else if subject.EncodeSPKIAsRSAPSS { addRSAPSSSubjectPublicKeyInfo(tbs, subject.PrivateKey.Public().(*rsa.PublicKey)) } else { spki, err := x509.MarshalPKIXPublicKey(subject.PrivateKey.Public()) @@ -385,6 +446,57 @@ } } +func reparseSPKI(cert *x509.Certificate) { + spkiSeq := cryptobyte.String(cert.RawSubjectPublicKeyInfo) + var spki, alg cryptobyte.String + var algOID asn1.ObjectIdentifier + if !spkiSeq.ReadASN1(&spki, cbasn1.SEQUENCE) || + !spki.ReadASN1(&alg, cbasn1.SEQUENCE) || + !alg.ReadASN1ObjectIdentifier(&algOID) { + return + } + // This function only supports ML-DSA SPKIs + var mldsaParams *mldsa.Parameters + if algOID.Equal(oidMLDSA44) { + mldsaParams = mldsa.MLDSA44() + } else if algOID.Equal(oidMLDSA65) { + mldsaParams = mldsa.MLDSA65() + } else if algOID.Equal(oidMLDSA87) { + mldsaParams = mldsa.MLDSA87() + } else { + // Not an ML-DSA OID + return + } + // The AlgorithmIdentifier (for ML-DSA) should have empty params + if !alg.Empty() { + return + } + var publicKey []byte + if !spki.ReadASN1BitStringAsBytes(&publicKey) { + return + } + mldsaPubKey, err := mldsa.NewPublicKey(mldsaParams, publicKey) + if err != nil { + return + } + cert.PublicKey = mldsaPubKey +} + +// ParseX509Certificate is a wrapper around x509.ParseCertificate, but it also +// handles parsing ML-DSA public keys. +// +// TODO(crbug.com/505771670): Remove this once crypto/x509 supports ML-DSA. +func ParseX509Certificate(der []byte) (*x509.Certificate, error) { + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + if cert.PublicKey == nil { + reparseSPKI(cert) + } + return cert, nil +} + func writeTempCertFile(certs [][]byte) string { f, err := os.CreateTemp(tmpDir, "test-cert") if err != nil { @@ -407,9 +519,32 @@ if err != nil { panic(fmt.Sprintf("failed to create temp file: %s", err)) } - keyDER, err := x509.MarshalPKCS8PrivateKey(privKey) - if err != nil { - panic(fmt.Sprintf("failed to marshal test key: %s", err)) + var keyDER []byte + // TODO(crbug.com/505771670): Remove this once crypto/x509 supports ML-DSA. + if mldsaKey, ok := privKey.(*mldsa.PrivateKey); ok { + sigAlg, err := mldsaX509SignatureAlgorithm(mldsaKey) + if err != nil { + panic(fmt.Sprintf("failed to get SignatureAlgorithm for ML-DSA key: %s", err)) + } + bb := cryptobyte.NewBuilder(make([]byte, 0, 54)) + bb.AddASN1(cbasn1.SEQUENCE, func(pkcs8 *cryptobyte.Builder) { + pkcs8.AddASN1Uint64(0) + sigAlg.Marshal(pkcs8) + pkcs8.AddASN1(cbasn1.OCTET_STRING, func(privKey *cryptobyte.Builder) { + privKey.AddASN1(cbasn1.Tag(0).ContextSpecific(), func(b *cryptobyte.Builder) { + b.AddBytes(mldsaKey.Bytes()) + }) + }) + }) + keyDER, err = bb.Bytes() + if err != nil { + panic(fmt.Sprintf("Failed to marshal test ML-DSA key: %s", err)) + } + } else { + keyDER, err = x509.MarshalPKCS8PrivateKey(privKey) + if err != nil { + panic(fmt.Sprintf("failed to marshal test key: %s", err)) + } } if err := pem.Encode(f, &pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}); err != nil { panic(fmt.Sprintf("failed to write test key: %s", err))
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go index 2d17483..ad8e3f7 100644 --- a/ssl/test/runner/common.go +++ b/ssl/test/runner/common.go
@@ -329,6 +329,11 @@ signatureEd25519 signatureAlgorithm = 0x0807 signatureEd448 signatureAlgorithm = 0x0808 + // ML-DSA algorithms (draft-ietf-tls-mldsa-02) + signatureMLDSA44 signatureAlgorithm = 0x0904 + signatureMLDSA65 signatureAlgorithm = 0x0905 + signatureMLDSA87 signatureAlgorithm = 0x0906 + // draft-ietf-tls-tls13-pkcs1-00 signatureRSAPKCS1WithSHA256Legacy signatureAlgorithm = 0x0420
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go index 4f26321..b5d7084 100644 --- a/ssl/test/runner/handshake_client.go +++ b/ssl/test/runner/handshake_client.go
@@ -23,6 +23,7 @@ "boringssl.googlesource.com/boringssl.git/ssl/test/runner/hpke" "boringssl.googlesource.com/boringssl.git/ssl/test/runner/spake2plus" + "filippo.io/mldsa" "golang.org/x/crypto/cryptobyte" ) @@ -1924,7 +1925,7 @@ certs := make([]*x509.Certificate, len(certMsg.certificates)) if certMsg.certificateType == certTypeX509 { for i, certEntry := range certMsg.certificates { - cert, err := x509.ParseCertificate(certEntry.data) + cert, err := ParseX509Certificate(certEntry.data) if err != nil { c.sendAlert(alertBadCertificate) return errors.New("tls: failed to parse certificate from server: " + err.Error()) @@ -1971,7 +1972,7 @@ } switch peerPublicKey.(type) { - case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, *mldsa.PublicKey: break default: c.sendAlert(alertUnsupportedCertificate)
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go index 636c932..8425f74 100644 --- a/ssl/test/runner/handshake_server.go +++ b/ssl/test/runner/handshake_server.go
@@ -22,6 +22,7 @@ "boringssl.googlesource.com/boringssl.git/ssl/test/runner/hpke" "boringssl.googlesource.com/boringssl.git/ssl/test/runner/spake2plus" + "filippo.io/mldsa" "golang.org/x/crypto/cryptobyte" ) @@ -2351,7 +2352,7 @@ certs := make([]*x509.Certificate, len(certificates)) var err error for i, asn1Data := range certificates { - if certs[i], err = x509.ParseCertificate(asn1Data); err != nil { + if certs[i], err = ParseX509Certificate(asn1Data); err != nil { c.sendAlert(alertBadCertificate) return nil, errors.New("tls: failed to parse client certificate: " + err.Error()) } @@ -2386,7 +2387,7 @@ if len(certs) > 0 { pub := certs[0].PublicKey switch pub.(type) { - case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey: + case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey, *mldsa.PublicKey: break default: c.sendAlert(alertUnsupportedCertificate)
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go index 31bffa2..f092873 100644 --- a/ssl/test/runner/runner.go +++ b/ssl/test/runner/runner.go
@@ -45,6 +45,7 @@ "time" "boringssl.googlesource.com/boringssl.git/util/testresult" + "filippo.io/mldsa" "golang.org/x/crypto/cryptobyte" ) @@ -139,6 +140,10 @@ ed25519Key ed25519.PrivateKey + mldsa44Key *mldsa.PrivateKey + mldsa65Key *mldsa.PrivateKey + mldsa87Key *mldsa.PrivateKey + channelIDKey ecdsa.PrivateKey ) @@ -187,6 +192,21 @@ } ed25519Key = k.(ed25519.PrivateKey) + for _, k := range []struct { + params *mldsa.Parameters + key **mldsa.PrivateKey + }{ + {mldsa.MLDSA44(), &mldsa44Key}, + {mldsa.MLDSA65(), &mldsa65Key}, + {mldsa.MLDSA87(), &mldsa87Key}, + } { + key, err := mldsa.GenerateKey(k.params) + if err != nil { + panic(fmt.Sprintf("Failed to generate ML-DSA test key: %s", err)) + } + *k.key = key + } + channelIDKeyPath = writeTempKeyFile(&channelIDKey) } @@ -214,6 +234,9 @@ ecdsaP384Certificate Credential ecdsaP521Certificate Credential ed25519Certificate Credential + mldsa44Certificate Credential + mldsa65Certificate Credential + mldsa87Certificate Credential garbageCertificate Credential pssCertificate Credential ) @@ -230,6 +253,9 @@ {"ECDSA P-384", &ecdsaP384Key, &ecdsaP384Certificate}, {"ECDSA P-521", &ecdsaP521Key, &ecdsaP521Certificate}, {"Ed25519", ed25519Key, &ed25519Certificate}, + {"ML-DSA 44", mldsa44Key, &mldsa44Certificate}, + {"ML-DSA 65", mldsa65Key, &mldsa65Certificate}, + {"ML-DSA 87", mldsa87Key, &mldsa87Certificate}, } { // For each test key, make a self-signed root that issues a leaf, using // the same algorithm.
diff --git a/ssl/test/runner/sign.go b/ssl/test/runner/sign.go index 2dc8359..8d799fd 100644 --- a/ssl/test/runner/sign.go +++ b/ssl/test/runner/sign.go
@@ -19,6 +19,8 @@ "fmt" "math/big" "slices" + + "filippo.io/mldsa" ) type signer interface { @@ -286,6 +288,31 @@ return nil } +type mldsaSigner struct{} + +func (m *mldsaSigner) supportsKey(key crypto.PrivateKey) bool { + _, ok := key.(*mldsa.PrivateKey) + return ok +} + +func (m *mldsaSigner) signMessage(key crypto.PrivateKey, config *Config, msg []byte) ([]byte, error) { + privKey, ok := key.(*mldsa.PrivateKey) + if !ok { + return nil, errors.New("invalid key type for ML-DSA") + } + + return privKey.Sign(nil, msg, nil) +} + +func (m *mldsaSigner) verifyMessage(key crypto.PublicKey, msg, sig []byte) error { + pubKey, ok := key.(*mldsa.PublicKey) + if !ok { + return errors.New("invalid key type for ML-DSA") + } + + return mldsa.Verify(pubKey, msg, sig, nil) +} + func getSigner(isClient bool, version version, key any, config *Config, sigAlg signatureAlgorithm, isVerify bool) (signer, error) { // TLS 1.1 and below use legacy signature algorithms. protoVers := version.protocolVersion() @@ -347,6 +374,12 @@ return &rsaPSSSigner{crypto.SHA512}, nil case signatureEd25519: return &ed25519Signer{}, nil + case signatureMLDSA44: + return &mldsaSigner{}, nil + case signatureMLDSA65: + return &mldsaSigner{}, nil + case signatureMLDSA87: + return &mldsaSigner{}, nil } return nil, fmt.Errorf("unsupported signature algorithm %04x", sigAlg)
diff --git a/ssl/test/runner/signature_algorithm_tests.go b/ssl/test/runner/signature_algorithm_tests.go index 6e56228..bb93eb4 100644 --- a/ssl/test/runner/signature_algorithm_tests.go +++ b/ssl/test/runner/signature_algorithm_tests.go
@@ -40,6 +40,9 @@ {"RSA_PSS_SHA384", signatureRSAPSSWithSHA384, &rsaCertificate, 0}, {"RSA_PSS_SHA512", signatureRSAPSSWithSHA512, &rsaCertificate, 0}, {"Ed25519", signatureEd25519, &ed25519Certificate, 0}, + {"ML-DSA-44", signatureMLDSA44, &mldsa44Certificate, 0}, + {"ML-DSA-65", signatureMLDSA65, &mldsa65Certificate, 0}, + {"ML-DSA-87", signatureMLDSA87, &mldsa87Certificate, 0}, // Tests for key types prior to TLS 1.2. {"RSA", 0, &rsaCertificate, 0}, {"ECDSA", 0, &ecdsaP256Certificate, CurveP256}, @@ -95,6 +98,7 @@ var shouldFail bool isTLS12PKCS1 := hasComponent(alg.name, "PKCS1") && !hasComponent(alg.name, "LEGACY") isTLS13PKCS1 := hasComponent(alg.name, "PKCS1") && hasComponent(alg.name, "LEGACY") + isMLDSA := alg.id == signatureMLDSA44 || alg.id == signatureMLDSA65 || alg.id == signatureMLDSA87 // TLS 1.3 removes a number of signature algorithms. if ver.version >= VersionTLS13 && (alg.id == signatureECDSAWithSHA1 || isTLS12PKCS1) { @@ -107,6 +111,11 @@ shouldFail = true } + // ML-DSA is only supported for TLS 1.3 + if ver.version < VersionTLS13 && isMLDSA { + shouldFail = true + } + // By default, BoringSSL does not sign with these algorithms. signDefault := !shouldFail if isTLS13PKCS1 { @@ -115,7 +124,7 @@ // By default, BoringSSL does not accept these algorithms. verifyDefault := !shouldFail - if alg.id == signatureECDSAWithSHA1 || alg.id == signatureECDSAWithP521AndSHA512 || alg.id == signatureEd25519 || isTLS13PKCS1 { + if alg.id == signatureECDSAWithSHA1 || alg.id == signatureECDSAWithP521AndSHA512 || alg.id == signatureEd25519 || isTLS13PKCS1 || isMLDSA { verifyDefault = false } @@ -140,6 +149,12 @@ if ver.version <= VersionTLS12 && signTestType == serverTest { return ":NO_SHARED_CIPHER:" } + if ver.version <= VersionTLS12 && isMLDSA { + // In TLS 1.2, there's no valid way for the server to request an + // ML-DSA client certificate (there's no ClientCertificateType for + // ML-DSA). + return ":UNKNOWN_CERTIFICATE_TYPE:" + } return ":NO_COMMON_SIGNATURE_ALGORITHMS:" } signLocalError := func(shouldFail bool) string { @@ -154,6 +169,12 @@ if !shouldFail { return "" } + if ver.version <= VersionTLS12 && verifyTestType == clientTest && isMLDSA { + // TLS 1.2 has no support for ML-DSA signatures. The client code + // rejects a server's attempt to sign with ML-DSA when it sees the + // certificate. + return ":WRONG_CERTIFICATE_TYPE:" + } // If the shim rejects the signature algorithm, but the // runner forcibly selects it anyway, the shim should notice. return ":WRONG_SIGNATURE_TYPE:"