/* * * Copyright 2021 gRPC 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 * * http://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 advancedtls import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "fmt" "math/big" "net" "net/netip" "os" "path" "strings" "testing" "time" "google.golang.org/grpc/security/advancedtls/testdata" ) func TestUnsupportedCRLs(t *testing.T) { crlBytesSomeReasons := []byte(`-----BEGIN X509 CRL----- MIIEeDCCA2ACAQEwDQYJKoZIhvcNAQELBQAwQjELMAkGA1UEBhMCVVMxHjAcBgNV BAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczETMBEGA1UEAxMKR1RTIENBIDFPMRcN MjEwNDI2MTI1OTQxWhcNMjEwNTA2MTE1OTQwWjCCAn0wIgIRAPOOG3L4VLC7CAAA AABxQgEXDTIxMDQxOTEyMTgxOFowIQIQUK0UwBZkVdQIAAAAAHFCBRcNMjEwNDE5 MTIxODE4WjAhAhBRIXBJaKoQkQgAAAAAcULHFw0yMTA0MjAxMjE4MTdaMCICEQCv qQWUq5UxmQgAAAAAcULMFw0yMTA0MjAxMjE4MTdaMCICEQDdv5k1kKwKTQgAAAAA cUOQFw0yMTA0MjExMjE4MTZaMCICEQDGIEfR8N9sEAgAAAAAcUOWFw0yMTA0MjEx MjE4MThaMCECEBHgbLXlj5yUCAAAAABxQ/IXDTIxMDQyMTIzMDAyNlowIQIQE1wT 2GGYqKwIAAAAAHFD7xcNMjEwNDIxMjMwMDI5WjAiAhEAo/bSyDjpVtsIAAAAAHFE txcNMjEwNDIyMjMwMDI3WjAhAhARdCrSrHE0dAgAAAAAcUS/Fw0yMTA0MjIyMzAw MjhaMCECEHONohfWn3wwCAAAAABxRX8XDTIxMDQyMzIzMDAyOVowIgIRAOYkiUPA os4vCAAAAABxRYgXDTIxMDQyMzIzMDAyOFowIQIQRNTow5Eg2gEIAAAAAHFGShcN MjEwNDI0MjMwMDI2WjAhAhBX32dH4/WQ6AgAAAAAcUZNFw0yMTA0MjQyMzAwMjZa MCICEQDHnUM1vsaP/wgAAAAAcUcQFw0yMTA0MjUyMzAwMjZaMCECEEm5rvmL8sj6 CAAAAABxRxQXDTIxMDQyNTIzMDAyN1owIQIQW16OQs4YQYkIAAAAAHFIABcNMjEw NDI2MTI1NDA4WjAhAhAhSohpYsJtDQgAAAAAcUgEFw0yMTA0MjYxMjU0MDlaoGkw ZzAfBgNVHSMEGDAWgBSY0fhuEOvPm+xgnxiQG6DrfQn9KzALBgNVHRQEBAICBngw NwYDVR0cAQH/BC0wK6AmoCSGImh0dHA6Ly9jcmwucGtpLmdvb2cvR1RTMU8xY29y ZS5jcmyBAf8wDQYJKoZIhvcNAQELBQADggEBADPBXbxVxMJ1HC7btXExRUpJHUlU YbeCZGx6zj5F8pkopbmpV7cpewwhm848Fx4VaFFppZQZd92O08daEC6aEqoug4qF z6ZrOLzhuKfpW8E93JjgL91v0FYN7iOcT7+ERKCwVEwEkuxszxs7ggW6OJYJNvHh priIdmcPoiQ3ZrIRH0vE3BfUcNXnKFGATWuDkiRI0I4A5P7NiOf+lAuGZet3/eom 0chgts6sdau10GfeUpHUd4f8e93cS/QeLeG16z7LC8vRLstU3m3vrknpZbdGqSia 97w66mqcnQh9V0swZiEnVLmLufaiuDZJ+6nUzSvLqBlb/ei3T/tKV0BoKJA= -----END X509 CRL-----`) crlBytesIndirect := []byte(`-----BEGIN X509 CRL----- MIIDGjCCAgICAQEwDQYJKoZIhvcNAQELBQAwdjELMAkGA1UEBhMCVVMxEzARBgNV BAgTCkNhbGlmb3JuaWExFDASBgNVBAoTC1Rlc3RpbmcgTHRkMSowKAYDVQQLEyFU ZXN0aW5nIEx0ZCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxEDAOBgNVBAMTB1Rlc3Qg Q0EXDTIxMDExNjAyMjAxNloXDTIxMDEyMDA2MjAxNlowgfIwbAIBAhcNMjEwMTE2 MDIyMDE2WjBYMAoGA1UdFQQDCgEEMEoGA1UdHQEB/wRAMD6kPDA6MQwwCgYDVQQG EwNVU0ExDTALBgNVBAcTBGhlcmUxCzAJBgNVBAoTAnVzMQ4wDAYDVQQDEwVUZXN0 MTAgAgEDFw0yMTAxMTYwMjIwMTZaMAwwCgYDVR0VBAMKAQEwYAIBBBcNMjEwMTE2 MDIyMDE2WjBMMEoGA1UdHQEB/wRAMD6kPDA6MQwwCgYDVQQGEwNVU0ExDTALBgNV BAcTBGhlcmUxCzAJBgNVBAoTAnVzMQ4wDAYDVQQDEwVUZXN0MqBjMGEwHwYDVR0j BBgwFoAURJSDWAOfhGCryBjl8dsQjBitl3swCgYDVR0UBAMCAQEwMgYDVR0cAQH/ BCgwJqAhoB+GHWh0dHA6Ly9jcmxzLnBraS5nb29nL3Rlc3QuY3JshAH/MA0GCSqG SIb3DQEBCwUAA4IBAQBVXX67mr2wFPmEWCe6mf/wFnPl3xL6zNOl96YJtsd7ulcS TEbdJpaUnWFQ23+Tpzdj/lI2aQhTg5Lvii3o+D8C5r/Jc5NhSOtVJJDI/IQLh4pG NgGdljdbJQIT5D2Z71dgbq1ocxn8DefZIJjO3jp8VnAm7AIMX2tLTySzD2MpMeMq XmcN4lG1e4nx+xjzp7MySYO42NRY3LkphVzJhu3dRBYhBKViRJxw9hLttChitJpF 6Kh6a0QzrEY/QDJGhE1VrAD2c5g/SKnHPDVoCWo4ACIICi76KQQSIWfIdp4W/SY3 qsSIp8gfxSyzkJP+Ngkm2DdLjlJQCZ9R0MZP9Xj4 -----END X509 CRL-----`) var tests = []struct { desc string in []byte }{ { desc: "some reasons", in: crlBytesSomeReasons, }, { desc: "indirect", in: crlBytesIndirect, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { crl, err := parseRevocationList(tt.in) if err != nil { t.Fatal(err) } if _, err := parseCRLExtensions(crl); err == nil { t.Error("expected error got ok") } }) } } func TestCheckCertRevocation(t *testing.T) { dummyCrlFile := []byte(`-----BEGIN X509 CRL----- MIIDGjCCAgICAQEwDQYJKoZIhvcNAQELBQAwdjELMAkGA1UEBhMCVVMxEzARBgNV BAgTCkNhbGlmb3JuaWExFDASBgNVBAoTC1Rlc3RpbmcgTHRkMSowKAYDVQQLEyFU ZXN0aW5nIEx0ZCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxEDAOBgNVBAMTB1Rlc3Qg Q0EXDTIxMDExNjAyMjAxNloXDTIxMDEyMDA2MjAxNlowgfIwbAIBAhcNMjEwMTE2 MDIyMDE2WjBYMAoGA1UdFQQDCgEEMEoGA1UdHQEB/wRAMD6kPDA6MQwwCgYDVQQG EwNVU0ExDTALBgNVBAcTBGhlcmUxCzAJBgNVBAoTAnVzMQ4wDAYDVQQDEwVUZXN0 MTAgAgEDFw0yMTAxMTYwMjIwMTZaMAwwCgYDVR0VBAMKAQEwYAIBBBcNMjEwMTE2 MDIyMDE2WjBMMEoGA1UdHQEB/wRAMD6kPDA6MQwwCgYDVQQGEwNVU0ExDTALBgNV BAcTBGhlcmUxCzAJBgNVBAoTAnVzMQ4wDAYDVQQDEwVUZXN0MqBjMGEwHwYDVR0j BBgwFoAURJSDWAOfhGCryBjl8dsQjBitl3swCgYDVR0UBAMCAQEwMgYDVR0cAQH/ BCgwJqAhoB+GHWh0dHA6Ly9jcmxzLnBraS5nb29nL3Rlc3QuY3JshAH/MA0GCSqG SIb3DQEBCwUAA4IBAQBVXX67mr2wFPmEWCe6mf/wFnPl3xL6zNOl96YJtsd7ulcS TEbdJpaUnWFQ23+Tpzdj/lI2aQhTg5Lvii3o+D8C5r/Jc5NhSOtVJJDI/IQLh4pG NgGdljdbJQIT5D2Z71dgbq1ocxn8DefZIJjO3jp8VnAm7AIMX2tLTySzD2MpMeMq XmcN4lG1e4nx+xjzp7MySYO42NRY3LkphVzJhu3dRBYhBKViRJxw9hLttChitJpF 6Kh6a0QzrEY/QDJGhE1VrAD2c5g/SKnHPDVoCWo4ACIICi76KQQSIWfIdp4W/SY3 qsSIp8gfxSyzkJP+Ngkm2DdLjlJQCZ9R0MZP9Xj4 -----END X509 CRL-----`) crl, err := parseRevocationList(dummyCrlFile) if err != nil { t.Fatalf("parseRevocationList(dummyCrlFile) failed: %v", err) } crlExt := &CRL{certList: crl} var revocationTests = []struct { desc string in x509.Certificate revoked revocationStatus }{ { desc: "Single revoked", in: x509.Certificate{ Issuer: pkix.Name{ Country: []string{"USA"}, Locality: []string{"here"}, Organization: []string{"us"}, CommonName: "Test1", }, SerialNumber: big.NewInt(2), CRLDistributionPoints: []string{"test"}, }, revoked: RevocationRevoked, }, { desc: "Revoked no entry issuer", in: x509.Certificate{ Issuer: pkix.Name{ Country: []string{"USA"}, Locality: []string{"here"}, Organization: []string{"us"}, CommonName: "Test1", }, SerialNumber: big.NewInt(3), CRLDistributionPoints: []string{"test"}, }, revoked: RevocationRevoked, }, { desc: "Revoked new entry issuer", in: x509.Certificate{ Issuer: pkix.Name{ Country: []string{"USA"}, Locality: []string{"here"}, Organization: []string{"us"}, CommonName: "Test2", }, SerialNumber: big.NewInt(4), CRLDistributionPoints: []string{"test"}, }, revoked: RevocationRevoked, }, { desc: "Single unrevoked", in: x509.Certificate{ Issuer: pkix.Name{ Country: []string{"USA"}, Locality: []string{"here"}, Organization: []string{"us"}, CommonName: "Test2", }, SerialNumber: big.NewInt(1), CRLDistributionPoints: []string{"test"}, }, revoked: RevocationUnrevoked, }, { desc: "Single unrevoked Issuer", in: x509.Certificate{ Issuer: crl.Issuer, SerialNumber: big.NewInt(2), CRLDistributionPoints: []string{"test"}, }, revoked: RevocationUnrevoked, }, } for _, tt := range revocationTests { rawIssuer, err := asn1.Marshal(tt.in.Issuer.ToRDNSequence()) if err != nil { t.Fatalf("asn1.Marshal(%v) failed: %v", tt.in.Issuer.ToRDNSequence(), err) } tt.in.RawIssuer = rawIssuer t.Run(tt.desc, func(t *testing.T) { rev, err := checkCertRevocation(&tt.in, crlExt) if err != nil { t.Errorf("checkCertRevocation(%v) err = %v", tt.in.Issuer, err) } else if rev != tt.revoked { t.Errorf("checkCertRevocation(%v(%v)) returned %v wanted %v", tt.in.Issuer, tt.in.SerialNumber, rev, tt.revoked) } }) } } func makeChain(t *testing.T, name string) []*x509.Certificate { t.Helper() certChain := make([]*x509.Certificate, 0) rest, err := os.ReadFile(name) if err != nil { t.Fatalf("os.ReadFile(%v) failed %v", name, err) } for len(rest) > 0 { var block *pem.Block block, rest = pem.Decode(rest) c, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("ParseCertificate error %v", err) } t.Logf("Parsed Cert sub = %v iss = %v", c.Subject, c.Issuer) certChain = append(certChain, c) } return certChain } func loadCRL(t *testing.T, path string) *CRL { crl, err := ReadCRLFile(path) if err != nil { t.Fatalf("ReadCRLFile(%v) failed err = %v", path, err) } return crl } func checkRevocation(conn tls.ConnectionState, cfg RevocationOptions) error { return checkChainRevocation(conn.VerifiedChains, cfg) } func TestVerifyCrl(t *testing.T) { tamperedSignature := loadCRL(t, testdata.Path("crl/1.crl")) // Change the signature so it won't verify tamperedSignature.certList.Signature[0]++ tamperedContent := loadCRL(t, testdata.Path("crl/provider_crl_empty.pem")) // Change the content so it won't find a match tamperedContent.rawIssuer[0]++ verifyTests := []struct { desc string crl *CRL certs []*x509.Certificate cert *x509.Certificate errWant string }{ { desc: "Pass intermediate", crl: loadCRL(t, testdata.Path("crl/1.crl")), certs: makeChain(t, testdata.Path("crl/unrevoked.pem")), cert: makeChain(t, testdata.Path("crl/unrevoked.pem"))[1], errWant: "", }, { desc: "Pass leaf", crl: loadCRL(t, testdata.Path("crl/2.crl")), certs: makeChain(t, testdata.Path("crl/unrevoked.pem")), cert: makeChain(t, testdata.Path("crl/unrevoked.pem"))[2], errWant: "", }, { desc: "Fail wrong cert chain", crl: loadCRL(t, testdata.Path("crl/3.crl")), certs: makeChain(t, testdata.Path("crl/unrevoked.pem")), cert: makeChain(t, testdata.Path("crl/revokedInt.pem"))[1], errWant: "No certificates matched", }, { desc: "Fail no certs", crl: loadCRL(t, testdata.Path("crl/1.crl")), certs: []*x509.Certificate{}, cert: makeChain(t, testdata.Path("crl/unrevoked.pem"))[1], errWant: "No certificates matched", }, { desc: "Fail Tampered signature", crl: tamperedSignature, certs: makeChain(t, testdata.Path("crl/unrevoked.pem")), cert: makeChain(t, testdata.Path("crl/unrevoked.pem"))[1], errWant: "verification failure", }, { desc: "Fail Tampered content", crl: tamperedContent, certs: makeChain(t, testdata.Path("crl/provider_client_trust_cert.pem")), cert: makeChain(t, testdata.Path("crl/provider_client_trust_cert.pem"))[0], errWant: "No certificates", }, { desc: "Fail CRL by malicious CA", crl: loadCRL(t, testdata.Path("crl/provider_malicious_crl_empty.pem")), certs: makeChain(t, testdata.Path("crl/provider_client_trust_cert.pem")), cert: makeChain(t, testdata.Path("crl/provider_client_trust_cert.pem"))[0], errWant: "verification error", }, { desc: "Fail KeyUsage without cRLSign bit", crl: loadCRL(t, testdata.Path("crl/provider_malicious_crl_empty.pem")), certs: makeChain(t, testdata.Path("crl/provider_malicious_client_trust_cert.pem")), cert: makeChain(t, testdata.Path("crl/provider_malicious_client_trust_cert.pem"))[0], errWant: "certificate can't be used", }, } for _, tt := range verifyTests { t.Run(tt.desc, func(t *testing.T) { err := verifyCRL(tt.crl, tt.certs) switch { case tt.errWant == "" && err != nil: t.Errorf("Valid CRL did not verify err = %v", err) case tt.errWant != "" && err == nil: t.Error("Invalid CRL verified") case tt.errWant != "" && !strings.Contains(err.Error(), tt.errWant): t.Errorf("fetchIssuerCRL(_, %v, %v, _) = %v; want Contains(%v)", tt.cert.RawIssuer, tt.certs, err, tt.errWant) } }) } } func TestRevokedCert(t *testing.T) { revokedIntChain := makeChain(t, testdata.Path("crl/revokedInt.pem")) revokedLeafChain := makeChain(t, testdata.Path("crl/revokedLeaf.pem")) validChain := makeChain(t, testdata.Path("crl/unrevoked.pem")) rawCRLs := make([][]byte, 6) for i := 1; i <= 6; i++ { rawCRL, err := os.ReadFile(testdata.Path(fmt.Sprintf("crl/%d.crl", i))) if err != nil { t.Fatalf("readFile(%v) failed err = %v", fmt.Sprintf("crl/%d.crl", i), err) } rawCRLs = append(rawCRLs, rawCRL) } staticCRLProvider := NewStaticCRLProvider(rawCRLs) directoryCRLProvider, err := NewFileWatcherCRLProvider(FileWatcherOptions{CRLDirectory: testdata.Path("crl")}) if err != nil { t.Fatalf("NewFileWatcherCRLProvider: err = %v", err) } defer directoryCRLProvider.Close() var revocationTests = []struct { desc string in tls.ConnectionState revoked bool denyUndetermined bool }{ { desc: "Single unrevoked", in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{validChain}}, revoked: false, }, { desc: "Single revoked intermediate", in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{revokedIntChain}}, revoked: true, }, { desc: "Single revoked leaf", in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{revokedLeafChain}}, revoked: true, }, { desc: "Multi one revoked", in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{validChain, revokedLeafChain}}, revoked: false, }, { desc: "Multi revoked", in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{revokedLeafChain, revokedIntChain}}, revoked: true, }, { desc: "Multi unrevoked", in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{validChain, validChain}}, revoked: false, }, { desc: "Undetermined revoked", in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{ {&x509.Certificate{CRLDistributionPoints: []string{"test"}}}, }}, revoked: true, denyUndetermined: true, }, { desc: "Undetermined allowed", in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{ {&x509.Certificate{CRLDistributionPoints: []string{"test"}}}, }}, revoked: false, }, } for _, tt := range revocationTests { t.Run(fmt.Sprintf("%v with x509 crl dir", tt.desc), func(t *testing.T) { err := checkRevocation(tt.in, RevocationOptions{ CRLProvider: directoryCRLProvider, DenyUndetermined: tt.denyUndetermined, }) t.Logf("checkRevocation err = %v", err) if tt.revoked && err == nil { t.Error("Revoked certificate chain was allowed") } else if !tt.revoked && err != nil { t.Error("Unrevoked certificate not allowed") } }) t.Run(fmt.Sprintf("%v with static provider", tt.desc), func(t *testing.T) { err := checkRevocation(tt.in, RevocationOptions{ DenyUndetermined: tt.denyUndetermined, CRLProvider: staticCRLProvider, }) t.Logf("checkRevocation err = %v", err) if tt.revoked && err == nil { t.Error("Revoked certificate chain was allowed") } else if !tt.revoked && err != nil { t.Error("Unrevoked certificate not allowed") } }) } } func setupTLSConn(t *testing.T) (net.Listener, *x509.Certificate, *ecdsa.PrivateKey) { t.Helper() templ := x509.Certificate{ SerialNumber: big.NewInt(5), BasicConstraintsValid: true, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), IsCA: true, Subject: pkix.Name{CommonName: "test-cert"}, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, IPAddresses: []net.IP{netip.MustParseAddr("::1").AsSlice()}, CRLDistributionPoints: []string{"http://static.corp.google.com/crl/campus-sln/borg"}, } key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("ecdsa.GenerateKey failed err = %v", err) } rawCert, err := x509.CreateCertificate(rand.Reader, &templ, &templ, key.Public(), key) if err != nil { t.Fatalf("x509.CreateCertificate failed err = %v", err) } cert, err := x509.ParseCertificate(rawCert) if err != nil { t.Fatalf("x509.ParseCertificate failed err = %v", err) } srvCfg := tls.Config{ Certificates: []tls.Certificate{ { Certificate: [][]byte{cert.Raw}, PrivateKey: key, }, }, } l, err := tls.Listen("tcp6", "[::1]:0", &srvCfg) if err != nil { t.Fatalf("tls.Listen failed err = %v", err) } return l, cert, key } // TestVerifyConnection will setup a client/server connection and check revocation in the real TLS dialer func TestVerifyConnection(t *testing.T) { lis, cert, key := setupTLSConn(t) defer func() { lis.Close() }() var handshakeTests = []struct { desc string revoked []pkix.RevokedCertificate success bool }{ { desc: "Empty CRL", revoked: []pkix.RevokedCertificate{}, success: true, }, { desc: "Revoked Cert", revoked: []pkix.RevokedCertificate{ { SerialNumber: cert.SerialNumber, RevocationTime: time.Now(), }, }, success: false, }, } for _, tt := range handshakeTests { t.Run(tt.desc, func(t *testing.T) { // Accept one connection. go func() { conn, err := lis.Accept() if err != nil { t.Errorf("tls.Accept failed err = %v", err) } else { conn.Write([]byte("Hello, World!")) conn.Close() } }() dir, err := os.MkdirTemp("", "crl_dir") if err != nil { t.Fatalf("os.MkdirTemp failed err = %v", err) } defer os.RemoveAll(dir) template := &x509.RevocationList{ RevokedCertificates: tt.revoked, ThisUpdate: time.Now(), NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(1), } crl, err := x509.CreateRevocationList(rand.Reader, template, cert, key) if err != nil { t.Fatalf("templ.CreateRevocationList failed err = %v", err) } err = os.WriteFile(path.Join(dir, fmt.Sprintf("%s.r0", cert.Subject.ToRDNSequence())), crl, 0777) if err != nil { t.Fatalf("os.WriteFile failed err = %v", err) } cp := x509.NewCertPool() cp.AddCert(cert) provider, err := NewFileWatcherCRLProvider(FileWatcherOptions{CRLDirectory: dir}) if err != nil { t.Errorf("NewFileWatcherCRLProvider: err = %v", err) } defer provider.Close() cliCfg := tls.Config{ RootCAs: cp, VerifyConnection: func(cs tls.ConnectionState) error { return checkRevocation(cs, RevocationOptions{CRLProvider: provider}) }, } conn, err := tls.Dial(lis.Addr().Network(), lis.Addr().String(), &cliCfg) t.Logf("tls.Dial err = %v", err) if tt.success && err != nil { t.Errorf("Expected success got err = %v", err) } if !tt.success && err == nil { t.Error("Expected error, but got success") } if err == nil { conn.Close() } }) } }