| /* |
| * |
| * 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/hex" |
| "encoding/pem" |
| "fmt" |
| "math/big" |
| "net" |
| "os" |
| "path" |
| "strings" |
| "testing" |
| "time" |
| |
| lru "github.com/hashicorp/golang-lru" |
| "google.golang.org/grpc/security/advancedtls/testdata" |
| ) |
| |
| func TestX509NameHash(t *testing.T) { |
| nameTests := []struct { |
| in pkix.Name |
| out string |
| }{ |
| { |
| in: pkix.Name{ |
| Country: []string{"US"}, |
| Organization: []string{"Example"}, |
| }, |
| out: "9cdd41ff", |
| }, |
| { |
| in: pkix.Name{ |
| Country: []string{"us"}, |
| Organization: []string{"example"}, |
| }, |
| out: "9cdd41ff", |
| }, |
| { |
| in: pkix.Name{ |
| Country: []string{" us"}, |
| Organization: []string{"example"}, |
| }, |
| out: "9cdd41ff", |
| }, |
| { |
| in: pkix.Name{ |
| Country: []string{"US"}, |
| Province: []string{"California"}, |
| Locality: []string{"Mountain View"}, |
| Organization: []string{"BoringSSL"}, |
| }, |
| out: "c24414d9", |
| }, |
| { |
| in: pkix.Name{ |
| Country: []string{"US"}, |
| Province: []string{"California"}, |
| Locality: []string{"Mountain View"}, |
| Organization: []string{"BoringSSL"}, |
| }, |
| out: "c24414d9", |
| }, |
| { |
| in: pkix.Name{ |
| SerialNumber: "87f4514475ba0a2b", |
| }, |
| out: "9dc713cd", |
| }, |
| { |
| in: pkix.Name{ |
| Country: []string{"US"}, |
| Province: []string{"California"}, |
| Locality: []string{"Mountain View"}, |
| Organization: []string{"Google LLC"}, |
| OrganizationalUnit: []string{"Production", "campus-sln"}, |
| CommonName: "Root CA (2021-02-02T07:30:36-08:00)", |
| }, |
| out: "0b35a562", |
| }, |
| { |
| in: pkix.Name{ |
| ExtraNames: []pkix.AttributeTypeAndValue{ |
| {Type: asn1.ObjectIdentifier{5, 5, 5, 5}, Value: "aaaa"}, |
| }, |
| }, |
| out: "eea339da", |
| }, |
| } |
| for _, tt := range nameTests { |
| t.Run(tt.in.String(), func(t *testing.T) { |
| h := x509NameHash(tt.in.ToRDNSequence()) |
| if h != tt.out { |
| t.Errorf("x509NameHash(%v): Got %v wanted %v", tt.in, h, tt.out) |
| } |
| }) |
| } |
| } |
| |
| 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 := x509.ParseCRL(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 := x509.ParseCRL(dummyCrlFile) |
| if err != nil { |
| t.Fatalf("x509.ParseCRL(dummyCrlFile) failed: %v", err) |
| } |
| crlExt := &certificateListExt{CertList: crl} |
| var crlIssuer pkix.Name |
| crlIssuer.FillFromRDNSequence(&crl.TBSCertList.Issuer) |
| |
| 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: crlIssuer, |
| 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) *certificateListExt { |
| b, err := os.ReadFile(path) |
| if err != nil { |
| t.Fatalf("readFile(%v) failed err = %v", path, err) |
| } |
| crl, err := x509.ParseCRL(b) |
| if err != nil { |
| t.Fatalf("ParseCrl(%v) failed err = %v", path, err) |
| } |
| crlExt, err := parseCRLExtensions(crl) |
| if err != nil { |
| t.Fatalf("parseCRLExtensions(%v) failed err = %v", path, err) |
| } |
| crlExt.RawIssuer, err = extractCRLIssuer(b) |
| if err != nil { |
| t.Fatalf("extractCRLIssuer(%v) failed err= %v", path, err) |
| } |
| return crlExt |
| } |
| |
| func TestCachedCRL(t *testing.T) { |
| cache, err := lru.New(5) |
| if err != nil { |
| t.Fatalf("lru.New: err = %v", err) |
| } |
| |
| tests := []struct { |
| desc string |
| val interface{} |
| ok bool |
| }{ |
| { |
| desc: "Valid", |
| val: &certificateListExt{ |
| CertList: &pkix.CertificateList{ |
| TBSCertList: pkix.TBSCertificateList{ |
| NextUpdate: time.Now().Add(time.Hour), |
| }, |
| }}, |
| ok: true, |
| }, |
| { |
| desc: "Expired", |
| val: &certificateListExt{ |
| CertList: &pkix.CertificateList{ |
| TBSCertList: pkix.TBSCertificateList{ |
| NextUpdate: time.Now().Add(-time.Hour), |
| }, |
| }}, |
| ok: false, |
| }, |
| { |
| desc: "Wrong Type", |
| val: "string", |
| ok: false, |
| }, |
| { |
| desc: "Empty", |
| val: nil, |
| ok: false, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.desc, func(t *testing.T) { |
| if tt.val != nil { |
| cache.Add(hex.EncodeToString([]byte(tt.desc)), tt.val) |
| } |
| _, ok := cachedCrl([]byte(tt.desc), cache) |
| if tt.ok != ok { |
| t.Errorf("Cache ok error expected %v vs %v", tt.ok, ok) |
| } |
| }) |
| } |
| } |
| |
| func TestGetIssuerCRLCache(t *testing.T) { |
| cache, err := lru.New(5) |
| if err != nil { |
| t.Fatalf("lru.New: err = %v", err) |
| } |
| |
| tests := []struct { |
| desc string |
| rawIssuer []byte |
| certs []*x509.Certificate |
| }{ |
| { |
| desc: "Valid", |
| rawIssuer: makeChain(t, testdata.Path("crl/unrevoked.pem"))[1].RawIssuer, |
| certs: makeChain(t, testdata.Path("crl/unrevoked.pem")), |
| }, |
| { |
| desc: "Unverified", |
| rawIssuer: makeChain(t, testdata.Path("crl/unrevoked.pem"))[1].RawIssuer, |
| }, |
| { |
| desc: "Not Found", |
| rawIssuer: []byte("not_found"), |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.desc, func(t *testing.T) { |
| cache.Purge() |
| _, err := fetchIssuerCRL(tt.rawIssuer, tt.certs, RevocationConfig{ |
| RootDir: testdata.Path("."), |
| Cache: cache, |
| }) |
| if err == nil && cache.Len() == 0 { |
| t.Error("Verified CRL not added to cache") |
| } |
| if err != nil && cache.Len() != 0 { |
| t.Error("Unverified CRL added to cache") |
| } |
| }) |
| } |
| } |
| |
| func TestVerifyCrl(t *testing.T) { |
| tampered := loadCRL(t, testdata.Path("crl/1.crl")) |
| // Change the signature so it won't verify |
| tampered.CertList.SignatureValue.Bytes[0]++ |
| |
| verifyTests := []struct { |
| desc string |
| crl *certificateListExt |
| 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 mached", |
| }, |
| { |
| 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 mached", |
| }, |
| { |
| desc: "Fail Tampered signature", |
| crl: tampered, |
| certs: makeChain(t, testdata.Path("crl/unrevoked.pem")), |
| cert: makeChain(t, testdata.Path("crl/unrevoked.pem"))[1], |
| errWant: "verification failure", |
| }, |
| } |
| |
| for _, tt := range verifyTests { |
| t.Run(tt.desc, func(t *testing.T) { |
| err := verifyCRL(tt.crl, tt.cert.RawIssuer, 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")) |
| cache, err := lru.New(5) |
| if err != nil { |
| t.Fatalf("lru.New: err = %v", err) |
| } |
| |
| var revocationTests = []struct { |
| desc string |
| in tls.ConnectionState |
| revoked bool |
| allowUndetermined 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, |
| }, |
| { |
| desc: "Undetermined allowed", |
| in: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{ |
| {&x509.Certificate{CRLDistributionPoints: []string{"test"}}}, |
| }}, |
| revoked: false, |
| allowUndetermined: true, |
| }, |
| } |
| |
| for _, tt := range revocationTests { |
| t.Run(tt.desc, func(t *testing.T) { |
| err := CheckRevocation(tt.in, RevocationConfig{ |
| RootDir: testdata.Path("crl"), |
| AllowUndetermined: tt.allowUndetermined, |
| Cache: cache, |
| }) |
| 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, |
| ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, |
| IPAddresses: []net.IP{net.ParseIP("::1")}, |
| 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) |
| |
| crl, err := cert.CreateCRL(rand.Reader, key, tt.revoked, time.Now(), time.Now().Add(time.Hour)) |
| if err != nil { |
| t.Fatalf("templ.CreateCRL failed err = %v", err) |
| } |
| |
| err = os.WriteFile(path.Join(dir, fmt.Sprintf("%s.r0", x509NameHash(cert.Subject.ToRDNSequence()))), crl, 0777) |
| if err != nil { |
| t.Fatalf("os.WriteFile failed err = %v", err) |
| } |
| |
| cp := x509.NewCertPool() |
| cp.AddCert(cert) |
| cliCfg := tls.Config{ |
| RootCAs: cp, |
| VerifyConnection: func(cs tls.ConnectionState) error { |
| return CheckRevocation(cs, RevocationConfig{RootDir: dir}) |
| }, |
| } |
| 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() |
| } |
| }) |
| } |
| } |
| |
| func TestIssuerNonPrintableString(t *testing.T) { |
| rawIssuer, err := hex.DecodeString("300c310a300806022a030c023a29") |
| if err != nil { |
| t.Fatalf("failed to decode issuer: %s", err) |
| } |
| _, err = fetchCRL(rawIssuer, RevocationConfig{RootDir: testdata.Path("crl")}) |
| if err != nil { |
| t.Fatalf("fetchCRL failed: %s", err) |
| } |
| } |
| |
| // TestCRLCacheExpirationReloading tests the basic expiration and reloading of a |
| // cached CRL. The setup places an empty CRL in the cache, and a corresponding |
| // CRL with a revocation in the CRL directory. We then validate the certificate |
| // to verify that the certificate is not revoked. Then, we modify the |
| // NextUpdate time to be in the past so that when we next check for revocation, |
| // the existing cache entry should be seen as expired, and the CRL in the |
| // directory showing `revokedInt.pem` as revoked will be loaded, resulting in |
| // the check returning `RevocationRevoked`. |
| func TestCRLCacheExpirationReloading(t *testing.T) { |
| cache, err := lru.New(5) |
| if err != nil { |
| t.Fatalf("Creating cache failed") |
| } |
| |
| var certs = makeChain(t, testdata.Path("crl/revokedInt.pem")) |
| // Certs[1] has the same issuer as the revoked cert |
| rawIssuer := certs[1].RawIssuer |
| |
| // `3.crl`` revokes `revokedInt.pem` |
| crl := loadCRL(t, testdata.Path("crl/3.crl")) |
| // Modify the crl so that the cert is NOT revoked and add it to the cache |
| crl.CertList.TBSCertList.RevokedCertificates = nil |
| crl.CertList.TBSCertList.NextUpdate = time.Now().Add(time.Hour) |
| cache.Add(hex.EncodeToString(rawIssuer), crl) |
| var cfg = RevocationConfig{RootDir: testdata.Path("crl"), Cache: cache} |
| revocationStatus := checkChain(certs, cfg) |
| if revocationStatus != RevocationUnrevoked { |
| t.Fatalf("Certificate check should be RevocationUnrevoked, was %v", revocationStatus) |
| } |
| |
| // Modify the entry in the cache so that the cache will be refreshed |
| crl.CertList.TBSCertList.NextUpdate = time.Now() |
| cache.Add(hex.EncodeToString(rawIssuer), crl) |
| |
| revocationStatus = checkChain(certs, cfg) |
| if revocationStatus != RevocationRevoked { |
| t.Fatalf("A certificate should have been `RevocationRevoked` but was %v", revocationStatus) |
| } |
| } |