diff --git a/auth.go b/auth.go index 998deb9b..15644482 100644 --- a/auth.go +++ b/auth.go @@ -37,6 +37,13 @@ func verifyHandshakeSignature(sigType uint8, pubkey crypto.PublicKey, hashFunc c if !ed25519.Verify(pubKey, signed, sig) { return errors.New("Ed25519 verification failure") } + case signatureMLDSA: + if hashFunc != directSigning { + return errors.New("tls: ML-DSA must use direct signing") + } + if err := verifyMLDSAHandshakeSignature(pubkey, signed, sig); err != nil { + return err + } case signaturePKCS1v15: pubKey, ok := pubkey.(*rsa.PublicKey) if !ok { @@ -105,6 +112,8 @@ func typeAndHashFromSignatureScheme(signatureAlgorithm SignatureScheme) (sigType sigType = signatureECDSA case Ed25519: sigType = signatureEd25519 + case MLDSA44, MLDSA65, MLDSA87: + sigType = signatureMLDSA default: return 0, 0, fmt.Errorf("unsupported signature algorithm: %v", signatureAlgorithm) } @@ -119,6 +128,8 @@ func typeAndHashFromSignatureScheme(signatureAlgorithm SignatureScheme) (sigType hash = crypto.SHA512 case Ed25519: hash = directSigning + case MLDSA44, MLDSA65, MLDSA87: + hash = directSigning default: return 0, 0, fmt.Errorf("unsupported signature algorithm: %v", signatureAlgorithm) } @@ -141,6 +152,9 @@ func legacyTypeAndHashFromPublicKey(pub crypto.PublicKey) (sigType uint8, hash c // complexity, so we can't even test it properly. return 0, 0, fmt.Errorf("tls: Ed25519 public keys are not supported before TLS 1.2") default: + if isMLDSAPublicKey(pub) { + return 0, 0, errors.New("tls: ML-DSA public keys are not supported before TLS 1.3") + } return 0, 0, fmt.Errorf("tls: unsupported public key: %T", pub) } } @@ -211,7 +225,10 @@ func signatureSchemesForCertificate(version uint16, cert *Certificate) []Signatu case ed25519.PublicKey: sigAlgs = []SignatureScheme{Ed25519} default: - return nil + sigAlgs = signatureSchemesForMLDSAPublicKey(version, pub) + if sigAlgs == nil { + return nil + } } if cert.SupportedSignatureAlgorithms != nil { @@ -284,6 +301,9 @@ func unsupportedCertificateError(cert *Certificate) error { return fmt.Errorf("tls: certificate RSA key size too small for supported signature algorithms") case ed25519.PublicKey: default: + if err := unsupportedMLDSACertificateError(pub); err != nil { + return err + } return fmt.Errorf("tls: unsupported certificate key (%T)", pub) } diff --git a/auth_test.go b/auth_test.go index f67a8e0f..bf3073fe 100644 --- a/auth_test.go +++ b/auth_test.go @@ -157,6 +157,42 @@ func TestLegacyTypeAndHash(t *testing.T) { } } +func TestMLDSASignatureSchemeUsesDirectSigning(t *testing.T) { + for _, sigAlg := range []SignatureScheme{MLDSA44, MLDSA65, MLDSA87} { + sigType, hash, err := typeAndHashFromSignatureScheme(sigAlg) + if err != nil { + t.Fatalf("typeAndHashFromSignatureScheme(%v) returned error: %v", sigAlg, err) + } + if sigType != signatureMLDSA { + t.Fatalf("typeAndHashFromSignatureScheme(%v) signature type = %#x, want %#x", sigAlg, sigType, signatureMLDSA) + } + if hash != directSigning { + t.Fatalf("typeAndHashFromSignatureScheme(%v) hash = %#x, want directSigning", sigAlg, hash) + } + } +} + +func TestDefaultSupportedSignatureAlgorithmsFollowGoMLDSASupport(t *testing.T) { + for _, sigAlg := range []SignatureScheme{MLDSA44, MLDSA65, MLDSA87} { + got := isSupportedSignatureAlgorithm(sigAlg, supportedSignatureAlgorithms()) + if got != goMLDSASupported() { + t.Fatalf("default support for %v = %v, want %v", sigAlg, got, goMLDSASupported()) + } + } +} + +func TestSupportedSignatureAlgorithmsForVersionGatesMLDSA(t *testing.T) { + for _, sigAlg := range []SignatureScheme{MLDSA44, MLDSA65, MLDSA87} { + if isSupportedSignatureAlgorithm(sigAlg, supportedSignatureAlgorithmsForVersion(VersionTLS12)) { + t.Fatalf("%v advertised for TLS 1.2", sigAlg) + } + gotTLS13 := isSupportedSignatureAlgorithm(sigAlg, supportedSignatureAlgorithmsForVersion(VersionTLS13)) + if gotTLS13 != goMLDSASupported() { + t.Fatalf("TLS 1.3 support for %v = %v, want %v", sigAlg, gotTLS13, goMLDSASupported()) + } + } +} + // TestSupportedSignatureAlgorithms checks that all supportedSignatureAlgorithms // have valid type and hash information. func TestSupportedSignatureAlgorithms(t *testing.T) { @@ -168,7 +204,7 @@ func TestSupportedSignatureAlgorithms(t *testing.T) { if sigType == 0 { t.Errorf("%v: missing signature type", sigAlg) } - if hash == 0 && sigAlg != Ed25519 { + if hash == 0 && sigAlg != Ed25519 && !isMLDSASignatureScheme(sigAlg) { t.Errorf("%v: missing hash", sigAlg) } } diff --git a/cache.go b/cache.go index a7677611..bb52feed 100644 --- a/cache.go +++ b/cache.go @@ -82,7 +82,7 @@ func (cc *certCache) newCert(der []byte) (*activeCert, error) { return cc.active(entry.(*cacheEntry)), nil } - cert, err := x509.ParseCertificate(der) + cert, err := parseCertificate(der) if err != nil { return nil, err } diff --git a/common.go b/common.go index 73b6dad5..c334ea53 100644 --- a/common.go +++ b/common.go @@ -206,12 +206,13 @@ const ( signatureRSAPSS signatureECDSA signatureEd25519 + signatureMLDSA signatureEdDilithium3 ) // directSigning is a standard Hash value that signals that no pre-hashing // should be performed, and that the input should be signed directly. It is the -// hash function associated with the Ed25519 signature scheme. +// hash function associated with the Ed25519 and ML-DSA signature schemes. var directSigning crypto.Hash = 0 // helloRetryRequestRandom is set as the Random value of a ServerHello @@ -414,6 +415,11 @@ const ( // EdDSA algorithms. Ed25519 SignatureScheme = 0x0807 + // ML-DSA algorithms. + MLDSA44 SignatureScheme = 0x0904 + MLDSA65 SignatureScheme = 0x0905 + MLDSA87 SignatureScheme = 0x0906 + // Legacy signature and hash algorithms for TLS 1.2. PKCS1WithSHA1 SignatureScheme = 0x0201 ECDSAWithSHA1 SignatureScheme = 0x0203 @@ -914,6 +920,10 @@ type Config struct { // autoSessionTicketKeys is like sessionTicketKeys but is owned by the // auto-rotation logic. See Config.ticketKeys. autoSessionTicketKeys []ticketKey + + // testingOnlyForceSignatureAlgorithms freezes signature_algorithms output + // for recorded wire tests whose testdata predates newer default algorithms. + testingOnlyForceSignatureAlgorithms []SignatureScheme } // EncryptedClientHelloKey holds a private key that is associated @@ -1018,6 +1028,7 @@ func (c *Config) Clone() *Config { EncryptedClientHelloKeys: c.EncryptedClientHelloKeys, sessionTicketKeys: c.sessionTicketKeys, autoSessionTicketKeys: c.autoSessionTicketKeys, + testingOnlyForceSignatureAlgorithms: c.testingOnlyForceSignatureAlgorithms, PreferSkipResumptionOnNilExtension: c.PreferSkipResumptionOnNilExtension, // [UTLS] } @@ -1532,7 +1543,7 @@ func (cri *CertificateRequestInfo) SupportsCertificate(c *Certificate) error { // chain.Leaf was nil. if j != 0 || x509Cert == nil { var err error - if x509Cert, err = x509.ParseCertificate(cert); err != nil { + if x509Cert, err = parseCertificate(cert); err != nil { return fmt.Errorf("failed to parse certificate #%d in the chain: %w", j, err) } } @@ -1602,7 +1613,8 @@ var writerMutex sync.Mutex type Certificate struct { Certificate [][]byte // PrivateKey contains the private key corresponding to the public key in - // Leaf. This must implement crypto.Signer with an RSA, ECDSA or Ed25519 PublicKey. + // Leaf. This must implement crypto.Signer with an RSA, ECDSA, Ed25519, or + // ML-DSA PublicKey. // For a server up to TLS 1.2, it can also implement crypto.Decrypter with // an RSA PublicKey. PrivateKey crypto.PrivateKey @@ -1627,7 +1639,7 @@ func (c *Certificate) leaf() (*x509.Certificate, error) { if c.Leaf != nil { return c.Leaf, nil } - return x509.ParseCertificate(c.Certificate[0]) + return parseCertificate(c.Certificate[0]) } type handshakeMessage interface { @@ -1741,6 +1753,23 @@ func supportedSignatureAlgorithms() []SignatureScheme { // [uTLS] SECTION END } +func supportedSignatureAlgorithmsForVersion(maxVersion uint16) []SignatureScheme { + sigAlgs := supportedSignatureAlgorithms() + if maxVersion >= VersionTLS13 { + return sigAlgs + } + return slices.DeleteFunc(slices.Clone(sigAlgs), isMLDSASignatureScheme) +} + +func isMLDSASignatureScheme(sigAlg SignatureScheme) bool { + switch sigAlg { + case MLDSA44, MLDSA65, MLDSA87: + return true + default: + return false + } +} + func isSupportedSignatureAlgorithm(sigAlg SignatureScheme, supportedSignatureAlgorithms []SignatureScheme) bool { for _, s := range supportedSignatureAlgorithms { if s == sigAlg { diff --git a/common_string.go b/common_string.go index e15dd488..25df3964 100644 --- a/common_string.go +++ b/common_string.go @@ -18,6 +18,9 @@ func _() { _ = x[ECDSAWithP384AndSHA384-1283] _ = x[ECDSAWithP521AndSHA512-1539] _ = x[Ed25519-2055] + _ = x[MLDSA44-2308] + _ = x[MLDSA65-2309] + _ = x[MLDSA87-2310] _ = x[PKCS1WithSHA1-513] _ = x[ECDSAWithSHA1-515] } @@ -32,10 +35,12 @@ const ( _SignatureScheme_name_6 = "PKCS1WithSHA512" _SignatureScheme_name_7 = "ECDSAWithP521AndSHA512" _SignatureScheme_name_8 = "PSSWithSHA256PSSWithSHA384PSSWithSHA512Ed25519" + _SignatureScheme_name_9 = "MLDSA44MLDSA65MLDSA87" ) var ( _SignatureScheme_index_8 = [...]uint8{0, 13, 26, 39, 46} + _SignatureScheme_index_9 = [...]uint8{0, 7, 14, 21} ) func (i SignatureScheme) String() string { @@ -59,6 +64,9 @@ func (i SignatureScheme) String() string { case 2052 <= i && i <= 2055: i -= 2052 return _SignatureScheme_name_8[_SignatureScheme_index_8[i]:_SignatureScheme_index_8[i+1]] + case 2308 <= i && i <= 2310: + i -= 2308 + return _SignatureScheme_name_9[_SignatureScheme_index_9[i]:_SignatureScheme_index_9[i+1]] default: return "SignatureScheme(" + strconv.FormatInt(int64(i), 10) + ")" } diff --git a/defaults.go b/defaults.go index f36b6c42..7b28629d 100644 --- a/defaults.go +++ b/defaults.go @@ -30,7 +30,7 @@ func defaultCurvePreferences() []CurveID { // the code advertises as supported in a TLS 1.2+ ClientHello and in a TLS 1.2+ // CertificateRequest. The two fields are merged to match with TLS 1.3. // Note that in TLS 1.2, the ECDSA algorithms are not constrained to P-256, etc. -var defaultSupportedSignatureAlgorithms = []SignatureScheme{ +var defaultSupportedSignatureAlgorithms = append(defaultMLDSASignatureAlgorithms(), PSSWithSHA256, ECDSAWithP256AndSHA256, Ed25519, @@ -43,7 +43,7 @@ var defaultSupportedSignatureAlgorithms = []SignatureScheme{ ECDSAWithP521AndSHA512, PKCS1WithSHA1, ECDSAWithSHA1, -} +) // [uTLS section begins] // var tlsrsakex = godebug.New("tlsrsakex") diff --git a/dicttls/signaturescheme.go b/dicttls/signaturescheme.go index d0b9803f..70e5e3dc 100644 --- a/dicttls/signaturescheme.go +++ b/dicttls/signaturescheme.go @@ -38,6 +38,9 @@ const ( SigScheme_ecdsa_brainpoolP256r1tls13_sha256 uint16 = 0x081A SigScheme_ecdsa_brainpoolP384r1tls13_sha384 uint16 = 0x081B SigScheme_ecdsa_brainpoolP512r1tls13_sha512 uint16 = 0x081C + SigScheme_mldsa44 uint16 = 0x0904 + SigScheme_mldsa65 uint16 = 0x0905 + SigScheme_mldsa87 uint16 = 0x0906 ) var DictSignatureSchemeValueIndexed = map[uint16]string{ @@ -75,6 +78,9 @@ var DictSignatureSchemeValueIndexed = map[uint16]string{ 0x081A: "ecdsa_brainpoolP256r1tls13_sha256", 0x081B: "ecdsa_brainpoolP384r1tls13_sha384", 0x081C: "ecdsa_brainpoolP512r1tls13_sha512", + 0x0904: "mldsa44", + 0x0905: "mldsa65", + 0x0906: "mldsa87", } var DictSignatureSchemeNameIndexed = map[string]uint16{ @@ -113,4 +119,7 @@ var DictSignatureSchemeNameIndexed = map[string]uint16{ "ecdsa_brainpoolP256r1tls13_sha256": 0x081A, "ecdsa_brainpoolP384r1tls13_sha384": 0x081B, "ecdsa_brainpoolP512r1tls13_sha512": 0x081C, + "mldsa44": 0x0904, + "mldsa65": 0x0905, + "mldsa87": 0x0906, } diff --git a/go.mod b/go.mod index 6cc2d356..6c240e03 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/refraction-networking/utls -go 1.24 +go 1.24.0 retract ( v1.4.1 // #218 @@ -9,10 +9,11 @@ retract ( require ( github.com/andybalholm/brotli v1.0.6 + github.com/cloudflare/circl v1.6.4 github.com/klauspost/compress v1.17.4 - golang.org/x/crypto v0.36.0 - golang.org/x/net v0.38.0 - golang.org/x/sys v0.31.0 + golang.org/x/crypto v0.45.0 + golang.org/x/net v0.47.0 + golang.org/x/sys v0.38.0 ) -require golang.org/x/text v0.23.0 // indirect +require golang.org/x/text v0.31.0 // indirect diff --git a/go.sum b/go.sum index 82793448..b41ae0ed 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,14 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= +github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= diff --git a/handshake_client.go b/handshake_client.go index bba58a5a..47f628ea 100644 --- a/handshake_client.go +++ b/handshake_client.go @@ -136,7 +136,10 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echCli } if maxVersion >= VersionTLS12 { - hello.supportedSignatureAlgorithms = supportedSignatureAlgorithms() + hello.supportedSignatureAlgorithms = supportedSignatureAlgorithmsForVersion(maxVersion) + } + if config.testingOnlyForceSignatureAlgorithms != nil { + hello.supportedSignatureAlgorithms = config.testingOnlyForceSignatureAlgorithms } if testingOnlyForceClientHelloSignatureAlgorithms != nil { hello.supportedSignatureAlgorithms = testingOnlyForceClientHelloSignatureAlgorithms @@ -1217,10 +1220,17 @@ func (c *Conn) verifyServerCertificate(certificates [][]byte) error { } } - switch certs[0].PublicKey.(type) { + switch pub := certs[0].PublicKey.(type) { case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: break default: + if isMLDSAPublicKey(pub) { + if c.vers < VersionTLS13 { + c.sendAlert(alertUnsupportedCertificate) + return errors.New("tls: ML-DSA certificates require TLS 1.3") + } + break + } c.sendAlert(alertUnsupportedCertificate) return fmt.Errorf("tls: server's certificate contains an unsupported type of public key: %T", certs[0].PublicKey) } diff --git a/handshake_server.go b/handshake_server.go index e38ebaeb..8619b4f6 100644 --- a/handshake_server.go +++ b/handshake_server.go @@ -641,7 +641,10 @@ func (hs *serverHandshakeState) doFullHandshake() error { } if c.vers >= VersionTLS12 { certReq.hasSignatureAlgorithm = true - certReq.supportedSignatureAlgorithms = supportedSignatureAlgorithms() + certReq.supportedSignatureAlgorithms = supportedSignatureAlgorithmsForVersion(c.vers) + if c.config.testingOnlyForceSignatureAlgorithms != nil { + certReq.supportedSignatureAlgorithms = c.config.testingOnlyForceSignatureAlgorithms + } } // An empty list of certificateAuthorities signals to @@ -910,7 +913,7 @@ func (c *Conn) processCertsFromClient(certificate Certificate) error { 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 = parseCertificate(asn1Data); err != nil { c.sendAlert(alertBadCertificate) return errors.New("tls: failed to parse client certificate: " + err.Error()) } @@ -969,9 +972,16 @@ func (c *Conn) processCertsFromClient(certificate Certificate) error { c.scts = certificate.SignedCertificateTimestamps if len(certs) > 0 { - switch certs[0].PublicKey.(type) { + switch pub := certs[0].PublicKey.(type) { case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey: default: + if isMLDSAPublicKey(pub) { + if c.vers < VersionTLS13 { + c.sendAlert(alertUnsupportedCertificate) + return errors.New("tls: ML-DSA certificates require TLS 1.3") + } + break + } c.sendAlert(alertUnsupportedCertificate) return fmt.Errorf("tls: client certificate contains an unsupported public key of type %T", certs[0].PublicKey) } diff --git a/handshake_server_tls13.go b/handshake_server_tls13.go index 510db6f6..dd160220 100644 --- a/handshake_server_tls13.go +++ b/handshake_server_tls13.go @@ -834,6 +834,9 @@ func (hs *serverHandshakeStateTLS13) sendServerCertificate() error { certReq.ocspStapling = true certReq.scts = true certReq.supportedSignatureAlgorithms = supportedSignatureAlgorithms() + if c.config.testingOnlyForceSignatureAlgorithms != nil { + certReq.supportedSignatureAlgorithms = c.config.testingOnlyForceSignatureAlgorithms + } if c.config.ClientCAs != nil { certReq.certificateAuthorities = c.config.ClientCAs.Subjects() } diff --git a/handshake_test.go b/handshake_test.go index c00ace04..d92d93c0 100644 --- a/handshake_test.go +++ b/handshake_test.go @@ -458,6 +458,9 @@ func runMain(m *testing.M) int { MinVersion: VersionTLS10, MaxVersion: VersionTLS13, } + if !*update { + testConfig.testingOnlyForceSignatureAlgorithms = recordedTestSignatureAlgorithms() + } testConfig.Certificates[0].Certificate = [][]byte{testRSACertificate} testConfig.Certificates[0].PrivateKey = testRSAPrivateKey testConfig.Certificates[1].Certificate = [][]byte{testSNICertificate} @@ -475,6 +478,17 @@ func runMain(m *testing.M) int { return m.Run() } +func recordedTestSignatureAlgorithms() []SignatureScheme { + sigAlgs := supportedSignatureAlgorithms() + filtered := make([]SignatureScheme, 0, len(sigAlgs)) + for _, sigAlg := range sigAlgs { + if !isMLDSASignatureScheme(sigAlg) { + filtered = append(filtered, sigAlg) + } + } + return filtered +} + func testHandshake(t *testing.T, clientConfig, serverConfig *Config) (serverState, clientState ConnectionState, err error) { // [uTLS SECTION BEGIN] return testUtlsHandshake(t, clientConfig, serverConfig, spec) diff --git a/mldsa.go b/mldsa.go new file mode 100644 index 00000000..a1779247 --- /dev/null +++ b/mldsa.go @@ -0,0 +1,43 @@ +package tls + +import ( + "crypto" + "errors" +) + +func isMLDSAPublicKey(pub crypto.PublicKey) bool { + _, ok := mldsaSignatureSchemeForPublicKey(pub) + return ok +} + +func signatureSchemesForMLDSAPublicKey(version uint16, pub crypto.PublicKey) []SignatureScheme { + if version != VersionTLS13 { + return nil + } + if sigAlg, ok := mldsaSignatureSchemeForPublicKey(pub); ok { + return []SignatureScheme{sigAlg} + } + return nil +} + +func unsupportedMLDSACertificateError(pub crypto.PublicKey) error { + if isMLDSAPublicKey(pub) { + return errors.New("tls: ML-DSA certificates require TLS 1.3") + } + return nil +} + +func publicKeyMatchesMLDSAPrivateKey(pub crypto.PublicKey, priv crypto.PrivateKey) bool { + if pub == nil || priv == nil { + return false + } + signer, ok := priv.(crypto.Signer) + if !ok { + return false + } + pubEq, ok := pub.(interface{ Equal(crypto.PublicKey) bool }) + if !ok { + return false + } + return pubEq.Equal(signer.Public()) +} diff --git a/mldsa_auth_test.go b/mldsa_auth_test.go new file mode 100644 index 00000000..eef9f2a2 --- /dev/null +++ b/mldsa_auth_test.go @@ -0,0 +1,104 @@ +//go:build !nomldsa + +package tls + +import ( + "crypto" + "testing" + + "github.com/cloudflare/circl/sign/mldsa/mldsa44" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" +) + +func TestLegacyTypeAndHashRejectsMLDSA(t *testing.T) { + pub, _, err := mldsa44.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + if _, _, err := legacyTypeAndHashFromPublicKey(pub); err == nil { + t.Fatal("legacy signature path accepted ML-DSA") + } +} + +func TestMLDSACIRCLVerifyHandshakeSignature(t *testing.T) { + tests := []struct { + name string + gen func() (crypto.PublicKey, crypto.Signer, error) + }{ + { + name: "mldsa44", + gen: func() (crypto.PublicKey, crypto.Signer, error) { + pub, priv, err := mldsa44.GenerateKey(nil) + return pub, priv, err + }, + }, + { + name: "mldsa65", + gen: func() (crypto.PublicKey, crypto.Signer, error) { + pub, priv, err := mldsa65.GenerateKey(nil) + return pub, priv, err + }, + }, + { + name: "mldsa87", + gen: func() (crypto.PublicKey, crypto.Signer, error) { + pub, priv, err := mldsa87.GenerateKey(nil) + return pub, priv, err + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pub, priv, err := test.gen() + if err != nil { + t.Fatal(err) + } + msg := []byte("uTLS ML-DSA CertificateVerify input") + sig, err := priv.Sign(nil, msg, crypto.Hash(0)) + if err != nil { + t.Fatal(err) + } + if err := verifyMLDSAHandshakeSignature(pub, msg, sig); err != nil { + t.Fatalf("valid signature rejected: %v", err) + } + sig[0] ^= 0x80 + if err := verifyMLDSAHandshakeSignature(pub, msg, sig); err == nil { + t.Fatal("invalid signature accepted") + } + }) + } +} + +func TestVerifyHandshakeSignatureMLDSARequiresDirectSigning(t *testing.T) { + pub, priv, err := mldsa44.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + msg := []byte("uTLS ML-DSA CertificateVerify input") + sig, err := priv.Sign(nil, msg, crypto.Hash(0)) + if err != nil { + t.Fatal(err) + } + if err := verifyHandshakeSignature(signatureMLDSA, pub, crypto.SHA256, msg, sig); err == nil { + t.Fatal("ML-DSA signature accepted with a pre-hash function") + } +} + +func TestSelectSignatureSchemeMLDSARequiresTLS13(t *testing.T) { + cert, err := X509KeyPair([]byte(testMLDSA44CertPEM), []byte(testingKeyToPrivateKeyPEM(testMLDSA44KeyPEM))) + if err != nil { + t.Fatal(err) + } + if _, err := selectSignatureScheme(VersionTLS12, &cert, []SignatureScheme{MLDSA44}); err == nil { + t.Fatal("TLS 1.2 selected ML-DSA signature scheme") + } + got, err := selectSignatureScheme(VersionTLS13, &cert, []SignatureScheme{MLDSA44}) + if err != nil { + t.Fatal(err) + } + if got != MLDSA44 { + t.Fatalf("selected %v, want %v", got, MLDSA44) + } +} diff --git a/mldsa_circl.go b/mldsa_circl.go new file mode 100644 index 00000000..9886ee2d --- /dev/null +++ b/mldsa_circl.go @@ -0,0 +1,111 @@ +//go:build !nomldsa + +package tls + +import ( + "crypto" + "crypto/x509" + "errors" + "fmt" + + circlPki "github.com/cloudflare/circl/pki" + circlSign "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/mldsa/mldsa44" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" +) + +func goMLDSASupported() bool { + return true +} + +func defaultMLDSASignatureAlgorithms() []SignatureScheme { + return []SignatureScheme{MLDSA44, MLDSA65, MLDSA87} +} + +func mldsaSignatureSchemeForPublicKey(pub crypto.PublicKey) (SignatureScheme, bool) { + switch pub.(type) { + case *mldsa44.PublicKey: + return MLDSA44, true + case *mldsa65.PublicKey: + return MLDSA65, true + case *mldsa87.PublicKey: + return MLDSA87, true + } + if pub, ok := pub.(circlSign.PublicKey); ok { + switch pub.Scheme().Name() { + case "ML-DSA-44": + return MLDSA44, true + case "ML-DSA-65": + return MLDSA65, true + case "ML-DSA-87": + return MLDSA87, true + } + } + return 0, false +} + +func isMLDSAPrivateKey(priv crypto.PrivateKey) bool { + switch priv.(type) { + case *mldsa44.PrivateKey, *mldsa65.PrivateKey, *mldsa87.PrivateKey: + return true + } + if priv, ok := priv.(circlSign.PrivateKey); ok { + _, ok := mldsaSignatureSchemeForPublicKey(priv.Public()) + return ok + } + return false +} + +func verifyMLDSAHandshakeSignature(pubkey crypto.PublicKey, signed, sig []byte) error { + pub, ok := pubkey.(circlSign.PublicKey) + if !ok { + return fmt.Errorf("tls: expected ML-DSA public key, got %T", pubkey) + } + scheme := pub.Scheme() + switch scheme.Name() { + case "ML-DSA-44", "ML-DSA-65", "ML-DSA-87": + default: + return fmt.Errorf("tls: unsupported ML-DSA public key scheme %q", scheme.Name()) + } + if !scheme.Verify(pub, signed, sig, nil) { + return errors.New("tls: invalid ML-DSA signature") + } + return nil +} + +// parseCertificate recognizes ML-DSA leaf public keys, but chain signatures +// still verify through crypto/x509, which has no ML-DSA support: only +// classically-issued ML-DSA leaf certificates pass verification. +func parseCertificate(der []byte) (*x509.Certificate, error) { + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + patchMLDSAPublicKey(cert) + return cert, nil +} + +func patchMLDSAPublicKey(cert *x509.Certificate) { + if cert == nil || cert.PublicKey != nil || len(cert.RawSubjectPublicKeyInfo) == 0 { + return + } + pub, err := circlPki.UnmarshalPKIXPublicKey(cert.RawSubjectPublicKeyInfo) + if err != nil { + return + } + if isMLDSAPublicKey(pub) { + cert.PublicKey = pub + } +} + +func parseMLDSAPrivateKey(der []byte) (crypto.PrivateKey, error) { + priv, err := circlPki.UnmarshalPKIXPrivateKey(der) + if err != nil { + return nil, err + } + if !isMLDSAPrivateKey(priv) { + return nil, errors.New("tls: parsed CIRCL key is not ML-DSA") + } + return priv, nil +} diff --git a/mldsa_handshake_test.go b/mldsa_handshake_test.go new file mode 100644 index 00000000..c03f7eeb --- /dev/null +++ b/mldsa_handshake_test.go @@ -0,0 +1,141 @@ +//go:build !nomldsa + +package tls + +import ( + "crypto/x509" + "testing" + "time" +) + +func TestHandshakeServerMLDSACertificateTLS13(t *testing.T) { + for _, test := range mldsaHandshakeTests() { + t.Run(test.name, func(t *testing.T) { + serverCert, err := X509KeyPair([]byte(test.serverCertPEM), []byte(testingKeyToPrivateKeyPEM(test.serverKeyPEM))) + if err != nil { + t.Fatal(err) + } + rootCAs := x509.NewCertPool() + if !rootCAs.AppendCertsFromPEM([]byte(testRootCertPEM)) { + t.Fatal("failed to append root cert") + } + clientConfig := &Config{ + ServerName: "test.golang.example", + RootCAs: rootCAs, + Time: mldsaTestTime, + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + } + serverConfig := &Config{ + Certificates: []Certificate{serverCert}, + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + } + client, server := localPipe(t) + defer client.Close() + defer server.Close() + + errChan := make(chan error, 1) + go func() { + errChan <- Server(server, serverConfig).Handshake() + }() + clientConn := Client(client, clientConfig) + if err := clientConn.Handshake(); err != nil { + t.Fatal(err) + } + if err := <-errChan; err != nil { + t.Fatal(err) + } + if got := clientConn.ConnectionState().Version; got != VersionTLS13 { + t.Fatalf("version = %#x, want TLS 1.3", got) + } + expectMLDSAPublicKey(t, clientConn.ConnectionState().PeerCertificates[0].PublicKey, test.sigAlg) + }) + } +} + +func TestHandshakeClientMLDSACertificateTLS13(t *testing.T) { + for _, test := range mldsaHandshakeTests() { + t.Run(test.name, func(t *testing.T) { + serverCert, err := X509KeyPair([]byte(test.serverCertPEM), []byte(testingKeyToPrivateKeyPEM(test.serverKeyPEM))) + if err != nil { + t.Fatal(err) + } + clientCert, err := X509KeyPair([]byte(test.clientCertPEM), []byte(testingKeyToPrivateKeyPEM(test.clientKeyPEM))) + if err != nil { + t.Fatal(err) + } + rootCAs := x509.NewCertPool() + if !rootCAs.AppendCertsFromPEM([]byte(testRootCertPEM)) { + t.Fatal("failed to append root cert") + } + clientCAs := x509.NewCertPool() + if !clientCAs.AppendCertsFromPEM([]byte(testClientRootCertPEM)) { + t.Fatal("failed to append client root cert") + } + clientConfig := &Config{ + ServerName: "test.golang.example", + RootCAs: rootCAs, + Certificates: []Certificate{clientCert}, + Time: mldsaTestTime, + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + } + serverConfig := &Config{ + Certificates: []Certificate{serverCert}, + ClientAuth: RequireAndVerifyClientCert, + ClientCAs: clientCAs, + Time: mldsaTestTime, + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + } + client, server := localPipe(t) + defer client.Close() + defer server.Close() + + serverConn := Server(server, serverConfig) + errChan := make(chan error, 1) + go func() { + errChan <- serverConn.Handshake() + }() + clientConn := Client(client, clientConfig) + if err := clientConn.Handshake(); err != nil { + t.Fatal(err) + } + if err := <-errChan; err != nil { + t.Fatal(err) + } + state := serverConn.ConnectionState() + if len(state.PeerCertificates) == 0 { + t.Fatal("server did not receive peer certificate") + } + expectMLDSAPublicKey(t, state.PeerCertificates[0].PublicKey, test.sigAlg) + }) + } +} + +func mldsaHandshakeTests() []struct { + name string + sigAlg SignatureScheme + serverCertPEM string + serverKeyPEM string + clientCertPEM string + clientKeyPEM string +} { + return []struct { + name string + sigAlg SignatureScheme + serverCertPEM string + serverKeyPEM string + clientCertPEM string + clientKeyPEM string + }{ + {"MLDSA44", MLDSA44, testMLDSA44CertPEM, testMLDSA44KeyPEM, testClientMLDSA44CertPEM, testClientMLDSA44KeyPEM}, + {"MLDSA65", MLDSA65, testMLDSA65CertPEM, testMLDSA65KeyPEM, testClientMLDSA65CertPEM, testClientMLDSA65KeyPEM}, + {"MLDSA87", MLDSA87, testMLDSA87CertPEM, testMLDSA87KeyPEM, testClientMLDSA87CertPEM, testClientMLDSA87KeyPEM}, + } +} + +func mldsaTestTime() time.Time { + return time.Date(2017, time.January, 1, 0, 0, 0, 0, time.UTC) +} diff --git a/mldsa_off.go b/mldsa_off.go new file mode 100644 index 00000000..6debccdc --- /dev/null +++ b/mldsa_off.go @@ -0,0 +1,37 @@ +//go:build nomldsa + +package tls + +import ( + "crypto" + "crypto/x509" + "errors" +) + +func goMLDSASupported() bool { + return false +} + +func defaultMLDSASignatureAlgorithms() []SignatureScheme { + return nil +} + +func mldsaSignatureSchemeForPublicKey(pub crypto.PublicKey) (SignatureScheme, bool) { + return 0, false +} + +func isMLDSAPrivateKey(priv crypto.PrivateKey) bool { + return false +} + +func verifyMLDSAHandshakeSignature(pubkey crypto.PublicKey, signed, sig []byte) error { + return errors.New("tls: ML-DSA support is disabled by the nomldsa build tag") +} + +func parseCertificate(der []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(der) +} + +func parseMLDSAPrivateKey(der []byte) (crypto.PrivateKey, error) { + return nil, errors.New("tls: ML-DSA support is disabled by the nomldsa build tag") +} diff --git a/mldsa_openssl_interop_test.go b/mldsa_openssl_interop_test.go new file mode 100644 index 00000000..cdd0a880 --- /dev/null +++ b/mldsa_openssl_interop_test.go @@ -0,0 +1,306 @@ +// Copyright 2026 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. + +//go:build opensslinterop && !nomldsa + +package tls + +import ( + "bytes" + "crypto/x509" + "errors" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +const opensslInteropImage = "alpine:edge" + +func TestOpenSSLMLDSAInterop(t *testing.T) { + requireDocker(t) + + workDir := t.TempDir() + generateOpenSSLMLDSACerts(t, workDir) + + t.Run("OpenSSL client verifies uTLS ML-DSA server", func(t *testing.T) { + certPEM, err := os.ReadFile(filepath.Join(workDir, "server.crt")) + if err != nil { + t.Fatal(err) + } + keyPEM, err := os.ReadFile(filepath.Join(workDir, "server.key")) + if err != nil { + t.Fatal(err) + } + cert, err := X509KeyPair(certPEM, keyPEM) + if err != nil { + t.Fatal(err) + } + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + + serverErr := make(chan error, 1) + go func() { + conn, err := ln.Accept() + if err != nil { + serverErr <- err + return + } + defer conn.Close() + serverErr <- Server(conn, &Config{ + Certificates: []Certificate{cert}, + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + }).Handshake() + }() + + hostPort := ln.Addr().(*net.TCPAddr).Port + runDocker(t, workDir, "sh", "-lc", fmt.Sprintf( + "apk add --no-cache openssl >/dev/null && printf 'Q\\n' | openssl s_client -connect host.docker.internal:%d -servername localhost -verify_hostname localhost -tls1_3 -groups X25519 -sigalgs mldsa44 -CAfile /w/ca.crt -verify_return_error -brief", + hostPort, + )) + if err := <-serverErr; err != nil { + t.Fatal(err) + } + }) + + t.Run("uTLS client and OpenSSL ML-DSA server", func(t *testing.T) { + hostAddr := startOpenSSLMLDSAServer(t, workDir, "server.crt", "server.key") + roots := loadInteropCertPool(t, workDir, "ca.crt") + + clientConfig := &Config{ + ServerName: "localhost", + RootCAs: roots, + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + } + // The container installs OpenSSL before s_server starts listening, and + // Docker Desktop's proxy accepts connections before the backend is up, + // so retry the full handshake instead of probing the TCP port. + client := dialUTLSWithRetry(t, hostAddr, clientConfig, HelloChrome_150) + if got := client.ConnectionState().Version; got != VersionTLS13 { + client.Close() + t.Fatalf("version = %#x, want TLS 1.3", got) + } + if len(client.ConnectionState().PeerCertificates) == 0 { + client.Close() + t.Fatal("OpenSSL server did not send a certificate") + } + expectMLDSAPublicKey(t, client.ConnectionState().PeerCertificates[0].PublicKey, MLDSA44) + // s_server handles one connection at a time: close before the + // single-shot handshakes below or they never see a ServerHello. + client.Close() + + // The server is known to be up from here on, so failures below are + // meaningful single-shot handshakes, not readiness races. + t.Run("rejects untrusted CA", func(t *testing.T) { + err := utlsHandshakeErr(t, hostAddr, &Config{ + ServerName: "localhost", + RootCAs: loadInteropCertPool(t, workDir, "wrongca.crt"), + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + }, HelloChrome_150) + var unknownAuthority x509.UnknownAuthorityError + if !errors.As(err, &unknownAuthority) { + t.Fatalf("handshake error = %v, want x509.UnknownAuthorityError", err) + } + }) + + t.Run("classical-only ClientHello cannot negotiate", func(t *testing.T) { + err := utlsHandshakeErr(t, hostAddr, &Config{ + ServerName: "localhost", + RootCAs: roots, + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + }, HelloChrome_133) + if err == nil { + t.Fatal("handshake succeeded without advertising ML-DSA to an ML-DSA-only server") + } + t.Logf("handshake failed as expected: %v", err) + }) + }) + + t.Run("uTLS client and pure ML-DSA chain", func(t *testing.T) { + hostAddr := startOpenSSLMLDSAServer(t, workDir, "pqserver.crt", "pqserver.key") + + // TLS-level ML-DSA (CertificateVerify, leaf public key) works even for + // an ML-DSA-issued chain, so skipping x509 verification must succeed. + client := dialUTLSWithRetry(t, hostAddr, &Config{ + ServerName: "localhost", + InsecureSkipVerify: true, + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + }, HelloChrome_150) + expectMLDSAPublicKey(t, client.ConnectionState().PeerCertificates[0].PublicKey, MLDSA44) + client.Close() + + // crypto/x509 cannot verify ML-DSA chain signatures: trusting the + // ML-DSA root must still fail verification. If this starts passing, + // the classical-issuer-only restriction can be lifted. + err := utlsHandshakeErr(t, hostAddr, &Config{ + ServerName: "localhost", + RootCAs: loadInteropCertPool(t, workDir, "mldsa-ca.crt"), + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + }, HelloChrome_150) + var unknownAuthority x509.UnknownAuthorityError + if !errors.As(err, &unknownAuthority) { + t.Fatalf("handshake error = %v, want x509.UnknownAuthorityError", err) + } + t.Logf("pure ML-DSA chain rejected as expected: %v", err) + }) +} + +func startOpenSSLMLDSAServer(t *testing.T, workDir, certFile, keyFile string) string { + t.Helper() + + containerID := runDockerOutput(t, workDir, "docker", "run", "-d", "--rm", + "-v", dockerVolume(workDir)+":/w", + "-p", "127.0.0.1::4433", + opensslInteropImage, + "sh", "-lc", + fmt.Sprintf("apk add --no-cache openssl >/dev/null && openssl s_server -accept 4433 -cert /w/%s -key /w/%s -tls1_3 -groups X25519 -sigalgs mldsa44 -www -quiet", certFile, keyFile), + ) + containerID = strings.TrimSpace(containerID) + t.Cleanup(func() { + _ = exec.Command("docker", "rm", "-f", containerID).Run() + }) + return dockerPublishedAddress(t, containerID) +} + +func loadInteropCertPool(t *testing.T, workDir, certFile string) *x509.CertPool { + t.Helper() + + pem, err := os.ReadFile(filepath.Join(workDir, certFile)) + if err != nil { + t.Fatal(err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pem) { + t.Fatalf("failed to parse %s", certFile) + } + return pool +} + +func utlsHandshakeErr(t *testing.T, addr string, config *Config, helloID ClientHelloID) error { + t.Helper() + + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(15 * time.Second)) + return UClient(conn, config, helloID).Handshake() +} + +func requireDocker(t *testing.T) { + t.Helper() + + cmd := exec.Command("docker", "version", "--format", "{{.Server.Version}}") + if out, err := cmd.CombinedOutput(); err != nil { + t.Skipf("Docker is required for OpenSSL interop tests: %v\n%s", err, out) + } +} + +func generateOpenSSLMLDSACerts(t *testing.T, workDir string) { + t.Helper() + + runDocker(t, workDir, "sh", "-lc", strings.Join([]string{ + "apk add --no-cache openssl >/dev/null", + "cd /w", + "openssl version", + "openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ca.key", + "openssl req -x509 -new -key ca.key -sha256 -days 30 -subj '/CN=utls interop test root' -out ca.crt", + "openssl genpkey -algorithm ML-DSA-44 -out server.key", + "openssl req -new -key server.key -subj '/CN=localhost' -addext 'subjectAltName=DNS:localhost' -out server.csr", + "openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 30 -copy_extensions copy -out server.crt", + "openssl x509 -in server.crt -noout -text | grep -E 'Public Key Algorithm|Signature Algorithm'", + "openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out wrongca.key", + "openssl req -x509 -new -key wrongca.key -sha256 -days 30 -subj '/CN=utls interop wrong root' -out wrongca.crt", + "openssl genpkey -algorithm ML-DSA-44 -out mldsa-ca.key", + "openssl req -x509 -new -key mldsa-ca.key -days 30 -subj '/CN=utls interop ML-DSA root' -out mldsa-ca.crt", + "openssl genpkey -algorithm ML-DSA-44 -out pqserver.key", + "openssl req -new -key pqserver.key -subj '/CN=localhost' -addext 'subjectAltName=DNS:localhost' -out pqserver.csr", + "openssl x509 -req -in pqserver.csr -CA mldsa-ca.crt -CAkey mldsa-ca.key -CAcreateserial -days 30 -copy_extensions copy -out pqserver.crt", + "openssl x509 -in pqserver.crt -noout -text | grep -E 'Public Key Algorithm|Signature Algorithm'", + }, " && ")) +} + +func runDocker(t *testing.T, workDir string, args ...string) { + t.Helper() + + baseArgs := []string{"run", "--rm", "--add-host=host.docker.internal:host-gateway", "-v", dockerVolume(workDir) + ":/w", opensslInteropImage} + out := runDockerOutput(t, workDir, "docker", append(baseArgs, args...)...) + t.Log(out) +} + +func runDockerOutput(t *testing.T, workDir, name string, args ...string) string { + t.Helper() + + cmd := exec.Command(name, args...) + cmd.Dir = workDir + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + if err := cmd.Run(); err != nil { + t.Fatalf("%s %s failed: %v\n%s", name, strings.Join(args, " "), err, out.String()) + } + return out.String() +} + +func dockerVolume(path string) string { + return filepath.ToSlash(path) +} + +func dockerPublishedAddress(t *testing.T, containerID string) string { + t.Helper() + + out := runDockerOutput(t, "", "docker", "port", containerID, "4433/tcp") + line := strings.TrimSpace(out) + if line == "" { + t.Fatalf("docker port returned no address for %s", containerID) + } + if strings.HasPrefix(line, "0.0.0.0:") { + line = "127.0.0.1:" + strings.TrimPrefix(line, "0.0.0.0:") + } + if strings.HasPrefix(line, "[::]:") { + line = "127.0.0.1:" + strings.TrimPrefix(line, "[::]:") + } + return line +} + +func dialUTLSWithRetry(t *testing.T, addr string, config *Config, helloID ClientHelloID) *UConn { + t.Helper() + + deadline := time.Now().Add(30 * time.Second) + var lastErr error + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", addr, time.Second) + if err != nil { + lastErr = err + time.Sleep(250 * time.Millisecond) + continue + } + conn.SetDeadline(time.Now().Add(10 * time.Second)) + client := UClient(conn, config, helloID) + if err := client.Handshake(); err != nil { + conn.Close() + lastErr = err + time.Sleep(250 * time.Millisecond) + continue + } + conn.SetDeadline(time.Time{}) + return client + } + t.Fatalf("timed out handshaking with %s: %v", addr, lastErr) + return nil +} diff --git a/mldsa_test_fixtures_test.go b/mldsa_test_fixtures_test.go new file mode 100644 index 00000000..3f657bb6 --- /dev/null +++ b/mldsa_test_fixtures_test.go @@ -0,0 +1,421 @@ +//go:build !nomldsa + +package tls + +// Static fixtures copied from Go upstream's ML-DSA TLS work +// (certificates_test.go at go.googlesource.com/go commit c74ba7d26551): +// ML-DSA leaf certificates issued by the classical RSA test roots below, +// valid at mldsaTestTime. + +const testRootCertPEM = ` +-----BEGIN CERTIFICATE----- +MIIC2zCCAcOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDEwRSb290 +MB4XDTE2MDcxMjE3MzIwOVoXDTE3MDEyODE3MzIwOVowDzENMAsGA1UEAxMEUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMmvg91HhTlfaXcjKwqY +G35v928fjA9m4NOeJsH+stqIDoQJeCwimlKwZ0uSmNgABZHhVl4qhcavP0mXBVhG +wOAyKkZNYX5CP2Thhw8d0OFJpeP9kxDSFHev0Fa7XUGlBWQk7Zbmad72Agx+Oe6f +RUSVt2pmpAIEzRRtQJpqO6cwtXvHH1KA2XKLBK2zLup7V32N3fQC21WjX9dZhOa+ +Y1fqyF10V+ugHbcLQJxbso0hvnhLMLW3DNrZdoL6NaXUVG7UeweIbZ9z3oITOEVk +iEBKGbtvGd58UnFfwnecO30utOp+kaJoYLMme6w/elQ0WV+G+AysJGKFCAWRb13q +4HcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMghBhPWIX5rQ3No+FB3KfrcDBEaMA0GCSqGSIb3DQEBCwUAA4IBAQAs +aHAaPhMaHULTTn62b1/dpgxLLge+iVNyCaJmi4ic/XnkSZV3ZsZp8dvoQZPR2vmU +BQLr3nqEMqpVvEnsPhAYJoEAYEUNkSMdiT2rKp3eLF/pt03qSfrPjyg+LTnyRMYU +zzocbEqDHPH0NvBdQdWBCGqMIYulR02TEKKEqamuYMiFFQq2VaFS06rTIa4jOOwU +VRBVcjqzTs3W+TKXEgT7VuGQmW6OndRHG5iQdnlpv2tFBSiNT65IwzxLfLXr7POY +Js7Ja+lM2GRWzWSp5RZdCFs/6N4HzF911PrOzuTd6mE/jaGERCKyC+SEcUHo4PFH +US9Fl9vXoWO16TbQsw3D +-----END CERTIFICATE-----` + +const testClientRootCertPEM = ` +-----BEGIN CERTIFICATE----- +MIIC6TCCAdGgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtDbGll +bnQgUm9vdDAeFw0xNjA3MTIxNzMyMDlaFw0xNzAxMjgxNzMyMDlaMBYxFDASBgNV +BAMTC0NsaWVudCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +rjkvgogt5oCTTJq07XGumWrLAGsr/9c8ZuS1HNMCflb4D8CsqFSUUHWVh7C5T0AW +K/O3B9VDCiw/NT5VWqLLvcFpOwFQz9nRHGPZYfk/KRQiQYwio8Kd48CL0wgSh5pM +Y0zvhLsNxv3ezJqWNlRz8SDkVA551A83s8DqTt3YFmAA0odQMp+EH7e6AolbO75A +TtUa6G4A2ywA+vSoQ+g6ccqIbTmRCKKvrTlAXOZ5Oe1vk1FyBIK0rpo7vimfUU11 +sbZ0gGbbPFfybDSp0BIUUNNSTZbfsENz51aJwR2AmszhkP5b5M7aljdVxCAylkjM +G8ICiwcaZVcCxk3Me1/syQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAgQwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUEXNuRxhdpiEaG0xhJjnI+17pdCQwDQYJKoZI +hvcNAQELBQADggEBAHJyN8rYdY9dRT5oUfkci3SYn/q+uiguNotK8NH4qmM83Tdm +Ekpk1iM8K4JXLhK4AxnhDEAz4jhiSJjbl8RvEODRlc4/03bHa/JQJJjBIR27oQLl +OW+GTZNL0wyM5D8rqHPDr3PAfTEDDhCjfp+VQRaNs0uTLrJD3QAoSCSxfLptk0uW +ZcL7u715Ns1IbacEue3ZWkqI1pwrpMHaGigV6qsMWKJvbVqu7I+XdtW6/EK1drEv +3wk1sOmqK5oLy8sulgJ+oUHxoSsBPIRWYcdkqQZeyRf85iX1vIWqHm6eoMfYijzH +RtP3e2dpiCGsatBcFp06TQXe1TUsw0xULmKESkk= +-----END CERTIFICATE-----` + +const testMLDSA44CertPEM = ` +-----BEGIN CERTIFICATE----- +MIIHJDCCBgygAwIBAgIBEDANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDEwRSb290 +MB4XDTE2MDcxMjE3MzIwOVoXDTE3MDEyODE3MzIwOVowFDESMBAGA1UEAxMJTUwt +RFNBLTQ0MIIFMjALBglghkgBZQMEAxEDggUhAOxsnw0OV5dTjCEvArN6q+fd0CK+ +E0ULg8zQfNTz6+qWi6bpTbVR1J9A8829lKB7fXDXMl30wdNQbEePD06SRRtMjCrm +WoHokg8uFJlBFfvN4JCu+mAaocZSXcxl9Lvm8wYYM6svDZXe6Z1Jf2Qm6kvbN9Xf +U4yUMTjG0r1ppFP8ZX96I8DCMIjWdFZKvoc/LBpx5yo0nYv0J54zG40RoHNcMAXz +g9DDv9GICKINQLNmw7fTyeUKJ+LMd555oOo0v/zQjETJ7r8MXIgUPCJUpF6mRM/T +rTbBsVcXMaQX6Ir7QHd+nVafE0Xuw7QScV4U3GnC98v7KGOBMUMaMnf4WSXiVsH5 +zAMITyY7PTGzTqpNybetCLNbByrT2OjA3M6hVXqlqV1PP1tl29KYvKacmvGV5yfw +sIGfirY9IHgzEOR5Xg3WJU9fZfYhN3JF6Kxq+3H11V1hZOVd1U6w6p2kG+7DLGj8 +2ER3fO/Nj1s0hkl4R3slha+puTx6Fdmsj9QmEKxHJYyasBjT3pYyXmGaLgJJ6lKF +Fg8jq/VqZMJF0q2fg+lGeCN4RnV4M5Yp+7xxDWNa5lzwyyjoGAz2+2+a/Bmye0mo +segRLEVudCSl1FiWmrxdh/zgqc8JGPfAsT6KvDjW5c/VZZXFqTL3A5YkmcUmnSTJ +wqJEiSh0pxlaxFCfn5JYir8V2ddlBsoUMgvlX5hnp8XG2x1DDgLt1dqWcjywY+Ad +0+ik0N/LpqgCzV2TBo4s3/hjsUGY1JxfcO27jXI1dT/LkvvheihyW/N2jwJKwzpn +25vpufqEeaiQ/GmNBzKtMeow9/F37eHSU1q8WMT2s5XyIneNFNmt3PVJ1AGeE8/e +pE46tW2b6xHNeUQXelVPWDo3RRKy2a3HTaYKVpja+nzsXj39iccTqx30TCJOJkrr +NP61acLT//g96c5UrCqqFD8wwVXymi40pnlCY1g6iw3FSL8SQTgqySIEEwyVbMOG +wM7VSaEyIpfuHAIefxh8WWIDKOINrqfidfRUeSxZ892CrxFwDk6vL2qF4N+UuL2W +ZdIjXDQd1aGZM2A7mT3RJrFKTOxQLq2QLPR3oHJ/ELR08ODco2bJ/uqtYDzozz2G +mtiRjEBhFRoP/mxAYmliX/UWqoK7CRZGt3CTK0KjidA37ISey2Q/lsMTIJEtP+Ei +kRrHTSndSjAMsIKH9sh165Wzrix4SdgttwDyrhUUA+0nxksVf1RT03ZMgtrvkH8q +adojCcYaJAu1XfrpwQXADQzM0QPpabUUnWDP1JeMDSoOdZ2VBTUopqr4dxMJP5rX +YTEsLwrf4oGNllJTUrDD4oX8w/c4wOUpVh12sb7V/zr8tgNYJ/XZxdQjR6R+DTgR +eSt6mtTfY3xf0W0QIzLi2m3AkdeB1iIZhhAdDc4VuNuJnAZirncZMI2tov2kiCCM +IPYAP3m6mQCHGz93i9AUQJsuY0DWLI8mzlUYwUUi+k+45QjpH9wETVL66Upmy4V4 +hojiR+SxcjmUh9GSPVUPzI/qRyqWCYHtR/UYTbF8wkb6VtNkbV/sqINI3le9aLpR +4rwJHwV83lqmuWNh89pXRin0emqE/b5MIPp6IDIrS+GdWxLTB2opmQZtPG8MWWvN +U9KvDsojQ3n6yCZW+NPFcbYlbDisgQ4hoouzW3fblFLgV37A96F3B1gadgM+dW/b +HPGuzKclFmSNynowLymRoYrVyakPxdId6j0BDdSU0TCZi35ReW9lvVhN6GSjdjB0 +MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8E +AjAAMB8GA1UdIwQYMBaAFMghBhPWIX5rQ3No+FB3KfrcDBEaMB4GA1UdEQQXMBWC +E3Rlc3QuZ29sYW5nLmV4YW1wbGUwDQYJKoZIhvcNAQELBQADggEBAFQNTSizfnQy +ZRhJEh8r21Lw3T/LfaY/meuDpdaL+PR0/GAbdbS2sIQHBtib2HjY3B7pg31ZlNCT +rnPQmIj2F1EyGU0LoDjlN3UUqr2VZzJQ9blIQFx0DOz7UsW7nXAXSYfhGNdxT+nz +VGb0gNbVsmVh7vfDsq+1MqX1W+/HaeQba4/GFsGGxeJRjM5Hd2WCU01fs7ZwsodR +iHZujj/WcLnmHTTDhMLYOvZoWrf5VCNsSoV4UeQzV0bVa7xbZ3OeRQA6dDtqRMdl +Z4YMjiX/Q+DqMyRU0mebQkZJP2OxcasdT6EIyDPXBSs2/ZXqLwXKcuprPUHEcIvC +5oYWLUWYOf4= +-----END CERTIFICATE-----` + +const testMLDSA44KeyPEM = ` +-----BEGIN TESTING KEY----- +MDQCAQAwCwYJYIZIAWUDBAMRBCKAIKjca7do6h6TzowpEdGGuG0qv0TIYdgnqV2S +Mcvu+n6A +-----END TESTING KEY-----` + +const testMLDSA65CertPEM = ` +-----BEGIN CERTIFICATE----- +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 +3xQD501vTZeBE/2Z+9u5jR78I8z9oijS +-----END CERTIFICATE-----` + +const testMLDSA65KeyPEM = ` +-----BEGIN TESTING KEY----- +MDQCAQAwCwYJYIZIAWUDBAMSBCKAIEEEYHBN/doadMzAAyaqjdxoQDKDiGb+9tKn +xCTHrLlF +-----END TESTING KEY-----` + +const testMLDSA87CertPEM = ` +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE-----` + +const testMLDSA87KeyPEM = ` +-----BEGIN TESTING KEY----- +MDQCAQAwCwYJYIZIAWUDBAMTBCKAIF21EV0gObGfCvdWN+cM+pRH0lPg1qU2VLip +r0TYx5dY +-----END TESTING KEY-----` + +const testClientMLDSA44CertPEM = ` +-----BEGIN CERTIFICATE----- +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 +TKUtVXxH5czj31ZMuAYfycdf/sdfvprjxcI= +-----END CERTIFICATE-----` + +const testClientMLDSA44KeyPEM = ` +-----BEGIN TESTING KEY----- +MDQCAQAwCwYJYIZIAWUDBAMRBCKAIHdJoEDI6UeYdZ2U21l0ep3akAB7qA+mLAWX +cufwhk2g +-----END TESTING KEY-----` + +const testClientMLDSA65CertPEM = ` +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE-----` + +const testClientMLDSA65KeyPEM = ` +-----BEGIN TESTING KEY----- +MDQCAQAwCwYJYIZIAWUDBAMSBCKAIOtV+uJia3QCimf1E6NpjLFdNFvp5eQCjjJO +JA04aNQw +-----END TESTING KEY-----` + +const testClientMLDSA87CertPEM = ` +-----BEGIN CERTIFICATE----- +MIIMNjCCCx6gAwIBAgIBFTANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtDbGll +bnQgUm9vdDAeFw0xNjA3MTIxNzMyMDlaFw0xNzAxMjgxNzMyMDlaMB8xHTAbBgNV +BAMTFGNsaWVudEF1dGggTUwtRFNBLTg3MIIKMjALBglghkgBZQMEAxMDggohAMF/ +yPOM8ZsfB87w9rHpB1srIiI8OwdUTmazkUBDNpj3BLzbfGpJRZ0DdMvMdVTrRJrA +xA1uUyUVghSnNizLhIxhGUUFwkv83o7vapI49JqGSvVtqttmVTphJcmlJKfQWbLO +wKaCc0glvYwZeRLfyYl/n0McWmLmFW1DC01f8lmZWRcMDtW7oX01S9q2HJsOwcWG +VKvaGVGZvZud8TXnoVQBg3wPIOynCTCRzNEDMM/WjftSD5FSipp5/ySMlLe1w1Ja +e04WZY3RhLbKisXBJmOipHUtUJCBXbptTtXkP66AfWRZ9/CCO/kZ+aEzl0nNFsVm +xBk1jRK+yFYxcf0vQq0copVfJyZl5r16CWDacm0HF2fWGWnlA/r3SYVfT+tG2ttX +dibmZSTUdiOTJ+OrsN55cwTjFPLCfvdXHpwEcqEwHfrBKpM4G7dJ6cmMabDQIg4z +g/KC8MBLcDrD8GyXdSR8XTya2ih/aU5DxBTzQl7nbm3Zd6yFgtyGlp2DEDcsoyH3 +QNUN39EcWXl6LLwrNxH3TK/dzAmIMTr5LV/s0lj9cqagx6LDQ5J9pwIdFtnXMWln +9XrRf+a91SMsTCy/mM8jQ2Fn3lurNH0WGYKJ9XUmTY8YGpdVj6gNsvwnmrSZdyHN +R9Jb7/DbRO/QxozJWwzLkMb5ERLRWZnFfZF/mdWcLp9x1Et/3FhzuPC+eJagYH8E +sEdA8ljyFn7J8DU3V0pcQonn8btgJLT7n2X7d/KDkDNE1wOWTYRHkCek8mAJ3VLT +mAWc79boZjeWMyToAsai8jTHUluz07RrAWG8r2wgI37nZQLn5O2RIqa3QFGqYZNB +1RJIvOkTZ3gG/a7pMZn1+Z3h6RH0MoAnqqwlVdGJcCa3NsSUaYlZUibfb6l2s/97 +DBXK9W5usL4DKocVgaiKw4kUJAto6JZqFj3i31cPVnWJmbSM93ecsS7/Tx7O71IG +5T7qyBXht8NzQjvhxfhaU6ZOD6uNQKIHxG/MrCaNOSongqYZBnM/Wsx1YsGIO/yz +DRgj6p0CxkEvO/ZqMxbhfSGNDBRIlDbhSbCNO3GNe/Pv95aPxr9yXeiLe9ETvI3V +x8/pwfFSWT862QNOL8hPZaabdUdVA+Ea3KGbIKxfNHzLIzg95HQIPJKd1kqeQHJK +r6gtcpP6uQakemF9LWsQomPQsMYKgPcBzlEpKP1FbiKgsR0Rrcu7MOxbVg7pPcnF +ObU0a1fGjQR3MjNFJr2OR0gSIkiuBLwEs8lEaFdlBuTcM58NUN8eHhzynNT2/3kt +YaFIDVCHvgSvNLXZ03TY07GThNNIeWe2K3nE6Sw0A96otj7WkSg2PKGYOin9hT32 +Jap9LK9swuqhcH/JuvQFA/4xvdNI2mJ0IrunU7ezkGh1BkH3FPsXYEWth7XseCYX +kVVKGzQ6MypI8lUU1sCc9ioYyaS6dtFIZ86HkC0PWlzFeAJe6S4V9MrCvaCbXOVN +9bt3k5TpCPNH/L6djIL3jgAxLrtWzn+7UihfgRocZ1NSq4gUYn7HC6wHPcuEiZv8 +xEXXG5frv8Bw0Xhad8GPL4V8Tmsr3DuMZwKiN4pw7len29KpAEsABQ7ayMN2ixvG +sp8t0O3oaJ6QWeWiXZp63LLKzfB8fbEUR4SllEttsGMieBVQWnFdhtTliOzIXRLm +Z/0Zh1lLF3sGnJyhddqlSGdOGDXkLbr8N2RPu8RPxu/wbFF8JSXKdLdHdnwwtf3u +BV6psV03JNhDjr+Le2diGQhHWFPLmR+YglKiJIdWVMy2vs0Qy5uMVuEF1mXQETsd +A8MvlQv9jUkXp8+U5e0WIlb7LxEjvy0Lt31i1jkUBT1elpHX5sjjh7si/5Myyy/V +rVZzh3htMAkpjuc0Xfcd/DHDxXI4QRTJxfI82PfU6hvjPbCdlTVYvitmQPLAqdp7 +onkNkgIVbEjZ+zRTS0nMkEX52xjUjuZX23mUcLyLe+v87AlQMEnXZg4iU9lHTOPh +iDmHOnrpYxquwWny6g0jGyJ+wBTY9WgyKy0n/zak6iHldwj3ds3dpjbaNo+Kmb6z +ujD3heqJ24goHAitpMGdpRqYvhy7kM5fZFqVEP5/kjX4qlNqzbAdiNjg9Jcs0C5G +dQExJ8U2R9aUgykyJu0EhSFavd8rROGBHwE0fIr8NDJ0i7RzNeJQxHwXzR1sKW6A +qdQfSrE41Yd8hvEIx558hxHKg6Lybc2krfQGFEhbw7zy+mkLnUeNvMD6oVj2n1ue +5uhx32piNkAIMFEv7t3dCGPiVvDJTQFslezylXT+bWZLDT+Q3tGcU6ISPxem3XAf +1aT7gEzrOnugdSuc6kgP5saMxdwPb6hIcKAs8JAFxd8bAVW3vBYL/wM+RYNnO449 +BMN5MHUdP7y/JgTNWMlpK4WXFZ4+zHc0ONF6bjW0O9X0IjIldFBfqk/Esbo0i2z0 +FLnTfBBjuZ/QZ+I7kgFusoSwTqlJSUWxXkTUNWNURJZz7fS4aG0Nw/hBvkwSirlN +QfZC9PuR+Sb6Hto2znb/1/lpuVm0s/zPdRGOTeZEML+a3tmFBd2mUUTvZOUv8PTV +aQ+fXr4Xf3efxxvGay4NNdtegVGblWY0i9CR3XX+p3OPgnJhXRv7wP/s/OecYuhH +naQr+okuaGYvQ8IhuB0R10NpX3PXjuMNDzilSHDYfEFd0QsFZpOnEpGdNhc92mjQ +CJrUtZIjYYfLyLsdxCZ+vn/Q9jwXUs3AYRCzD1+srhjMNEpguWyJAbrtstNLZ/Rh +D6zawImYn2SexEPiHNYYjABzhaj/GktuBX3kc5AS4sZWLJRuVF4huLcrqrH43n/W +8QXVPScSi2VoL9hDPj5qDH7o6Z6sHPOUvPBqrxQS3rC/n1NuHHMM9o7hOqDITxju +fLjyf1KW1u4qWinrNCPE/RjCS8x0jJZcy2dcAJhuciINxVo2jJ0jkbXiKSnh8AHF +OtllmUxHn70ojgzlkCrp69DERMoENd4yYYMHCfNdp+5EJ9yFIVBk2Yuaiw7r4BH8 +iOdsvk6YTBI2uFEKEqkYi/O4vqTyEXWkKKLlsLcL0Z1MVO1+tcHFD1VH0M+AED/b +kRkrThM7W3DA2kiSqdCkJsV6VoTf8TPwykHdXPdyASbOAlbWCQMizHShTdOYkRtb +LbaaSAepumRi0Gr/pOhj7YVDX2eBfkinn2Alof2FGZYQ1YIm7vEA55NizLc8/zaB +QqPcw8qJw+5Dxu8dpu53vmT5868PXw0qsiF3TQLFUbAmtebTOS4rcmYEkpUXq3Kc +jSNlNgYdcPS4qZZNPwCSFnK9xa3HJY1B/nQdis24zz+haicIA7EH2QP3vzmeOxov +v+6zeUtItCmmJj+Z3v7JMp5ic+H86pnSjeJxoyymymq34+f+KSc3IyX5ogm1ppY9 +THqbIHHyeP8mZf8WI9qihJ2bK0LvkbKN4kv7Qkay6CIbm8s8V218DvH0HESi5aN2 +MHQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB +/wQCMAAwHwYDVR0jBBgwFoAUEXNuRxhdpiEaG0xhJjnI+17pdCQwHgYDVR0RBBcw +FYITdGVzdC5nb2xhbmcuZXhhbXBsZTANBgkqhkiG9w0BAQsFAAOCAQEAaqe3/b+W +WOSWcvN1P7gb2JIDigcNvStq04GTItDmAFS3TFkSZryJ97cfW4HjYesrk+uNMDG4 +JsUT1I4pyEqIV4LaCAEzuWLRl4Mfi8aEgJWHzSTNBflmJnv1vZ1a/bbyXgYq4bCi ++VlDVm2AJ4hBoEE6KuzZZRh84C/nl/nmuW/lRGynr8i2oatHCY73V3hGvZ0tcXpJ +NwYUG4xDVhCk/U5aqeKJ/rI1odErvifSJYCCfqmDxE25pBL2Ci9FfQK8izPKrO/l +NhZaZhuAYd/i+OqZK1BoiRs4xpVRhKRXol8ES2Hr+Sz+OhcX3xfrfY8DHyHp5rmW +SV/Pw5w+PSi7tg== +-----END CERTIFICATE-----` + +const testClientMLDSA87KeyPEM = ` +-----BEGIN TESTING KEY----- +MDQCAQAwCwYJYIZIAWUDBAMTBCKAIJOInTmP6L2i2T7bVftiIR5OkrnZTSQLimAK +NFW4io0J +-----END TESTING KEY-----` diff --git a/mldsa_x509_test.go b/mldsa_x509_test.go new file mode 100644 index 00000000..93dda622 --- /dev/null +++ b/mldsa_x509_test.go @@ -0,0 +1,58 @@ +//go:build !nomldsa + +package tls + +import ( + "crypto" + "strings" + "testing" +) + +func TestParseCertificatePatchesMLDSAPublicKey(t *testing.T) { + tests := []struct { + name string + certPEM string + keyPEM string + sigAlg SignatureScheme + }{ + {"MLDSA44", testMLDSA44CertPEM, testMLDSA44KeyPEM, MLDSA44}, + {"MLDSA65", testMLDSA65CertPEM, testMLDSA65KeyPEM, MLDSA65}, + {"MLDSA87", testMLDSA87CertPEM, testMLDSA87KeyPEM, MLDSA87}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cert, err := X509KeyPair([]byte(test.certPEM), []byte(testingKeyToPrivateKeyPEM(test.keyPEM))) + if err != nil { + t.Fatal(err) + } + if cert.Leaf == nil { + t.Fatal("X509KeyPair did not set Leaf") + } + expectMLDSAPublicKey(t, cert.Leaf.PublicKey, test.sigAlg) + if !isMLDSAPrivateKey(cert.PrivateKey) { + t.Fatalf("PrivateKey = %T, want ML-DSA", cert.PrivateKey) + } + if !publicKeyMatchesMLDSAPrivateKey(cert.Leaf.PublicKey, cert.PrivateKey) { + t.Fatal("ML-DSA public/private key mismatch") + } + }) + } +} + +func expectMLDSAPublicKey(t *testing.T, pub crypto.PublicKey, want SignatureScheme) { + t.Helper() + + got, ok := mldsaSignatureSchemeForPublicKey(pub) + if !ok { + t.Fatalf("PublicKey = %T, want ML-DSA", pub) + } + if got != want { + t.Fatalf("ML-DSA signature scheme = %v, want %v", got, want) + } +} + +func testingKeyToPrivateKeyPEM(keyPEM string) string { + keyPEM = strings.ReplaceAll(keyPEM, "BEGIN TESTING KEY", "BEGIN PRIVATE KEY") + return strings.ReplaceAll(keyPEM, "END TESTING KEY", "END PRIVATE KEY") +} diff --git a/tls.go b/tls.go index d76e0008..bb87f0c8 100644 --- a/tls.go +++ b/tls.go @@ -300,18 +300,12 @@ func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { // We don't need to parse the public key for TLS, but we so do anyway // to check that it looks sane and matches the private key. - x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + x509Cert, err := parseCertificate(cert.Certificate[0]) if err != nil { return fail(err) } - // [uTLS section begins] - // if x509keypairleaf.Value() != "0" { - // cert.Leaf = x509Cert - // } else { - // x509keypairleaf.IncNonDefault() - // } - // [uTLS section ends] + cert.Leaf = x509Cert cert.PrivateKey, err = parsePrivateKey(keyDERBlock.Bytes) if err != nil { @@ -344,6 +338,12 @@ func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { return fail(errors.New("tls: private key does not match public key")) } default: + if isMLDSAPublicKey(pub) { + if !publicKeyMatchesMLDSAPrivateKey(pub, cert.PrivateKey) { + return fail(errors.New("tls: private key does not match public key")) + } + break + } return fail(errors.New("tls: unknown public key algorithm")) } @@ -362,9 +362,15 @@ func parsePrivateKey(der []byte) (crypto.PrivateKey, error) { case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: return key, nil default: + if isMLDSAPrivateKey(key) { + return key, nil + } return nil, errors.New("tls: found unknown private key type in PKCS#8 wrapping") } } + if key, err := parseMLDSAPrivateKey(der); err == nil { + return key, nil + } if key, err := x509.ParseECPrivateKey(der); err == nil { return key, nil } diff --git a/tls_test.go b/tls_test.go index c91263b1..f67c4967 100644 --- a/tls_test.go +++ b/tls_test.go @@ -899,7 +899,7 @@ func TestCloneNonFuncFields(t *testing.T) { f.Set(reflect.ValueOf([]EncryptedClientHelloKey{ {Config: []byte{1}, PrivateKey: []byte{1}}, })) - case "mutex", "autoSessionTicketKeys", "sessionTicketKeys": + case "mutex", "autoSessionTicketKeys", "sessionTicketKeys", "testingOnlyForceSignatureAlgorithms": continue // these are unexported fields that are handled separately case "ApplicationSettings": // [UTLS] ALPS (Application Settings) f.Set(reflect.ValueOf(map[string][]byte{"a": {1}})) @@ -910,6 +910,7 @@ func TestCloneNonFuncFields(t *testing.T) { // Set the unexported fields related to session ticket keys, which are copied with Clone(). c1.autoSessionTicketKeys = []ticketKey{c1.ticketKeyFromBytes(c1.SessionTicketKey)} c1.sessionTicketKeys = []ticketKey{c1.ticketKeyFromBytes(c1.SessionTicketKey)} + c1.testingOnlyForceSignatureAlgorithms = []SignatureScheme{PSSWithSHA256} c2 := c1.Clone() if !reflect.DeepEqual(&c1, c2) { diff --git a/u_common.go b/u_common.go index a9f90f75..a540be2e 100644 --- a/u_common.go +++ b/u_common.go @@ -613,7 +613,9 @@ var ( HelloFirefox_120 = ClientHelloID{helloFirefox, "120", nil, nil} HelloFirefox_148 = ClientHelloID{helloFirefox, "148", nil, nil} - HelloChrome_Auto = HelloChrome_133 + // Track the newest Chrome profile whose advertised TLS capabilities are + // supported by the current Go toolchain. + HelloChrome_Auto = defaultChromeAutoID() HelloChrome_58 = ClientHelloID{helloChrome, "58", nil, nil} HelloChrome_62 = ClientHelloID{helloChrome, "62", nil, nil} HelloChrome_70 = ClientHelloID{helloChrome, "70", nil, nil} @@ -645,6 +647,9 @@ var ( HelloChrome_131 = ClientHelloID{helloChrome, "131", nil, nil} // Chrome w/ New ALPS codepoint HelloChrome_133 = ClientHelloID{helloChrome, "133", nil, nil} + // Chrome w/ ML-DSA signature algorithms + HelloChrome_150 = ClientHelloID{helloChrome, "150", nil, nil} + HelloChrome_150_PSK = ClientHelloID{helloChrome, "150_PSK", nil, nil} HelloIOS_Auto = HelloIOS_14 HelloIOS_11_1 = ClientHelloID{helloIOS, "111", nil, nil} // legacy "111" means 11.1 @@ -670,6 +675,13 @@ var ( HelloQQ_11_1 = ClientHelloID{helloQQ, "11.1", nil, nil} ) +func defaultChromeAutoID() ClientHelloID { + if goMLDSASupported() { + return HelloChrome_150 + } + return HelloChrome_133 +} + type Weights struct { Extensions_Append_ALPN float64 TLSVersMax_Set_VersionTLS13 float64 diff --git a/u_handshake_client.go b/u_handshake_client.go index 9928f0c3..972dccb8 100644 --- a/u_handshake_client.go +++ b/u_handshake_client.go @@ -258,7 +258,10 @@ func (c *Conn) makeClientHelloForApplyPreset() (*clientHelloMsg, *keySharePrivat } if maxVersion >= VersionTLS12 { - hello.supportedSignatureAlgorithms = supportedSignatureAlgorithms() + hello.supportedSignatureAlgorithms = supportedSignatureAlgorithmsForVersion(maxVersion) + } + if config.testingOnlyForceSignatureAlgorithms != nil { + hello.supportedSignatureAlgorithms = config.testingOnlyForceSignatureAlgorithms } if testingOnlyForceClientHelloSignatureAlgorithms != nil { hello.supportedSignatureAlgorithms = testingOnlyForceClientHelloSignatureAlgorithms diff --git a/u_parrots.go b/u_parrots.go index 8416fa33..f3fd1704 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -964,6 +964,90 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { &UtlsGREASEExtension{}, }), }, nil + case HelloChrome_150: + signatureAlgorithms := []SignatureScheme{ + MLDSA44, + MLDSA65, + MLDSA87, + ECDSAWithP256AndSHA256, + PSSWithSHA256, + PKCS1WithSHA256, + ECDSAWithP384AndSHA384, + PSSWithSHA384, + PKCS1WithSHA384, + PSSWithSHA512, + PKCS1WithSHA512, + } + return ClientHelloSpec{ + CipherSuites: []uint16{ + GREASE_PLACEHOLDER, + TLS_AES_128_GCM_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{ + 0x00, // compressionNone + }, + Extensions: ShuffleChromeTLSExtensions([]TLSExtension{ + &UtlsGREASEExtension{}, + &KeyShareExtension{KeyShares: []KeyShare{ + {Group: CurveID(GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: X25519MLKEM768}, + {Group: X25519}, + }}, + &SNIExtension{}, + &ApplicationSettingsExtensionNew{ + SupportedProtocols: []string{"h2"}, + }, + &RenegotiationInfoExtension{ + Renegotiation: RenegotiateOnceAsClient, + }, + &SupportedCurvesExtension{Curves: []CurveID{ + GREASE_PLACEHOLDER, + X25519MLKEM768, + X25519, + CurveP256, + CurveP384, + }}, + &UtlsCompressCertExtension{Algorithms: []CertCompressionAlgo{ + CertCompressionBrotli, + }}, + &SessionTicketExtension{}, + &StatusRequestExtension{}, + &ExtendedMasterSecretExtension{}, + &SupportedVersionsExtension{Versions: []uint16{ + GREASE_PLACEHOLDER, + VersionTLS13, + VersionTLS12, + }}, + &SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: signatureAlgorithms}, + &SCTExtension{}, + &SupportedPointsExtension{SupportedPoints: []byte{ + 0x00, // pointFormatUncompressed + }}, + BoringGREASEECH(), + &ALPNExtension{AlpnProtocols: []string{ + "h2", + "http/1.1", + }}, + &PSKKeyExchangeModesExtension{Modes: []uint8{ + PskModeDHE, + }}, + &UtlsGREASEExtension{}, + }), + }, nil case HelloFirefox_55, HelloFirefox_56: return ClientHelloSpec{ TLSVersMax: VersionTLS12, @@ -2927,6 +3011,91 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { &UtlsPreSharedKeyExtension{}, }), }, nil + case HelloChrome_150_PSK: + signatureAlgorithms := []SignatureScheme{ + MLDSA44, + MLDSA65, + MLDSA87, + ECDSAWithP256AndSHA256, + PSSWithSHA256, + PKCS1WithSHA256, + ECDSAWithP384AndSHA384, + PSSWithSHA384, + PKCS1WithSHA384, + PSSWithSHA512, + PKCS1WithSHA512, + } + return ClientHelloSpec{ + CipherSuites: []uint16{ + GREASE_PLACEHOLDER, + TLS_AES_128_GCM_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{ + 0x00, // compressionNone + }, + Extensions: ShuffleChromeTLSExtensions([]TLSExtension{ + &UtlsGREASEExtension{}, + &ApplicationSettingsExtensionNew{ + SupportedProtocols: []string{"h2"}, + }, + &SupportedVersionsExtension{Versions: []uint16{ + GREASE_PLACEHOLDER, + VersionTLS13, + VersionTLS12, + }}, + &SCTExtension{}, + BoringGREASEECH(), + &KeyShareExtension{KeyShares: []KeyShare{ + {Group: CurveID(GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: X25519MLKEM768}, + {Group: X25519}, + }}, + &SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: signatureAlgorithms}, + &SupportedCurvesExtension{Curves: []CurveID{ + GREASE_PLACEHOLDER, + X25519MLKEM768, + X25519, + CurveP256, + CurveP384, + }}, + &UtlsCompressCertExtension{Algorithms: []CertCompressionAlgo{ + CertCompressionBrotli, + }}, + &ExtendedMasterSecretExtension{}, + &SessionTicketExtension{}, + &SNIExtension{}, + &RenegotiationInfoExtension{ + Renegotiation: RenegotiateOnceAsClient, + }, + &PSKKeyExchangeModesExtension{Modes: []uint8{ + PskModeDHE, + }}, + &SupportedPointsExtension{SupportedPoints: []byte{ + 0x00, // pointFormatUncompressed + }}, + &StatusRequestExtension{}, + &ALPNExtension{AlpnProtocols: []string{ + "h2", + "http/1.1", + }}, + &UtlsGREASEExtension{}, + &UtlsPreSharedKeyExtension{}, + }), + }, nil default: if id.Client == helloRandomized || id.Client == helloRandomizedALPN || id.Client == helloRandomizedNoALPN { // Use empty values as they can be filled later by UConn.ApplyPreset or manually. diff --git a/u_parrots_cases_test.go b/u_parrots_cases_test.go new file mode 100644 index 00000000..622d9c8a --- /dev/null +++ b/u_parrots_cases_test.go @@ -0,0 +1,85 @@ +package tls + +import ( + "go/ast" + "go/parser" + "go/token" + "testing" +) + +// TestUTLSIdToSpecNoDuplicateCaseLabels guards a merge hazard: ClientHelloID +// values are package-level vars, not consts, so the compiler does not reject a +// duplicate case in the utlsIdToSpec switch and the second copy silently becomes +// dead code. A cross-region merge can introduce such a duplicate without a +// textual conflict, so assert every case label appears exactly once. +func TestUTLSIdToSpecNoDuplicateCaseLabels(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "u_parrots.go", nil, 0) + if err != nil { + t.Fatalf("parse u_parrots.go: %v", err) + } + + var sw *ast.SwitchStmt + ast.Inspect(file, func(n ast.Node) bool { + fn, ok := n.(*ast.FuncDecl) + if !ok || fn.Name.Name != "utlsIdToSpec" { + return true + } + ast.Inspect(fn.Body, func(m ast.Node) bool { + s, ok := m.(*ast.SwitchStmt) + if !ok { + return true + } + if id, ok := s.Tag.(*ast.Ident); ok && id.Name == "id" { + sw = s + return false + } + return true + }) + return false + }) + if sw == nil { + t.Fatal("could not find `switch id` in utlsIdToSpec") + } + + seen := map[string]int{} + for _, stmt := range sw.Body.List { + cc, ok := stmt.(*ast.CaseClause) + if !ok { + continue + } + for _, expr := range cc.List { // nil List is the default clause + id, ok := expr.(*ast.Ident) + if !ok { + t.Fatalf("unexpected non-identifier case expression at %s", fset.Position(expr.Pos())) + } + seen[id.Name]++ + } + } + + for name, count := range seen { + if count > 1 { + t.Errorf("case %s appears %d times in utlsIdToSpec (duplicate/dead case)", name, count) + } + } +} + +// TestChromeProfilesGenerateSpec checks that the well-known Chrome profiles are +// reachable and produce a non-empty spec without error. +func TestChromeProfilesGenerateSpec(t *testing.T) { + for _, id := range []ClientHelloID{ + HelloChrome_133, + HelloChrome_150, + HelloChrome_150_PSK, + } { + t.Run(id.Str(), func(t *testing.T) { + spec, err := UTLSIdToSpec(id) + if err != nil { + t.Fatalf("UTLSIdToSpec(%s): %v", id.Str(), err) + } + if len(spec.Extensions) == 0 { + t.Fatalf("%s: empty extension list", id.Str()) + } + }) + } +} diff --git a/u_parrots_test.go b/u_parrots_test.go index 1a28d56d..bd27f251 100644 --- a/u_parrots_test.go +++ b/u_parrots_test.go @@ -3,6 +3,8 @@ package tls import ( "bytes" "net" + "reflect" + "slices" "testing" ) @@ -152,3 +154,113 @@ func TestHybridClassicalKeySharesAreIndependentByDefault(t *testing.T) { t.Fatal("expected independent keyshares by default: hybrid/classical ECDHE private keys should differ") } } + +func findSignatureAlgorithmsExtension(t *testing.T, exts []TLSExtension) []SignatureScheme { + t.Helper() + + for _, ext := range exts { + if sigExt, ok := ext.(*SignatureAlgorithmsExtension); ok { + return sigExt.SupportedSignatureAlgorithms + } + } + + t.Fatal("signature_algorithms extension not found") + return nil +} + +func hasApplicationSettingsNewExtension(exts []TLSExtension) bool { + for _, ext := range exts { + if _, ok := ext.(*ApplicationSettingsExtensionNew); ok { + return true + } + } + return false +} + +func hasPreSharedKeyExtension(exts []TLSExtension) bool { + for _, ext := range exts { + if _, ok := ext.(*UtlsPreSharedKeyExtension); ok { + return true + } + } + return false +} + +func hasTrustAnchorsExtension(exts []TLSExtension) bool { + for _, ext := range exts { + if genericExt, ok := ext.(*GenericExtension); ok && genericExt.Id == 0xca34 { + return true + } + if reflect.TypeOf(ext).String() == "*tls.TrustAnchorsExtension" { + return true + } + } + return false +} + +func expectedChrome150SignatureAlgorithms() []SignatureScheme { + return []SignatureScheme{ + MLDSA44, + MLDSA65, + MLDSA87, + ECDSAWithP256AndSHA256, + PSSWithSHA256, + PKCS1WithSHA256, + ECDSAWithP384AndSHA384, + PSSWithSHA384, + PKCS1WithSHA384, + PSSWithSHA512, + PKCS1WithSHA512, + } +} + +func TestHelloChrome150PrependsMLDSASignatureAlgorithms(t *testing.T) { + spec, err := UTLSIdToSpec(HelloChrome_150) + if err != nil { + t.Fatalf("unexpected error creating Chrome 150 spec: %v", err) + } + + sigAlgs := findSignatureAlgorithmsExtension(t, spec.Extensions) + expected := expectedChrome150SignatureAlgorithms() + if !slices.Equal(sigAlgs, expected) { + t.Fatalf("unexpected Chrome 150 signature algorithms:\nwant %v\ngot %v", expected, sigAlgs) + } + if !hasApplicationSettingsNewExtension(spec.Extensions) { + t.Fatal("expected Chrome 150 to keep the new ALPS extension codepoint") + } + if hasTrustAnchorsExtension(spec.Extensions) { + t.Fatal("expected Chrome 150 not to advertise the trust anchors extension") + } +} + +func TestHelloChromeAutoTracksGoMLDSASupport(t *testing.T) { + expected := HelloChrome_133 + if goMLDSASupported() { + expected = HelloChrome_150 + } + if HelloChrome_Auto != expected { + t.Fatalf("unexpected HelloChrome_Auto: want %s, got %s", expected.Str(), HelloChrome_Auto.Str()) + } +} + +func TestHelloChrome150PSKPrependsMLDSASignatureAlgorithms(t *testing.T) { + spec, err := UTLSIdToSpec(HelloChrome_150_PSK) + if err != nil { + t.Fatalf("unexpected error creating Chrome 150 PSK spec: %v", err) + } + + sigAlgs := findSignatureAlgorithmsExtension(t, spec.Extensions) + expected := expectedChrome150SignatureAlgorithms() + if !slices.Equal(sigAlgs, expected) { + t.Fatalf("unexpected Chrome 150 PSK signature algorithms:\nwant %v\ngot %v", expected, sigAlgs) + } + if !hasApplicationSettingsNewExtension(spec.Extensions) { + t.Fatal("expected Chrome 150 PSK to keep the new ALPS extension codepoint") + } + if hasTrustAnchorsExtension(spec.Extensions) { + t.Fatal("expected Chrome 150 PSK not to advertise the trust anchors extension") + } + if !hasPreSharedKeyExtension(spec.Extensions) { + t.Fatal("expected Chrome 150 PSK to keep the pre-shared key extension") + } +}