// Copyright 2025 The BoringSSL Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package runner import ( "fmt" "strconv" ) var testCurves = []struct { name string id CurveID }{ {"P-224", CurveP224}, {"P-256", CurveP256}, {"P-384", CurveP384}, {"P-521", CurveP521}, {"X25519", CurveX25519}, {"Kyber", CurveX25519Kyber768}, {"MLKEM", CurveX25519MLKEM768}, } const bogusCurve = 0x1234 func isPqGroup(r CurveID) bool { return r == CurveX25519Kyber768 || r == CurveX25519MLKEM768 } func isECDHGroup(r CurveID) bool { return r == CurveP224 || r == CurveP256 || r == CurveP384 || r == CurveP521 } func isX25519Group(r CurveID) bool { return r == CurveX25519 || r == CurveX25519Kyber768 || r == CurveX25519MLKEM768 } func addCurveTests() { // A set of cipher suites that ensures some curve-using mode is used. // Without this, servers may fall back to RSA key exchange. ecdheCiphers := []uint16{ TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_AES_256_GCM_SHA384, } for _, curve := range testCurves { for _, ver := range tlsVersions { if isPqGroup(curve.id) && ver.version < VersionTLS13 { continue } for _, testType := range []testType{clientTest, serverTest} { suffix := fmt.Sprintf("%s-%s-%s", testType, curve.name, ver.name) testCases = append(testCases, testCase{ testType: testType, name: "CurveTest-" + suffix, config: Config{ MaxVersion: ver.version, CipherSuites: ecdheCiphers, CurvePreferences: []CurveID{curve.id}, }, flags: append( []string{"-expect-curve-id", strconv.Itoa(int(curve.id))}, flagInts("-curves", shimConfig.AllCurves)..., ), expectations: connectionExpectations{ curveID: curve.id, }, }) badKeyShareLocalError := "remote error: illegal parameter" if testType == clientTest && ver.version >= VersionTLS13 { // If the shim is a TLS 1.3 client and the runner sends a bad // key share, the runner never reads the client's cleartext // alert because the runner has already started encrypting by // the time the client sees it. badKeyShareLocalError = "local error: bad record MAC" } testCases = append(testCases, testCase{ testType: testType, name: "CurveTest-Invalid-TruncateKeyShare-" + suffix, config: Config{ MaxVersion: ver.version, CipherSuites: ecdheCiphers, CurvePreferences: []CurveID{curve.id}, Bugs: ProtocolBugs{ TruncateKeyShare: true, }, }, flags: flagInts("-curves", shimConfig.AllCurves), shouldFail: true, expectedError: ":BAD_ECPOINT:", expectedLocalError: badKeyShareLocalError, }) testCases = append(testCases, testCase{ testType: testType, name: "CurveTest-Invalid-PadKeyShare-" + suffix, config: Config{ MaxVersion: ver.version, CipherSuites: ecdheCiphers, CurvePreferences: []CurveID{curve.id}, Bugs: ProtocolBugs{ PadKeyShare: true, }, }, flags: flagInts("-curves", shimConfig.AllCurves), shouldFail: true, expectedError: ":BAD_ECPOINT:", expectedLocalError: badKeyShareLocalError, }) if isECDHGroup(curve.id) { testCases = append(testCases, testCase{ testType: testType, name: "CurveTest-Invalid-Compressed-" + suffix, config: Config{ MaxVersion: ver.version, CipherSuites: ecdheCiphers, CurvePreferences: []CurveID{curve.id}, Bugs: ProtocolBugs{ SendCompressedCoordinates: true, }, }, flags: flagInts("-curves", shimConfig.AllCurves), shouldFail: true, expectedError: ":BAD_ECPOINT:", expectedLocalError: badKeyShareLocalError, }) testCases = append(testCases, testCase{ testType: testType, name: "CurveTest-Invalid-NotOnCurve-" + suffix, config: Config{ MaxVersion: ver.version, CipherSuites: ecdheCiphers, CurvePreferences: []CurveID{curve.id}, Bugs: ProtocolBugs{ ECDHPointNotOnCurve: true, }, }, flags: flagInts("-curves", shimConfig.AllCurves), shouldFail: true, expectedError: ":BAD_ECPOINT:", expectedLocalError: badKeyShareLocalError, }) } if isX25519Group(curve.id) { // Implementations should mask off the high order bit in X25519. testCases = append(testCases, testCase{ testType: testType, name: "CurveTest-SetX25519HighBit-" + suffix, config: Config{ MaxVersion: ver.version, CipherSuites: ecdheCiphers, CurvePreferences: []CurveID{curve.id}, Bugs: ProtocolBugs{ SetX25519HighBit: true, }, }, flags: flagInts("-curves", shimConfig.AllCurves), expectations: connectionExpectations{ curveID: curve.id, }, }) // Implementations should reject low order points. testCases = append(testCases, testCase{ testType: testType, name: "CurveTest-Invalid-LowOrderX25519Point-" + suffix, config: Config{ MaxVersion: ver.version, CipherSuites: ecdheCiphers, CurvePreferences: []CurveID{curve.id}, Bugs: ProtocolBugs{ LowOrderX25519Point: true, }, }, flags: flagInts("-curves", shimConfig.AllCurves), shouldFail: true, expectedError: ":BAD_ECPOINT:", expectedLocalError: badKeyShareLocalError, }) } if curve.id == CurveX25519MLKEM768 && testType == serverTest { testCases = append(testCases, testCase{ testType: testType, name: "CurveTest-Invalid-MLKEMEncapKeyNotReduced-" + suffix, config: Config{ MaxVersion: ver.version, CipherSuites: ecdheCiphers, CurvePreferences: []CurveID{curve.id}, Bugs: ProtocolBugs{ MLKEMEncapKeyNotReduced: true, }, }, flags: flagInts("-curves", shimConfig.AllCurves), shouldFail: true, expectedError: ":BAD_ECPOINT:", expectedLocalError: badKeyShareLocalError, }) } } } } // The server must be tolerant to bogus curves. testCases = append(testCases, testCase{ testType: serverTest, name: "UnknownCurve", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, CurvePreferences: []CurveID{bogusCurve, CurveP256}, }, }) // The server must be tolerant to bogus curves. testCases = append(testCases, testCase{ testType: serverTest, name: "UnknownCurve-TLS13", config: Config{ MaxVersion: VersionTLS13, CurvePreferences: []CurveID{bogusCurve, CurveP256}, }, }) // The server must not consider ECDHE ciphers when there are no // supported curves. testCases = append(testCases, testCase{ testType: serverTest, name: "NoSupportedCurves", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, Bugs: ProtocolBugs{ NoSupportedCurves: true, }, }, shouldFail: true, expectedError: ":NO_SHARED_CIPHER:", }) testCases = append(testCases, testCase{ testType: serverTest, name: "NoSupportedCurves-TLS13", config: Config{ MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ NoSupportedCurves: true, }, }, shouldFail: true, expectedError: ":NO_SHARED_GROUP:", }) // The server must fall back to another cipher when there are no // supported curves. testCases = append(testCases, testCase{ testType: serverTest, name: "NoCommonCurves", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{ TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_128_GCM_SHA256, }, CurvePreferences: []CurveID{CurveP224}, }, expectations: connectionExpectations{ cipher: TLS_RSA_WITH_AES_128_GCM_SHA256, }, }) // The client must reject bogus curves and disabled curves. testCases = append(testCases, testCase{ name: "BadECDHECurve", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, Bugs: ProtocolBugs{ SendCurve: bogusCurve, }, }, shouldFail: true, expectedError: ":WRONG_CURVE:", }) testCases = append(testCases, testCase{ name: "BadECDHECurve-TLS13", config: Config{ MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ SendCurve: bogusCurve, }, }, shouldFail: true, expectedError: ":WRONG_CURVE:", }) testCases = append(testCases, testCase{ name: "UnsupportedCurve", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, CurvePreferences: []CurveID{CurveP256}, Bugs: ProtocolBugs{ IgnorePeerCurvePreferences: true, }, }, flags: []string{"-curves", strconv.Itoa(int(CurveP384))}, shouldFail: true, expectedError: ":WRONG_CURVE:", }) testCases = append(testCases, testCase{ // TODO(davidben): Add a TLS 1.3 version where // HelloRetryRequest requests an unsupported curve. name: "UnsupportedCurve-ServerHello-TLS13", config: Config{ MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveP384}, Bugs: ProtocolBugs{ SendCurve: CurveP256, }, }, flags: []string{"-curves", strconv.Itoa(int(CurveP384))}, shouldFail: true, expectedError: ":WRONG_CURVE:", }) // The previous curve ID should be reported on TLS 1.2 resumption. testCases = append(testCases, testCase{ name: "CurveID-Resume-Client", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, CurvePreferences: []CurveID{CurveX25519}, }, flags: []string{"-expect-curve-id", strconv.Itoa(int(CurveX25519))}, resumeSession: true, }) testCases = append(testCases, testCase{ testType: serverTest, name: "CurveID-Resume-Server", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, CurvePreferences: []CurveID{CurveX25519}, }, flags: []string{"-expect-curve-id", strconv.Itoa(int(CurveX25519))}, resumeSession: true, }) // TLS 1.3 allows resuming at a differet curve. If this happens, the new // one should be reported. testCases = append(testCases, testCase{ name: "CurveID-Resume-Client-TLS13", config: Config{ MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveX25519}, }, resumeConfig: &Config{ MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveP256}, }, flags: []string{ "-on-initial-expect-curve-id", strconv.Itoa(int(CurveX25519)), "-on-resume-expect-curve-id", strconv.Itoa(int(CurveP256)), }, resumeSession: true, }) testCases = append(testCases, testCase{ testType: serverTest, name: "CurveID-Resume-Server-TLS13", config: Config{ MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveX25519}, }, resumeConfig: &Config{ MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveP256}, }, flags: []string{ "-on-initial-expect-curve-id", strconv.Itoa(int(CurveX25519)), "-on-resume-expect-curve-id", strconv.Itoa(int(CurveP256)), }, resumeSession: true, }) // Server-sent point formats are legal in TLS 1.2, but not in TLS 1.3. testCases = append(testCases, testCase{ name: "PointFormat-ServerHello-TLS12", config: Config{ MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ SendSupportedPointFormats: []byte{pointFormatUncompressed}, }, }, }) testCases = append(testCases, testCase{ name: "PointFormat-EncryptedExtensions-TLS13", config: Config{ MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ SendSupportedPointFormats: []byte{pointFormatUncompressed}, }, }, shouldFail: true, expectedError: ":ERROR_PARSING_EXTENSION:", }) // Server-sent supported groups/curves are legal in TLS 1.3. They are // illegal in TLS 1.2, but some servers send them anyway, so we must // tolerate them. testCases = append(testCases, testCase{ name: "SupportedCurves-ServerHello-TLS12", config: Config{ MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ SendServerSupportedCurves: true, }, }, }) testCases = append(testCases, testCase{ name: "SupportedCurves-EncryptedExtensions-TLS13", config: Config{ MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ SendServerSupportedCurves: true, }, }, }) // Test that we tolerate unknown point formats, as long as // pointFormatUncompressed is present. Limit ciphers to ECDHE ciphers to // check they are still functional. testCases = append(testCases, testCase{ name: "PointFormat-Client-Tolerance", config: Config{ MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ SendSupportedPointFormats: []byte{42, pointFormatUncompressed, 99, pointFormatCompressedPrime}, }, }, }) testCases = append(testCases, testCase{ testType: serverTest, name: "PointFormat-Server-Tolerance", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}, Bugs: ProtocolBugs{ SendSupportedPointFormats: []byte{42, pointFormatUncompressed, 99, pointFormatCompressedPrime}, }, }, }) // Test TLS 1.2 does not require the point format extension to be // present. testCases = append(testCases, testCase{ name: "PointFormat-Client-Missing", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}, Bugs: ProtocolBugs{ SendSupportedPointFormats: []byte{}, }, }, }) testCases = append(testCases, testCase{ testType: serverTest, name: "PointFormat-Server-Missing", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}, Bugs: ProtocolBugs{ SendSupportedPointFormats: []byte{}, }, }, }) // If the point format extension is present, uncompressed points must be // offered. BoringSSL requires this whether or not ECDHE is used. testCases = append(testCases, testCase{ name: "PointFormat-Client-MissingUncompressed", config: Config{ MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ SendSupportedPointFormats: []byte{pointFormatCompressedPrime}, }, }, shouldFail: true, expectedError: ":ERROR_PARSING_EXTENSION:", }) testCases = append(testCases, testCase{ testType: serverTest, name: "PointFormat-Server-MissingUncompressed", config: Config{ MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ SendSupportedPointFormats: []byte{pointFormatCompressedPrime}, }, }, shouldFail: true, expectedError: ":ERROR_PARSING_EXTENSION:", }) // Post-quantum groups require TLS 1.3. for _, curve := range testCurves { if !isPqGroup(curve.id) { continue } // Post-quantum groups should not be offered by a TLS 1.2 client. testCases = append(testCases, testCase{ name: "TLS12ClientShouldNotOffer-" + curve.name, config: Config{ Bugs: ProtocolBugs{ FailIfPostQuantumOffered: true, }, }, flags: []string{ "-max-version", strconv.Itoa(VersionTLS12), "-curves", strconv.Itoa(int(curve.id)), "-curves", strconv.Itoa(int(CurveX25519)), }, }) // Post-quantum groups should not be selected by a TLS 1.2 server. testCases = append(testCases, testCase{ testType: serverTest, name: "TLS12ServerShouldNotSelect-" + curve.name, flags: []string{ "-max-version", strconv.Itoa(VersionTLS12), "-curves", strconv.Itoa(int(curve.id)), "-curves", strconv.Itoa(int(CurveX25519)), }, expectations: connectionExpectations{ curveID: CurveX25519, }, }) // If a TLS 1.2 server selects a post-quantum group anyway, the client // should not accept it. testCases = append(testCases, testCase{ name: "ClientShouldNotAllowInTLS12-" + curve.name, config: Config{ MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ SendCurve: curve.id, }, }, flags: []string{ "-curves", strconv.Itoa(int(curve.id)), "-curves", strconv.Itoa(int(CurveX25519)), }, shouldFail: true, expectedError: ":WRONG_CURVE:", expectedLocalError: "remote error: illegal parameter", }) } // ML-KEM and Kyber should not be offered by default as a client. testCases = append(testCases, testCase{ name: "PostQuantumNotEnabledByDefaultInClients", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ FailIfPostQuantumOffered: true, }, }, }) // If ML-KEM is offered, both X25519 and ML-KEM should have a key-share. testCases = append(testCases, testCase{ name: "NotJustMLKEMKeyShare", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ ExpectedKeyShares: []CurveID{CurveX25519MLKEM768, CurveX25519}, }, }, flags: []string{ "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-curves", strconv.Itoa(int(CurveX25519)), "-expect-curve-id", strconv.Itoa(int(CurveX25519MLKEM768)), }, }) // ... and the other way around testCases = append(testCases, testCase{ name: "MLKEMKeyShareIncludedSecond", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ ExpectedKeyShares: []CurveID{CurveX25519, CurveX25519MLKEM768}, }, }, flags: []string{ "-curves", strconv.Itoa(int(CurveX25519)), "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-expect-curve-id", strconv.Itoa(int(CurveX25519)), }, }) // ... and even if there's another curve in the middle because it's the // first classical and first post-quantum "curves" that get key shares // included. testCases = append(testCases, testCase{ name: "MLKEMKeyShareIncludedThird", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ ExpectedKeyShares: []CurveID{CurveX25519, CurveX25519MLKEM768}, }, }, flags: []string{ "-curves", strconv.Itoa(int(CurveX25519)), "-curves", strconv.Itoa(int(CurveP256)), "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-expect-curve-id", strconv.Itoa(int(CurveX25519)), }, }) // If ML-KEM is the only configured curve, the key share is sent. testCases = append(testCases, testCase{ name: "JustConfiguringMLKEMWorks", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ ExpectedKeyShares: []CurveID{CurveX25519MLKEM768}, }, }, flags: []string{ "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-expect-curve-id", strconv.Itoa(int(CurveX25519MLKEM768)), }, }) // If both ML-KEM and Kyber are configured, only the preferred one's // key share should be sent. testCases = append(testCases, testCase{ name: "BothMLKEMAndKyber", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ ExpectedKeyShares: []CurveID{CurveX25519MLKEM768}, }, }, flags: []string{ "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-curves", strconv.Itoa(int(CurveX25519Kyber768)), "-expect-curve-id", strconv.Itoa(int(CurveX25519MLKEM768)), }, }) // As a server, ML-KEM is not yet supported by default. testCases = append(testCases, testCase{ testType: serverTest, name: "PostQuantumNotEnabledByDefaultForAServer", config: Config{ MinVersion: VersionTLS13, CurvePreferences: []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768, CurveX25519}, DefaultCurves: []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768}, }, flags: []string{ "-server-preference", "-expect-curve-id", strconv.Itoa(int(CurveX25519)), }, }) // In TLS 1.2, the curve list is also used to signal ECDSA curves. testCases = append(testCases, testCase{ testType: serverTest, name: "CheckECDSACurve-TLS12", config: Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS12, CurvePreferences: []CurveID{CurveP384}, }, shimCertificate: &ecdsaP256Certificate, shouldFail: true, expectedError: ":WRONG_CURVE:", }) // If the ECDSA certificate is ineligible due to a curve mismatch, the // server may still consider a PSK cipher suite. testCases = append(testCases, testCase{ testType: serverTest, name: "CheckECDSACurve-PSK-TLS12", config: Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS12, CipherSuites: []uint16{ TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA, }, CurvePreferences: []CurveID{CurveP384}, PreSharedKey: []byte("12345"), PreSharedKeyIdentity: "luggage combo", }, shimCertificate: &ecdsaP256Certificate, expectations: connectionExpectations{ cipher: TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA, }, flags: []string{ "-psk", "12345", "-psk-identity", "luggage combo", }, }) // In TLS 1.3, the curve list only controls ECDH. testCases = append(testCases, testCase{ testType: serverTest, name: "CheckECDSACurve-NotApplicable-TLS13", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveP384}, }, shimCertificate: &ecdsaP256Certificate, }) }