| // 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 ( |
| "bytes" |
| "fmt" |
| |
| "golang.org/x/crypto/cryptobyte" |
| ) |
| |
| func trustAnchorListFlagValue(ids ...[]byte) string { |
| b := cryptobyte.NewBuilder(nil) |
| for _, id := range ids { |
| addUint8LengthPrefixedBytes(b, id) |
| } |
| return base64FlagValue(b.BytesOrPanic()) |
| } |
| |
| func addTrustAnchorTests() { |
| id1 := []byte{1} |
| id2 := []byte{2, 2} |
| id3 := []byte{3, 3, 3} |
| id4 := []byte{4, 4, 4, 4} |
| |
| // Unsolicited trust_anchors extensions should be rejected. |
| testCases = append(testCases, testCase{ |
| name: "TrustAnchors-Unsolicited-Certificate", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| Bugs: ProtocolBugs{ |
| AlwaysMatchTrustAnchorID: true, |
| }, |
| }, |
| shouldFail: true, |
| expectedLocalError: "remote error: unsupported extension", |
| expectedError: ":UNEXPECTED_EXTENSION:", |
| }) |
| testCases = append(testCases, testCase{ |
| name: "TrustAnchors-Unsolicited-EncryptedExtensions", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| AvailableTrustAnchors: [][]byte{id1, id2}, |
| Bugs: ProtocolBugs{ |
| AlwaysSendAvailableTrustAnchors: true, |
| }, |
| }, |
| shouldFail: true, |
| expectedLocalError: "remote error: unsupported extension", |
| expectedError: ":UNEXPECTED_EXTENSION:", |
| }) |
| |
| // Test that the client sends trust anchors when configured, and correctly |
| // reports the server's response. |
| testCases = append(testCases, testCase{ |
| name: "TrustAnchors-ClientRequest-Match", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| AvailableTrustAnchors: [][]byte{id1, id2}, |
| Credential: rsaChainCertificate.WithTrustAnchorID(id1), |
| Bugs: ProtocolBugs{ |
| ExpectPeerRequestedTrustAnchors: [][]byte{id1, id3}, |
| }, |
| }, |
| flags: []string{ |
| "-requested-trust-anchors", trustAnchorListFlagValue(id1, id3), |
| "-expect-peer-match-trust-anchor", |
| "-expect-peer-available-trust-anchors", trustAnchorListFlagValue(id1, id2), |
| }, |
| }) |
| // The client should not like it if the server indicates the match with a non-empty |
| // extension. |
| testCases = append(testCases, testCase{ |
| name: "TrustAnchors-ClientRequest-Match-Non-Empty-Extension", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| AvailableTrustAnchors: [][]byte{id1, id2}, |
| Credential: rsaChainCertificate.WithTrustAnchorID(id1), |
| Bugs: ProtocolBugs{ |
| SendNonEmptyTrustAnchorMatch: true, |
| ExpectPeerRequestedTrustAnchors: [][]byte{id1, id3}, |
| }, |
| }, |
| flags: []string{ |
| "-requested-trust-anchors", trustAnchorListFlagValue(id1, id3), |
| }, |
| shouldFail: true, |
| expectedLocalError: "remote error: error decoding message", |
| expectedError: ":ERROR_PARSING_EXTENSION:", |
| }) |
| // The client should not like it if the server indicates the match on the incorrect |
| // certificate in the Certificate message. |
| testCases = append(testCases, testCase{ |
| name: "TrustAnchors-ClientRequest-Match-On-Incorrect-Certificate", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| AvailableTrustAnchors: [][]byte{id1, id2}, |
| Credential: rsaChainCertificate.WithTrustAnchorID(id1), |
| Bugs: ProtocolBugs{ |
| SendTrustAnchorWrongCertificate: true, |
| ExpectPeerRequestedTrustAnchors: [][]byte{id1, id3}, |
| }, |
| }, |
| flags: []string{ |
| "-requested-trust-anchors", trustAnchorListFlagValue(id1, id3), |
| }, |
| shouldFail: true, |
| expectedLocalError: "remote error: unsupported extension", |
| expectedError: ":UNEXPECTED_EXTENSION:", |
| }) |
| testCases = append(testCases, testCase{ |
| name: "TrustAnchors-ClientRequest-NoMatch", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| AvailableTrustAnchors: [][]byte{id1, id2}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerRequestedTrustAnchors: [][]byte{id3}, |
| }, |
| }, |
| flags: []string{ |
| "-requested-trust-anchors", trustAnchorListFlagValue(id3), |
| "-expect-no-peer-match-trust-anchor", |
| "-expect-peer-available-trust-anchors", trustAnchorListFlagValue(id1, id2), |
| }, |
| }) |
| |
| // Invalid trust anchor IDs are rejected in ClientHello, EncryptedExtensions, |
| // and configuration. |
| invalidIDs := []struct { |
| name string |
| id []byte |
| }{ |
| {"Empty", []byte{}}, |
| {"TooLong", bytes.Repeat([]byte{5}, 33)}, |
| {"NonMinimalComponent", []byte{0x80, 0x01}}, |
| {"TruncatedComponent", []byte{0xff}}, |
| } |
| for _, id := range invalidIDs { |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: fmt.Sprintf("TrustAnchors-%s-ClientHello", id.name), |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{id.id}, |
| }, |
| shouldFail: true, |
| expectedError: ":DECODE_ERROR:", |
| }) |
| testCases = append(testCases, testCase{ |
| name: fmt.Sprintf("TrustAnchors-%s-EncryptedExtensions", id.name), |
| config: Config{ |
| MinVersion: VersionTLS13, |
| AvailableTrustAnchors: [][]byte{id.id}, |
| }, |
| flags: []string{"-requested-trust-anchors", trustAnchorListFlagValue(id1)}, |
| shouldFail: true, |
| expectedError: ":DECODE_ERROR:", |
| }) |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: fmt.Sprintf("TrustAnchors-%s-ConfigCredential", id.name), |
| shimCredentials: []*Credential{rsaCertificate.WithTrustAnchorID(id.id)}, |
| shouldFail: true, |
| expectedError: ":INVALID_TRUST_ANCHOR_ID:", |
| }) |
| testCases = append(testCases, testCase{ |
| name: fmt.Sprintf("TrustAnchors-%s-ConfigRequest", id.name), |
| flags: []string{"-requested-trust-anchors", trustAnchorListFlagValue(id.id)}, |
| shouldFail: true, |
| expectedError: ":INVALID_TRUST_ANCHOR_LIST:", |
| }) |
| } |
| |
| // Test the server selection logic, as well as whether it correctly reports |
| // available trust anchors and the match status. (The general selection flow |
| // is covered in addCertificateSelectionTests.) |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-ServerSelect-Match", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{id2}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{id1, id2}, |
| ExpectPeerMatchTrustAnchor: new(true), |
| }, |
| }, |
| shimCredentials: []*Credential{ |
| rsaCertificate.WithTrustAnchorID(id1), |
| rsaCertificate.WithTrustAnchorID(id2), |
| }, |
| flags: []string{"-expect-selected-credential", "1"}, |
| }) |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-ServerSelect-None", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{id1}, |
| }, |
| shimCredentials: []*Credential{ |
| rsaCertificate.WithTrustAnchorID(id2), |
| rsaCertificate.WithTrustAnchorID(id3), |
| }, |
| shouldFail: true, |
| expectedError: ":NO_MATCHING_ISSUER:", |
| }) |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-ServerSelect-Fallback", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{id1}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{id2, id3}, |
| ExpectPeerMatchTrustAnchor: new(false), |
| }, |
| }, |
| shimCredentials: []*Credential{ |
| rsaCertificate.WithTrustAnchorID(id2), |
| rsaCertificate.WithTrustAnchorID(id3), |
| &rsaCertificate, |
| }, |
| flags: []string{"-expect-selected-credential", "2"}, |
| }) |
| |
| // When the server sends available trust anchors, it should filter the list |
| // by whether the credential would be usable with the connection at all. |
| p256DC := createDelegatedCredential(&rsaCertificate, delegatedCredentialConfig{ |
| dcAlgo: signatureECDSAWithP256AndSHA256, |
| algo: signatureRSAPSSWithSHA256, |
| }) |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-Server-FilterAvailable", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{id1}, |
| VerifySignatureAlgorithms: []signatureAlgorithm{signatureRSAPSSWithSHA256, signatureECDSAWithP256AndSHA256}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{id2}, |
| ExpectPeerMatchTrustAnchor: new(false), |
| }, |
| }, |
| shimCredentials: []*Credential{ |
| rsaCertificate.WithTrustAnchorID(id2), |
| // Ineligible because of signature algorithms. |
| rsaCertificate.WithTrustAnchorID(id3).WithSignatureAlgorithms(signatureRSAPSSWithSHA384), |
| // Ineligible because of credential type. |
| p256DC.WithTrustAnchorID(id4), |
| &rsaCertificate, |
| }, |
| flags: []string{"-expect-selected-credential", "3"}, |
| }) |
| |
| // After filtering, the list of available trust anchors may even be empty. |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-Server-FilterAvailable-Empty", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{id1}, |
| VerifySignatureAlgorithms: []signatureAlgorithm{signatureRSAPSSWithSHA256, signatureECDSAWithP256AndSHA256}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{}, |
| ExpectPeerMatchTrustAnchor: new(false), |
| }, |
| }, |
| shimCredentials: []*Credential{ |
| // Ineligible because of signature algorithms. |
| rsaCertificate.WithTrustAnchorID(id2).WithSignatureAlgorithms(signatureRSAPSSWithSHA384), |
| // Ineligible because of credential type. |
| p256DC.WithTrustAnchorID(id4), |
| &rsaCertificate, |
| }, |
| flags: []string{"-expect-selected-credential", "2"}, |
| }) |
| |
| // The caller can override the default available list. |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-Server-OverrideAvailable", |
| config: Config{ |
| RequestTrustAnchors: [][]byte{id1}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{id1, id2, id3}, |
| }, |
| }, |
| shimCredentials: []*Credential{&rsaCertificate}, |
| flags: []string{"-available-trust-anchors", trustAnchorListFlagValue(id1, id2, id3)}, |
| }) |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-Server-OverrideAvailable-NotRequested", |
| config: Config{ |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{}, |
| }, |
| }, |
| shimCredentials: []*Credential{&rsaCertificate}, |
| flags: []string{"-available-trust-anchors", trustAnchorListFlagValue(id1, id2, id3)}, |
| }) |
| |
| // The ClientHello list may be empty. The client must be able to send it and |
| // receive available trust anchors. |
| testCases = append(testCases, testCase{ |
| name: "TrustAnchors-ClientRequestEmpty", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| AvailableTrustAnchors: [][]byte{id1, id2}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerRequestedTrustAnchors: [][]byte{}, |
| }, |
| }, |
| flags: []string{ |
| "-requested-trust-anchors", trustAnchorListFlagValue(), |
| "-expect-peer-available-trust-anchors", trustAnchorListFlagValue(id1, id2), |
| }, |
| }) |
| // The server must be able to process it, and send available trust anchors. |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-ServerReceiveEmptyRequest", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{id1, id2}, |
| ExpectPeerMatchTrustAnchor: new(false), |
| }, |
| }, |
| shimCredentials: []*Credential{ |
| rsaCertificate.WithTrustAnchorID(id1), |
| rsaCertificate.WithTrustAnchorID(id2), |
| &rsaCertificate, |
| }, |
| flags: []string{"-expect-selected-credential", "2"}, |
| }) |
| |
| // This extension requires TLS 1.3. If a server receives this and negotiates |
| // TLS 1.2, it should ignore the extension and not accidentally send |
| // something in ServerHello (implicitly checked by runner). |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-TLS12-Server", |
| config: Config{ |
| MaxVersion: VersionTLS12, |
| RequestTrustAnchors: [][]byte{id1}, |
| }, |
| shimCredentials: []*Credential{ |
| rsaCertificate.WithTrustAnchorID(id1), |
| &rsaCertificate, |
| }, |
| // The first credential is skipped because the extension is ignored. |
| flags: []string{"-expect-selected-credential", "1"}, |
| }) |
| // The client should reject the extension in TLS 1.2 ServerHello. |
| testCases = append(testCases, testCase{ |
| name: "TrustAnchors-TLS12-Client", |
| config: Config{ |
| MaxVersion: VersionTLS12, |
| AvailableTrustAnchors: [][]byte{id1}, |
| Bugs: ProtocolBugs{ |
| AlwaysSendAvailableTrustAnchors: true, |
| }, |
| }, |
| flags: []string{"-requested-trust-anchors", trustAnchorListFlagValue(id1)}, |
| shouldFail: true, |
| expectedError: ":UNEXPECTED_EXTENSION:", |
| expectedLocalError: "remote error: unsupported extension", |
| }) |
| |
| // Having a trust_anchor_id property does not, on its own, gate the credential |
| // on an issuer match. |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-NoTrustAnchorNegotiationProperty", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{id2}, |
| }, |
| shimCredentials: []*Credential{ |
| rsaCertificate.WithProperties(CertificatePropertyList{ |
| TrustAnchorID: id1, |
| }), |
| ecdsaP256Certificate.WithTrustAnchorID(id2), |
| }, |
| // The first credential is chosen because it is not gated on a trust anchors. |
| flags: []string{"-expect-selected-credential", "0"}, |
| expectations: connectionExpectations{ |
| peerCertificate: &rsaCertificate, |
| }, |
| }) |
| |
| // The trust_anchor_negotiation property, however, does gate it. |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchors-TrustAnchorNegotiationProperty", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{id2}, |
| }, |
| shimCredentials: []*Credential{ |
| rsaCertificate.WithProperties(CertificatePropertyList{ |
| TrustAnchorID: id1, |
| TrustAnchorNegotiation: true, |
| }), |
| ecdsaP256Certificate.WithTrustAnchorID(id2), |
| }, |
| // The second credential is chosen because the first is gated on trust |
| // anchors and does not match. |
| flags: []string{"-expect-selected-credential", "1"}, |
| expectations: connectionExpectations{ |
| peerCertificate: &ecdsaP256Certificate, |
| }, |
| }) |
| |
| group := MakeTrustAnchorID(32473, 100, 200, 300, 400) |
| // These ranges match the test group. |
| match := MakeTrustAnchorIDPattern(32473).Range(50, 100).Exact(200).Range(300, 350).AtLeast(350).Bytes() |
| matchExact := MakeTrustAnchorIDPattern(32473, 100, 200, 300, 400).Bytes() |
| // These ranges do not. |
| tooShort := MakeTrustAnchorIDPattern(32473, 100, 200, 300).Bytes() |
| tooLong := MakeTrustAnchorIDPattern(32473, 100, 200, 300, 400, 0).Bytes() |
| componentTooLow := MakeTrustAnchorIDPattern(32473, 100, 200).Range(301, 350).Exact(400).Bytes() |
| componentTooHigh := MakeTrustAnchorIDPattern(32473, 100, 200).Range(250, 299).Exact(400).Bytes() |
| |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchorGroups-Match-Server", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{group}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{id1, id2}, |
| ExpectPeerMatchTrustAnchor: new(true), |
| }, |
| }, |
| shimCredentials: []*Credential{ |
| // Neither the ID nor the group inclusions match. |
| rsaCertificate.WithMustMatchIssuer(true).WithProperties(CertificatePropertyList{ |
| TrustAnchorID: id1, |
| TrustAnchorGroups: [][]byte{tooShort, tooLong, componentTooLow, componentTooHigh}, |
| }), |
| // The group inclusions match, after skipping some irrelevant inclusions. |
| ecdsaP256Certificate.WithMustMatchIssuer(true).WithProperties(CertificatePropertyList{ |
| TrustAnchorID: id2, |
| TrustAnchorGroups: [][]byte{tooShort, match}, |
| }), |
| }, |
| expectations: connectionExpectations{ |
| peerCertificate: &ecdsaP256Certificate, |
| }, |
| flags: []string{"-expect-selected-credential", "1"}, |
| }) |
| |
| testCases = append(testCases, testCase{ |
| testType: serverTest, |
| name: "TrustAnchorGroups-MatchExact-Server", |
| config: Config{ |
| MinVersion: VersionTLS13, |
| RequestTrustAnchors: [][]byte{group}, |
| Bugs: ProtocolBugs{ |
| ExpectPeerAvailableTrustAnchors: [][]byte{id1, id2}, |
| ExpectPeerMatchTrustAnchor: new(true), |
| }, |
| }, |
| shimCredentials: []*Credential{ |
| // Neither the ID nor the group inclusions match. |
| rsaCertificate.WithMustMatchIssuer(true).WithProperties(CertificatePropertyList{ |
| TrustAnchorID: id1, |
| TrustAnchorGroups: [][]byte{tooShort, tooLong, componentTooLow, componentTooHigh}, |
| }), |
| // The group inclusions match, after skipping some irrelevant inclusions. |
| ecdsaP256Certificate.WithMustMatchIssuer(true).WithProperties(CertificatePropertyList{ |
| TrustAnchorID: id2, |
| TrustAnchorGroups: [][]byte{tooShort, matchExact}, |
| }), |
| }, |
| expectations: connectionExpectations{ |
| peerCertificate: &ecdsaP256Certificate, |
| }, |
| flags: []string{"-expect-selected-credential", "1"}, |
| }) |
| } |