| // 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 ( | |
| "crypto" | |
| "crypto/ecdsa" | |
| "crypto/elliptic" | |
| "crypto/rand" | |
| "crypto/x509" | |
| "fmt" | |
| "time" | |
| "golang.org/x/crypto/cryptobyte" | |
| ) | |
| // delegatedCredentialConfig specifies the shape of a delegated credential, not | |
| // including the keys themselves. | |
| type delegatedCredentialConfig struct { | |
| // lifetime is the amount of time, from the notBefore of the parent | |
| // certificate, that the delegated credential is valid for. If zero, then 24 | |
| // hours is assumed. | |
| lifetime time.Duration | |
| // dcAlgo is the signature scheme that should be used with this delegated | |
| // credential. If zero, ECDSA with P-256 is assumed. | |
| dcAlgo signatureAlgorithm | |
| // algo is the signature algorithm that the delegated credential itself is | |
| // signed with. Cannot be zero. | |
| algo signatureAlgorithm | |
| } | |
| func createDelegatedCredential(parent *Credential, config delegatedCredentialConfig) *Credential { | |
| if parent.Type != CredentialTypeX509 { | |
| panic("delegated credentials must be issued by X.509 credentials") | |
| } | |
| dcAlgo := config.dcAlgo | |
| if dcAlgo == 0 { | |
| dcAlgo = signatureECDSAWithP256AndSHA256 | |
| } | |
| var dcPriv crypto.Signer | |
| switch dcAlgo { | |
| case signatureRSAPKCS1WithMD5, signatureRSAPKCS1WithSHA1, signatureRSAPKCS1WithSHA256, signatureRSAPKCS1WithSHA384, signatureRSAPKCS1WithSHA512, signatureRSAPSSWithSHA256, signatureRSAPSSWithSHA384, signatureRSAPSSWithSHA512: | |
| dcPriv = &rsa2048Key | |
| case signatureECDSAWithSHA1, signatureECDSAWithP256AndSHA256, signatureECDSAWithP384AndSHA384, signatureECDSAWithP521AndSHA512: | |
| var curve elliptic.Curve | |
| switch dcAlgo { | |
| case signatureECDSAWithSHA1, signatureECDSAWithP256AndSHA256: | |
| curve = elliptic.P256() | |
| case signatureECDSAWithP384AndSHA384: | |
| curve = elliptic.P384() | |
| case signatureECDSAWithP521AndSHA512: | |
| curve = elliptic.P521() | |
| default: | |
| panic("internal error") | |
| } | |
| priv, err := ecdsa.GenerateKey(curve, rand.Reader) | |
| if err != nil { | |
| panic(err) | |
| } | |
| dcPriv = priv | |
| default: | |
| panic(fmt.Errorf("unsupported DC signature algorithm: %x", dcAlgo)) | |
| } | |
| lifetime := config.lifetime | |
| if lifetime == 0 { | |
| lifetime = 24 * time.Hour | |
| } | |
| lifetimeSecs := int64(lifetime.Seconds()) | |
| if lifetimeSecs < 0 || lifetimeSecs > 1<<32 { | |
| panic(fmt.Errorf("lifetime %s is too long to be expressed", lifetime)) | |
| } | |
| // https://www.rfc-editor.org/rfc/rfc9345.html#section-4 | |
| dc := cryptobyte.NewBuilder(nil) | |
| dc.AddUint32(uint32(lifetimeSecs)) | |
| dc.AddUint16(uint16(dcAlgo)) | |
| pubBytes, err := x509.MarshalPKIXPublicKey(dcPriv.Public()) | |
| if err != nil { | |
| panic(err) | |
| } | |
| addUint24LengthPrefixedBytes(dc, pubBytes) | |
| var dummyConfig Config | |
| msg := delegatedCredentialSignedMessage(dc.BytesOrPanic(), config.algo, parent.Certificate[0]) | |
| parentSignature, err := signMessage(false /* server */, version{VersionTLS13}, parent.PrivateKey, &dummyConfig, config.algo, msg) | |
| if err != nil { | |
| panic(err) | |
| } | |
| dc.AddUint16(uint16(config.algo)) | |
| addUint16LengthPrefixedBytes(dc, parentSignature) | |
| dcCred := *parent | |
| dcCred.Type = CredentialTypeDelegated | |
| dcCred.DelegatedCredential = dc.BytesOrPanic() | |
| dcCred.PrivateKey = dcPriv | |
| dcCred.KeyPath = writeTempKeyFile(dcPriv) | |
| return &dcCred | |
| } | |
| func addDelegatedCredentialTests() { | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-NoClientSupport", | |
| config: Config{ | |
| MinVersion: VersionTLS13, | |
| MaxVersion: VersionTLS13, | |
| }, | |
| shimCredentials: []*Credential{p256DC, &rsaCertificate}, | |
| flags: []string{"-expect-selected-credential", "1"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: &rsaCertificate, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-Basic", | |
| config: Config{ | |
| MinVersion: VersionTLS13, | |
| MaxVersion: VersionTLS13, | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP256AndSHA256}, | |
| }, | |
| shimCredentials: []*Credential{p256DC, &rsaCertificate}, | |
| flags: []string{"-expect-selected-credential", "0"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: p256DC, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-ExactAlgorithmMatch", | |
| config: Config{ | |
| MinVersion: VersionTLS13, | |
| MaxVersion: VersionTLS13, | |
| // Test that the server doesn't mix up the two signature algorithm | |
| // fields. These options are a match because the signature_algorithms | |
| // extension matches against the signature on the delegated | |
| // credential, while the delegated_credential extension matches | |
| // against the signature made by the delegated credential. | |
| VerifySignatureAlgorithms: []signatureAlgorithm{signatureRSAPSSWithSHA256}, | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP256AndSHA256}, | |
| }, | |
| shimCredentials: []*Credential{p256DC, &rsaCertificate}, | |
| flags: []string{"-expect-selected-credential", "0"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: p256DC, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-SigAlgoMissing", | |
| config: Config{ | |
| MinVersion: VersionTLS13, | |
| MaxVersion: VersionTLS13, | |
| // If the client doesn't support the signature in the delegated credential, | |
| // the server should not use delegated credentials. | |
| VerifySignatureAlgorithms: []signatureAlgorithm{signatureRSAPSSWithSHA384}, | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP256AndSHA256}, | |
| }, | |
| shimCredentials: []*Credential{p256DC, &rsaCertificate}, | |
| flags: []string{"-expect-selected-credential", "1"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: &rsaCertificate, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-CertVerifySigAlgoMissing", | |
| config: Config{ | |
| MinVersion: VersionTLS13, | |
| MaxVersion: VersionTLS13, | |
| // If the client doesn't support the delegated credential's | |
| // CertificateVerify algorithm, the server should not use delegated | |
| // credentials. | |
| VerifySignatureAlgorithms: []signatureAlgorithm{signatureRSAPSSWithSHA256}, | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP384AndSHA384}, | |
| }, | |
| shimCredentials: []*Credential{p256DC, &rsaCertificate}, | |
| flags: []string{"-expect-selected-credential", "1"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: &rsaCertificate, | |
| }, | |
| }) | |
| // Delegated credentials are not supported below TLS 1.3, even if the | |
| // client sends the extension. | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-TLS11-Forbidden", | |
| config: Config{ | |
| MinVersion: VersionTLS11, | |
| MaxVersion: VersionTLS11, | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP256AndSHA256}, | |
| }, | |
| shimCredentials: []*Credential{p256DC, &rsaCertificate}, | |
| flags: []string{"-expect-selected-credential", "1"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: &rsaCertificate, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-TLS12-Forbidden", | |
| config: Config{ | |
| MinVersion: VersionTLS12, | |
| MaxVersion: VersionTLS12, | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP256AndSHA256}, | |
| }, | |
| shimCredentials: []*Credential{p256DC, &rsaCertificate}, | |
| flags: []string{"-expect-selected-credential", "1"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: &rsaCertificate, | |
| }, | |
| }) | |
| // Generate another delegated credential, so we can get the keys out of sync. | |
| dcWrongKey := createDelegatedCredential(&rsaCertificate, delegatedCredentialConfig{ | |
| algo: signatureRSAPSSWithSHA256, | |
| }) | |
| dcWrongKey.DelegatedCredential = p256DC.DelegatedCredential | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-KeyMismatch", | |
| // The handshake hints version of the test will, as a side effect, use a | |
| // custom private key. Custom private keys can't be checked for key | |
| // mismatches. | |
| skipHints: true, | |
| shimCredentials: []*Credential{dcWrongKey}, | |
| shouldFail: true, | |
| expectedError: ":KEY_VALUES_MISMATCH:", | |
| }) | |
| // RSA delegated credentials should be rejected at configuration time. | |
| rsaDC := createDelegatedCredential(&rsaCertificate, delegatedCredentialConfig{ | |
| algo: signatureRSAPSSWithSHA256, | |
| dcAlgo: signatureRSAPSSWithSHA256, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-NoRSA", | |
| shimCredentials: []*Credential{rsaDC}, | |
| shouldFail: true, | |
| expectedError: ":INVALID_SIGNATURE_ALGORITHM:", | |
| }) | |
| // If configured with multiple delegated credentials, the server can cleanly | |
| // select the first one that works. | |
| p384DC := createDelegatedCredential(&rsaCertificate, delegatedCredentialConfig{ | |
| dcAlgo: signatureECDSAWithP384AndSHA384, | |
| algo: signatureRSAPSSWithSHA256, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-Multiple", | |
| config: Config{ | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP384AndSHA384}, | |
| }, | |
| shimCredentials: []*Credential{p256DC, p384DC}, | |
| flags: []string{"-expect-selected-credential", "1"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: p384DC, | |
| }, | |
| }) | |
| // Delegated credentials participate in issuer-based certificate selection. | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-MatchIssuer", | |
| config: Config{ | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP256AndSHA256}, | |
| // The client requested p256DCFromECDSA's issuer. | |
| RootCAs: makeCertPoolFromRoots(p256DCFromECDSA), | |
| SendRootCAs: true, | |
| }, | |
| shimCredentials: []*Credential{ | |
| p256DC.WithMustMatchIssuer(true), p256DCFromECDSA.WithMustMatchIssuer(true)}, | |
| flags: []string{"-expect-selected-credential", "1"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: p256DCFromECDSA, | |
| }, | |
| }) | |
| // Delegated credentials participate in trust anchor IDs. | |
| id1 := []byte{1, 1} | |
| id2 := []byte{2, 2, 2} | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| name: "DelegatedCredentials-TrustAnchorIDs", | |
| config: Config{ | |
| RequestTrustAnchors: [][]byte{id2}, | |
| DelegatedCredentialAlgorithms: []signatureAlgorithm{signatureECDSAWithP256AndSHA256}, | |
| Bugs: ProtocolBugs{ | |
| ExpectPeerAvailableTrustAnchors: [][]byte{id1, id2}, | |
| }, | |
| }, | |
| shimCredentials: []*Credential{ | |
| p256DC.WithTrustAnchorID(id1), p256DCFromECDSA.WithTrustAnchorID(id2)}, | |
| flags: []string{"-expect-selected-credential", "1"}, | |
| expectations: connectionExpectations{ | |
| peerCertificate: p256DCFromECDSA, | |
| }, | |
| }) | |
| } |