certificate/constraints_v29.go (125 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* The following code is adapted from code from: * https://github.com/mozilla/tls-observatory/blob/7bc42856d2e5594614b56c2f55baf42bb9751b3d/certificate/constraints/constraints.go * https://github.com/jcjones/gx509/blob/master/gx509/technicalconstraints.go */ package main import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "net" "time" "golang.org/x/crypto/cryptobyte" cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" ) var oidPrecertificateSigningCertificate = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 4} // IsTechnicallyConstrainedMozPolicyV29 determines if a given certificate is "considered technically constrained" according to the following requirements: // - Mozilla Root Store Policy (MRSP) v2.9 (https://github.com/mozilla/pkipolicy/blob/2.9/rootstore/policy.md) // - CABForum TLS Baseline Requirements (TLSBR) v2.0.1 (https://cabforum.org/wp-content/uploads/CA-Browser-Forum-BR-v2.0.1.pdf) // - CABForum S/MIME Baseline Requirements (SBR) v1.0.1 (https://cabforum.org/wp-content/uploads/CA-Browser-Forum-SMIMEBR-1.0.1.pdf) func IsTechnicallyConstrainedMozPolicyV29(cert *x509.Certificate) bool { // MRSP 5.3.1: "For an intermediate certificate to be considered technically constrained, the certificate MUST include an Extended Key Usage (EKU) // extension specifying the extended key usage(s) allowed for the type of end entity certificates that the intermediate CA is authorized to issue." if len(cert.ExtKeyUsage) == 0 { return false } // Look for prerequisite EKUs. var hasServerAuth, hasEmailProtection bool for _, kp := range cert.ExtKeyUsage { switch kp { case x509.ExtKeyUsageAny: // MRSP 5.3.1: "The anyExtendedKeyUsage KeyPurposeId MUST NOT appear within this extension." // TLSBR 7.1.2.10.6: "anyExtendedKeyUsage 2.5.29.37.0 MUST NOT" // SBR 7.1.2.2(g): "anyExtendedKeyUsage SHALL NOT be present." return false case x509.ExtKeyUsageEmailProtection: // ip-kp-emailProtection hasEmailProtection = true case x509.ExtKeyUsageServerAuth: // id-kp-serverAuth hasServerAuth = true case x509.ExtKeyUsageNetscapeServerGatedCrypto: // For certificates with a notBefore before 23 August 2016, the id-Netscape-stepUp OID (aka Netscape Server Gated Crypto ("nsSGC")) is treated as equivalent to id-kp-serverAuth. if cert.NotBefore.Before(time.Date(2016, time.August, 23, 0, 0, 0, 0, time.UTC)) { hasServerAuth = true } } } if hasServerAuth { // Look for disqualifying companion EKUs. // COMMENT OUT because the following rule applies whether technically constrained or not. It does not determine if technically contrained. // for _, kp := range cert.ExtKeyUsage { // switch kp { // case x509.ExtKeyUsageCodeSigning, x509.ExtKeyUsageEmailProtection, x509.ExtKeyUsageTimeStamping, x509.ExtKeyUsageOCSPSigning: // // TLSBR 7.1.2.10.6: "id-kp-codeSigning 1.3.6.1.5.5.7.3.3 MUST NOT" // // TLSBR 7.1.2.10.6: "id-kp-emailProtection 1.3.6.1.5.5.7.3.4 MUST NOT" // // TLSBR 7.1.2.10.6: "id-kp-timeStamping 1.3.6.1.5.5.7.3.8 MUST NOT" // // TLSBR 7.1.2.10.6: "id-kp-OCSPSigning 1.3.6.1.5.5.7.3.9 MUST NOT" // return false // } // } for _, oid := range cert.UnknownExtKeyUsage { if oid.Equal(oidPrecertificateSigningCertificate) { // TLSBR 7.1.2.10.6: "Precertificate Signing Certificate 1.3.6.1.4.1.11129.2.4.4 MUST NOT" return false } } // MRSP 5.3.1: "If the intermediate CA certificate includes the id-kp-serverAuth extended key usage, then to be considered technically constrained, // the certificate MUST be name-constrained as described in section 7.1.2.5 of the TLS Baseline Requirements" if len(cert.PermittedDNSDomains) < 1 && !excludesAllDomains(cert) { // TLSBR 7.1.2.5.2: "The permittedSubtrees MUST contain at least one GeneralSubtree for...dNSName...UNLESS...excludedSubtrees...exclude[s] all names of that name type". return false } else if len(cert.PermittedIPRanges) < 1 && !excludesAllIPRanges(cert) { // TLSBR 7.1.2.5.2: "The permittedSubtrees MUST contain at least one GeneralSubtree for...iPAddress...UNLESS...excludedSubtrees...exclude[s] all names of that name type". return false } else { // TLSBR 7.1.2.5.2: "the permittedSubtrees MUST contain at least one GeneralSubtree of the directoryName GeneralName name type". return len(permittedDirNames(cert)) >= 1 } } else if hasEmailProtection { // Look for disqualifying companion EKUs. // COMMENT OUT because the following rule applies whether technically constrained or not. It does not determine if technically contrained. // for _, kp := range cert.ExtKeyUsage { // switch kp { // case x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageCodeSigning, x509.ExtKeyUsageTimeStamping: // // MRSP 5.3.1: "id-kp-serverAuth...MUST NOT be present." // // SBR 7.1.2.2(g): "id-kp-serverAuth, id-kp-codeSigning, id-kp-timeStamping...SHALL NOT be present." // return false // Unconstrained // } // } // MRSP 5.3.1: "If the intermediate CA certificate includes the id-kp-emailProtection extended key usage, then to be considered technically constrained, // it MUST comply with section 7.1.5 of the S/MIME Baseline Requirements and include the Name Constraints X.509v3 extension with constraints on rfc822Name, // with at least one name in permittedSubtrees" if len(cert.PermittedEmailAddresses) < 1 { return false } else { // SBR 7.1.5: "SHALL include the nameConstraints X.509v3 extension with constraints on...directoryName as follows:...For each directoryName in permittedSubtrees". return len(permittedDirNames(cert)) >= 1 } } else { return true // Constrained to some other EKU(s). } } func excludesAllDomains(cert *x509.Certificate) bool { for _, domain := range cert.ExcludedDNSDomains { if domain == "" { return true } } return false } func excludesAllIPRanges(cert *x509.Certificate) bool { // For iPAddresses in excludedSubtrees, both IPv4 and IPv6 must be present and the constraints must cover the entire range (0.0.0.0/0 for IPv4 and ::0/0 for IPv6). var excludesIPv4, excludesIPv6 bool for _, cidr := range cert.ExcludedIPRanges { if cidr.IP.Equal(net.IPv4zero) && isBufferAllZeros(cidr.Mask, net.IPv4len) { excludesIPv4 = true } if cidr.IP.Equal(net.IPv6zero) && isBufferAllZeros(cidr.Mask, net.IPv6len) { excludesIPv6 = true } } return excludesIPv4 && excludesIPv6 } func isBufferAllZeros(buf []byte, length int) bool { if length > len(buf) { return false } for i := 0; i < length; i++ { if buf[i] != 0 { return false } } return true } // The following code is adapted from: // https://cs.opensource.google/go/go/+/refs/tags/go1.21.4:src/crypto/x509/parser.go (the parseNameConstraintsExtension function) // https://go-review.googlesource.com/c/go/+/238362 (also tracked at https://github.com/golang/go/issues/15196) // // NOTE: If/when that proposed change (238362) becomes part of Go, it will be possible to retire the function below in favour of cert.PermittedDirNames. func permittedDirNames(cert *x509.Certificate) []pkix.RDNSequence { var dirNames []pkix.RDNSequence var oidNameConstraints = asn1.ObjectIdentifier{2, 5, 29, 30} for _, ext := range cert.Extensions { if ext.Id.Equal(oidNameConstraints) { outer := cryptobyte.String(ext.Value) var toplevel, permitted, excluded cryptobyte.String var havePermitted, haveExcluded bool if !outer.ReadASN1(&toplevel, cryptobyte_asn1.SEQUENCE) || !outer.Empty() || !toplevel.ReadOptionalASN1(&permitted, &havePermitted, cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) || !toplevel.ReadOptionalASN1(&excluded, &haveExcluded, cryptobyte_asn1.Tag(1).ContextSpecific().Constructed()) || !toplevel.Empty() { return nil // Invalid NameConstraints extension. } if !havePermitted || len(permitted) == 0 { return nil // No permittedSubtrees. } for !permitted.Empty() { var seq, value cryptobyte.String var tag cryptobyte_asn1.Tag if !permitted.ReadASN1(&seq, cryptobyte_asn1.SEQUENCE) || !seq.ReadAnyASN1(&value, &tag) { return nil // Invalid NameConstraints extension. } var dirNameTag = cryptobyte_asn1.Tag(4).ContextSpecific().Constructed() switch tag { case dirNameTag: var dirName pkix.RDNSequence if rest, err := asn1.Unmarshal(value, &dirName); err != nil { return nil // ASN.1 decode error. } else if len(rest) != 0 { return nil // Trailing data after dirname constraint. } dirNames = append(dirNames, dirName) } } } } return dirNames }