| /* |
| * |
| * 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 |
| } |