blob: e3cac66c07f1142ca2caa69d7169dffc94bcca3d [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 (
"bytes"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/binary"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/crypto/cryptobyte"
cbasn1 "golang.org/x/crypto/cryptobyte/asn1"
"google.golang.org/grpc/grpclog"
)
var grpclogLogger = grpclog.Component("advancedtls")
// Cache is an interface to cache CRL files.
// The cache implementation must be concurrency safe.
// A fixed size lru cache from golang-lru is recommended.
type Cache interface {
// Add adds a value to the cache.
Add(key, value interface{}) bool
// Get looks up a key's value from the cache.
Get(key interface{}) (value interface{}, ok bool)
}
// RevocationConfig contains options for CRL lookup.
type RevocationConfig struct {
// RootDir is the directory to search for CRL files.
// Directory format must match OpenSSL X509_LOOKUP_hash_dir(3).
RootDir string
// AllowUndetermined controls if certificate chains with RevocationUndetermined
// revocation status are allowed to complete.
AllowUndetermined bool
// Cache will store CRL files if not nil, otherwise files are reloaded for every lookup.
Cache Cache
}
// RevocationStatus is the revocation status for a certificate or chain.
type RevocationStatus int
const (
// RevocationUndetermined means we couldn't find or verify a CRL for the cert.
RevocationUndetermined RevocationStatus = iota
// RevocationUnrevoked means we found the CRL for the cert and the cert is not revoked.
RevocationUnrevoked
// RevocationRevoked means we found the CRL and the cert is revoked.
RevocationRevoked
)
func (s RevocationStatus) String() string {
return [...]string{"RevocationUndetermined", "RevocationUnrevoked", "RevocationRevoked"}[s]
}
// certificateListExt contains a pkix.CertificateList and parsed
// extensions that aren't provided by the golang CRL parser.
type certificateListExt struct {
CertList *pkix.CertificateList
// RFC5280, 5.2.1, all conforming CRLs must have a AKID with the ID method.
AuthorityKeyID []byte
RawIssuer []byte
}
const tagDirectoryName = 4
var (
// RFC5280, 5.2.4 id-ce-deltaCRLIndicator OBJECT IDENTIFIER ::= { id-ce 27 }
oidDeltaCRLIndicator = asn1.ObjectIdentifier{2, 5, 29, 27}
// RFC5280, 5.2.5 id-ce-issuingDistributionPoint OBJECT IDENTIFIER ::= { id-ce 28 }
oidIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28}
// RFC5280, 5.3.3 id-ce-certificateIssuer OBJECT IDENTIFIER ::= { id-ce 29 }
oidCertificateIssuer = asn1.ObjectIdentifier{2, 5, 29, 29}
// RFC5290, 4.2.1.1 id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
)
// x509NameHash implements the OpenSSL X509_NAME_hash function for hashed directory lookups.
//
// NOTE: due to the behavior of asn1.Marshal, if the original encoding of the RDN sequence
// contains strings which do not use the ASN.1 PrintableString type, the name will not be
// re-encoded using those types, resulting in a hash which does not match that produced
// by OpenSSL.
func x509NameHash(r pkix.RDNSequence) string {
var canonBytes []byte
// First, canonicalize all the strings.
for _, rdnSet := range r {
for i, rdn := range rdnSet {
value, ok := rdn.Value.(string)
if !ok {
continue
}
// OpenSSL trims all whitespace, does a tolower, and removes extra spaces between words.
// Implemented in x509_name_canon in OpenSSL
canonStr := strings.Join(strings.Fields(
strings.TrimSpace(strings.ToLower(value))), " ")
// Then it changes everything to UTF8 strings
rdnSet[i].Value = asn1.RawValue{Tag: asn1.TagUTF8String, Bytes: []byte(canonStr)}
}
}
// Finally, OpenSSL drops the initial sequence tag
// so we marshal all the RDNs separately instead of as a group.
for _, canonRdn := range r {
b, err := asn1.Marshal(canonRdn)
if err != nil {
continue
}
canonBytes = append(canonBytes, b...)
}
issuerHash := sha1.Sum(canonBytes)
// Openssl takes the first 4 bytes and encodes them as a little endian
// uint32 and then uses the hex to make the file name.
// In C++, this would be:
// (((unsigned long)md[0]) | ((unsigned long)md[1] << 8L) |
// ((unsigned long)md[2] << 16L) | ((unsigned long)md[3] << 24L)
// ) & 0xffffffffL;
fileHash := binary.LittleEndian.Uint32(issuerHash[0:4])
return fmt.Sprintf("%08x", fileHash)
}
// CheckRevocation checks the connection for revoked certificates based on RFC5280.
// This implementation has the following major limitations:
// - Indirect CRL files are not supported.
// - CRL loading is only supported from directories in the X509_LOOKUP_hash_dir format.
// - OnlySomeReasons is not supported.
// - Delta CRL files are not supported.
// - Certificate CRLDistributionPoint must be URLs, but are then ignored and converted into a file path.
// - CRL checks are done after path building, which goes against RFC4158.
func CheckRevocation(conn tls.ConnectionState, cfg RevocationConfig) error {
return CheckChainRevocation(conn.VerifiedChains, cfg)
}
// CheckChainRevocation checks the verified certificate chain
// for revoked certificates based on RFC5280.
func CheckChainRevocation(verifiedChains [][]*x509.Certificate, cfg RevocationConfig) error {
// Iterate the verified chains looking for one that is RevocationUnrevoked.
// A single RevocationUnrevoked chain is enough to allow the connection, and a single RevocationRevoked
// chain does not mean the connection should fail.
count := make(map[RevocationStatus]int)
for _, chain := range verifiedChains {
switch checkChain(chain, cfg) {
case RevocationUnrevoked:
// If any chain is RevocationUnrevoked then return no error.
return nil
case RevocationRevoked:
// If this chain is revoked, keep looking for another chain.
count[RevocationRevoked]++
continue
case RevocationUndetermined:
if cfg.AllowUndetermined {
return nil
}
count[RevocationUndetermined]++
continue
}
}
return fmt.Errorf("no unrevoked chains found: %v", count)
}
// checkChain will determine and check all certificates in chain against the CRL
// defined in the certificate with the following rules:
// 1. If any certificate is RevocationRevoked, return RevocationRevoked.
// 2. If any certificate is RevocationUndetermined, return RevocationUndetermined.
// 3. If all certificates are RevocationUnrevoked, return RevocationUnrevoked.
func checkChain(chain []*x509.Certificate, cfg RevocationConfig) RevocationStatus {
chainStatus := RevocationUnrevoked
for _, c := range chain {
switch checkCert(c, chain, cfg) {
case RevocationRevoked:
// Easy case, if a cert in the chain is revoked, the chain is revoked.
return RevocationRevoked
case RevocationUndetermined:
// If we couldn't find the revocation status for a cert, the chain is at best RevocationUndetermined
// keep looking to see if we find a cert in the chain that's RevocationRevoked,
// but return RevocationUndetermined at a minimum.
chainStatus = RevocationUndetermined
case RevocationUnrevoked:
// Continue iterating up the cert chain.
continue
}
}
return chainStatus
}
func cachedCrl(rawIssuer []byte, cache Cache) (*certificateListExt, bool) {
val, ok := cache.Get(hex.EncodeToString(rawIssuer))
if !ok {
return nil, false
}
crl, ok := val.(*certificateListExt)
if !ok {
return nil, false
}
// If the CRL is expired, force a reload.
if crl.CertList.HasExpired(time.Now()) {
return nil, false
}
return crl, true
}
// fetchIssuerCRL fetches and verifies the CRL for rawIssuer from disk or cache if configured in cfg.
func fetchIssuerCRL(rawIssuer []byte, crlVerifyCrt []*x509.Certificate, cfg RevocationConfig) (*certificateListExt, error) {
if cfg.Cache != nil {
if crl, ok := cachedCrl(rawIssuer, cfg.Cache); ok {
return crl, nil
}
}
crl, err := fetchCRL(rawIssuer, cfg)
if err != nil {
return nil, fmt.Errorf("fetchCRL() failed: %v", err)
}
if err := verifyCRL(crl, rawIssuer, crlVerifyCrt); err != nil {
return nil, fmt.Errorf("verifyCRL() failed: %v", err)
}
if cfg.Cache != nil {
cfg.Cache.Add(hex.EncodeToString(rawIssuer), crl)
}
return crl, nil
}
// checkCert checks a single certificate against the CRL defined in the certificate.
// It will fetch and verify the CRL(s) defined in the root directory specified by cfg.
// If we can't load any authoritative CRL files, the status is RevocationUndetermined.
// c is the certificate to check.
// crlVerifyCrt is the group of possible certificates to verify the crl.
func checkCert(c *x509.Certificate, crlVerifyCrt []*x509.Certificate, cfg RevocationConfig) RevocationStatus {
crl, err := fetchIssuerCRL(c.RawIssuer, crlVerifyCrt, cfg)
if err != nil {
// We couldn't load any CRL files for the certificate, so we don't know if it's RevocationUnrevoked or not.
grpclogLogger.Warningf("getIssuerCRL(%v) err = %v", c.Issuer, err)
return RevocationUndetermined
}
revocation, err := checkCertRevocation(c, crl)
if err != nil {
grpclogLogger.Warningf("checkCertRevocation(CRL %v) failed: %v", crl.CertList.TBSCertList.Issuer, err)
// We couldn't check the CRL file for some reason, so we don't know if it's RevocationUnrevoked or not.
return RevocationUndetermined
}
// Here we've gotten a CRL that loads and verifies.
// We only handle all-reasons CRL files, so this file
// is authoritative for the certificate.
return revocation
}
func checkCertRevocation(c *x509.Certificate, crl *certificateListExt) (RevocationStatus, error) {
// Per section 5.3.3 we prime the certificate issuer with the CRL issuer.
// Subsequent entries use the previous entry's issuer.
rawEntryIssuer := crl.RawIssuer
// Loop through all the revoked certificates.
for _, revCert := range crl.CertList.TBSCertList.RevokedCertificates {
// 5.3 Loop through CRL entry extensions for needed information.
for _, ext := range revCert.Extensions {
if oidCertificateIssuer.Equal(ext.Id) {
extIssuer, err := parseCertIssuerExt(ext)
if err != nil {
grpclogLogger.Info(err)
if ext.Critical {
return RevocationUndetermined, err
}
// Since this is a non-critical extension, we can skip it even though
// there was a parsing failure.
continue
}
rawEntryIssuer = extIssuer
} else if ext.Critical {
return RevocationUndetermined, fmt.Errorf("checkCertRevocation: Unhandled critical extension: %v", ext.Id)
}
}
// If the issuer and serial number appear in the CRL, the certificate is revoked.
if bytes.Equal(c.RawIssuer, rawEntryIssuer) && c.SerialNumber.Cmp(revCert.SerialNumber) == 0 {
// CRL contains the serial, so return revoked.
return RevocationRevoked, nil
}
}
// We did not find the serial in the CRL file that was valid for the cert
// so the certificate is not revoked.
return RevocationUnrevoked, nil
}
func parseCertIssuerExt(ext pkix.Extension) ([]byte, error) {
// 5.3.3 Certificate Issuer
// CertificateIssuer ::= GeneralNames
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
var generalNames []asn1.RawValue
if rest, err := asn1.Unmarshal(ext.Value, &generalNames); err != nil || len(rest) != 0 {
return nil, fmt.Errorf("asn1.Unmarshal failed: %v", err)
}
for _, generalName := range generalNames {
// GeneralName ::= CHOICE {
// otherName [0] OtherName,
// rfc822Name [1] IA5String,
// dNSName [2] IA5String,
// x400Address [3] ORAddress,
// directoryName [4] Name,
// ediPartyName [5] EDIPartyName,
// uniformResourceIdentifier [6] IA5String,
// iPAddress [7] OCTET STRING,
// registeredID [8] OBJECT IDENTIFIER }
if generalName.Tag == tagDirectoryName {
return generalName.Bytes, nil
}
}
// Conforming CRL issuers MUST include in this extension the
// distinguished name (DN) from the issuer field of the certificate that
// corresponds to this CRL entry.
// If we couldn't get a directoryName, we can't reason about this file so cert status is
// RevocationUndetermined.
return nil, errors.New("no DN found in certificate issuer")
}
// RFC 5280, 4.2.1.1
type authKeyID struct {
ID []byte `asn1:"optional,tag:0"`
}
// RFC5280, 5.2.5
// id-ce-issuingDistributionPoint OBJECT IDENTIFIER ::= { id-ce 28 }
// IssuingDistributionPoint ::= SEQUENCE {
// distributionPoint [0] DistributionPointName OPTIONAL,
// onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE,
// onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE,
// onlySomeReasons [3] ReasonFlags OPTIONAL,
// indirectCRL [4] BOOLEAN DEFAULT FALSE,
// onlyContainsAttributeCerts [5] BOOLEAN DEFAULT FALSE }
// -- at most one of onlyContainsUserCerts, onlyContainsCACerts,
// -- and onlyContainsAttributeCerts may be set to TRUE.
type issuingDistributionPoint struct {
DistributionPoint asn1.RawValue `asn1:"optional,tag:0"`
OnlyContainsUserCerts bool `asn1:"optional,tag:1"`
OnlyContainsCACerts bool `asn1:"optional,tag:2"`
OnlySomeReasons asn1.BitString `asn1:"optional,tag:3"`
IndirectCRL bool `asn1:"optional,tag:4"`
OnlyContainsAttributeCerts bool `asn1:"optional,tag:5"`
}
// parseCRLExtensions parses the extensions for a CRL
// and checks that they're supported by the parser.
func parseCRLExtensions(c *pkix.CertificateList) (*certificateListExt, error) {
if c == nil {
return nil, errors.New("c is nil, expected any value")
}
certList := &certificateListExt{CertList: c}
for _, ext := range c.TBSCertList.Extensions {
switch {
case oidDeltaCRLIndicator.Equal(ext.Id):
return nil, fmt.Errorf("delta CRLs unsupported")
case oidAuthorityKeyIdentifier.Equal(ext.Id):
var a authKeyID
if rest, err := asn1.Unmarshal(ext.Value, &a); err != nil {
return nil, fmt.Errorf("asn1.Unmarshal failed: %v", err)
} else if len(rest) != 0 {
return nil, errors.New("trailing data after AKID extension")
}
certList.AuthorityKeyID = a.ID
case oidIssuingDistributionPoint.Equal(ext.Id):
var dp issuingDistributionPoint
if rest, err := asn1.Unmarshal(ext.Value, &dp); err != nil {
return nil, fmt.Errorf("asn1.Unmarshal failed: %v", err)
} else if len(rest) != 0 {
return nil, errors.New("trailing data after IssuingDistributionPoint extension")
}
if dp.OnlyContainsUserCerts || dp.OnlyContainsCACerts || dp.OnlyContainsAttributeCerts {
return nil, errors.New("CRL only contains some certificate types")
}
if dp.IndirectCRL {
return nil, errors.New("indirect CRLs unsupported")
}
if dp.OnlySomeReasons.BitLength != 0 {
return nil, errors.New("onlySomeReasons unsupported")
}
case ext.Critical:
return nil, fmt.Errorf("unsupported critical extension: %v", ext.Id)
}
}
if len(certList.AuthorityKeyID) == 0 {
return nil, errors.New("authority key identifier extension missing")
}
return certList, nil
}
func fetchCRL(rawIssuer []byte, cfg RevocationConfig) (*certificateListExt, error) {
var parsedCRL *certificateListExt
// 6.3.3 (a) (1) (ii)
// According to X509_LOOKUP_hash_dir the format is issuer_hash.rN where N is an increasing number.
// There are no gaps, so we break when we can't find a file.
for i := 0; ; i++ {
// Unmarshal to RDNSeqence according to http://go/godoc/crypto/x509/pkix/#Name.
var r pkix.RDNSequence
rest, err := asn1.Unmarshal(rawIssuer, &r)
if len(rest) != 0 || err != nil {
return nil, fmt.Errorf("asn1.Unmarshal(Issuer) len(rest) = %d failed: %v", len(rest), err)
}
crlPath := fmt.Sprintf("%s.r%d", filepath.Join(cfg.RootDir, x509NameHash(r)), i)
crlBytes, err := os.ReadFile(crlPath)
if err != nil {
// Break when we can't read a CRL file.
grpclogLogger.Infof("readFile: %v", err)
break
}
crl, err := x509.ParseCRL(crlBytes)
if err != nil {
// Parsing errors for a CRL shouldn't happen so fail.
return nil, fmt.Errorf("x509.ParseCrl(%v) failed: %v", crlPath, err)
}
var certList *certificateListExt
if certList, err = parseCRLExtensions(crl); err != nil {
grpclogLogger.Infof("fetchCRL: unsupported crl %v: %v", crlPath, err)
// Continue to find a supported CRL
continue
}
rawCRLIssuer, err := extractCRLIssuer(crlBytes)
if err != nil {
return nil, err
}
certList.RawIssuer = rawCRLIssuer
// RFC5280, 6.3.3 (b) Verify the issuer and scope of the complete CRL.
if bytes.Equal(rawIssuer, rawCRLIssuer) {
parsedCRL = certList
// Continue to find the highest number in the .rN suffix.
continue
}
}
if parsedCRL == nil {
return nil, fmt.Errorf("fetchCrls no CRLs found for issuer")
}
return parsedCRL, nil
}
func verifyCRL(crl *certificateListExt, rawIssuer []byte, chain []*x509.Certificate) error {
// RFC5280, 6.3.3 (f) Obtain and validateate the certification path for the issuer of the complete CRL
// We intentionally limit our CRLs to be signed with the same certificate path as the certificate
// so we can use the chain from the connection.
for _, c := range chain {
// Use the key where the subject and KIDs match.
// This departs from RFC4158, 3.5.12 which states that KIDs
// cannot eliminate certificates, but RFC5280, 5.2.1 states that
// "Conforming CRL issuers MUST use the key identifier method, and MUST
// include this extension in all CRLs issued."
// So, this is much simpler than RFC4158 and should be compatible.
if bytes.Equal(c.SubjectKeyId, crl.AuthorityKeyID) && bytes.Equal(c.RawSubject, crl.RawIssuer) {
// RFC5280, 6.3.3 (g) Validate signature.
return c.CheckCRLSignature(crl.CertList)
}
}
return fmt.Errorf("verifyCRL: No certificates mached CRL issuer (%v)", crl.CertList.TBSCertList.Issuer)
}
var crlPemPrefix = []byte("-----BEGIN X509 CRL")
// extractCRLIssuer extracts the raw ASN.1 encoding of the CRL issuer. Due to the design of
// pkix.CertificateList and pkix.RDNSequence, it is not possible to reliably marshal the
// parsed Issuer to it's original raw encoding.
func extractCRLIssuer(crlBytes []byte) ([]byte, error) {
if bytes.HasPrefix(crlBytes, crlPemPrefix) {
block, _ := pem.Decode(crlBytes)
if block != nil && block.Type == "X509 CRL" {
crlBytes = block.Bytes
}
}
der := cryptobyte.String(crlBytes)
var issuer cryptobyte.String
if !der.ReadASN1(&der, cbasn1.SEQUENCE) ||
!der.ReadASN1(&der, cbasn1.SEQUENCE) ||
!der.SkipOptionalASN1(cbasn1.INTEGER) ||
!der.SkipASN1(cbasn1.SEQUENCE) ||
!der.ReadASN1Element(&issuer, cbasn1.SEQUENCE) {
return nil, errors.New("extractCRLIssuer: invalid ASN.1 encoding")
}
return issuer, nil
}