blob: 65ef3ca1b738d9cd2c18a9bccaf27bc97daf05b1 [file] [log] [blame]
/*
*
* 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)
}
}