mirror of https://github.com/grpc/grpc-go.git
773 lines
22 KiB
Go
773 lines
22 KiB
Go
/*
|
|
*
|
|
* 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)
|
|
}
|
|
}
|