package policy import ( "crypto/x509" "fmt" "net" "net/http" "net/url" "go.step.sm/crypto/x509util" "golang.org/x/crypto/ssh" "github.com/smallstep/certificates/errs" ) type NamePolicyReason int const ( // NotAllowed results when an instance of NamePolicyEngine // determines that there's a constraint which doesn't permit // a DNS or another type of SAN to be signed (or otherwise used). NotAllowed NamePolicyReason = iota + 1 // CannotParseDomain is returned when an error occurs // when parsing the domain part of SAN or subject. CannotParseDomain // CannotParseRFC822Name is returned when an error // occurs when parsing an email address. CannotParseRFC822Name // CannotMatch is the type of error returned when // an error happens when matching SAN types. CannotMatchNameToConstraint ) type NameType string const ( CNNameType NameType = "cn" DNSNameType NameType = "dns" IPNameType NameType = "ip" EmailNameType NameType = "email" URINameType NameType = "uri" PrincipalNameType NameType = "principal" ) type NamePolicyError struct { Reason NamePolicyReason NameType NameType Name string detail string } func (e *NamePolicyError) Error() string { switch e.Reason { case NotAllowed: return fmt.Sprintf("%s name %q not allowed", e.NameType, e.Name) case CannotParseDomain: return fmt.Sprintf("cannot parse %s domain %q", e.NameType, e.Name) case CannotParseRFC822Name: return fmt.Sprintf("cannot parse %s rfc822Name %q", e.NameType, e.Name) case CannotMatchNameToConstraint: return fmt.Sprintf("error matching %s name %q to constraint", e.NameType, e.Name) default: return fmt.Sprintf("unknown error reason (%d): %s", e.Reason, e.detail) } } // As implements the As(any) bool interface and allows to use "errors.As()" to // convert a NotAllowed NamePolicyError to an errs.Error. func (e *NamePolicyError) As(v any) bool { if e.Reason == NotAllowed { if err, ok := v.(**errs.Error); ok { *err = &errs.Error{ Status: http.StatusForbidden, Msg: fmt.Sprintf("The request was forbidden by the certificate authority: %s", e.Error()), Err: e, } return true } } return false } func (e *NamePolicyError) Detail() string { return e.detail } // NamePolicyEngine can be used to check that a CSR or Certificate meets all allowed and // denied names before a CA creates and/or signs the Certificate. // TODO(hs): the X509 RFC also defines name checks on directory name; support that? // TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine? // TODO(hs): implement matching URI schemes, paths, etc; not just the domain part of URI domains type NamePolicyEngine struct { // verifySubjectCommonName is set when Subject Common Name must be verified verifySubjectCommonName bool // allowLiteralWildcardNames allows literal wildcard DNS domains allowLiteralWildcardNames bool // permitted and exluded constraints similar to x509 Name Constraints permittedCommonNames []string excludedCommonNames []string permittedDNSDomains []string excludedDNSDomains []string permittedIPRanges []*net.IPNet excludedIPRanges []*net.IPNet permittedEmailAddresses []string excludedEmailAddresses []string permittedURIDomains []string excludedURIDomains []string permittedPrincipals []string excludedPrincipals []string // some internal counts for housekeeping numberOfCommonNameConstraints int numberOfDNSDomainConstraints int numberOfIPRangeConstraints int numberOfEmailAddressConstraints int numberOfURIDomainConstraints int numberOfPrincipalConstraints int totalNumberOfPermittedConstraints int totalNumberOfExcludedConstraints int totalNumberOfConstraints int } // NewNamePolicyEngine creates a new NamePolicyEngine with NamePolicyOptions func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { e := &NamePolicyEngine{} for _, option := range opts { if err := option(e); err != nil { return nil, err } } e.permittedCommonNames = removeDuplicates(e.permittedCommonNames) e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains) e.permittedIPRanges = removeDuplicateIPNets(e.permittedIPRanges) e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses) e.permittedURIDomains = removeDuplicates(e.permittedURIDomains) e.permittedPrincipals = removeDuplicates(e.permittedPrincipals) e.excludedCommonNames = removeDuplicates(e.excludedCommonNames) e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains) e.excludedIPRanges = removeDuplicateIPNets(e.excludedIPRanges) e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses) e.excludedURIDomains = removeDuplicates(e.excludedURIDomains) e.excludedPrincipals = removeDuplicates(e.excludedPrincipals) e.numberOfCommonNameConstraints = len(e.permittedCommonNames) + len(e.excludedCommonNames) e.numberOfDNSDomainConstraints = len(e.permittedDNSDomains) + len(e.excludedDNSDomains) e.numberOfIPRangeConstraints = len(e.permittedIPRanges) + len(e.excludedIPRanges) e.numberOfEmailAddressConstraints = len(e.permittedEmailAddresses) + len(e.excludedEmailAddresses) e.numberOfURIDomainConstraints = len(e.permittedURIDomains) + len(e.excludedURIDomains) e.numberOfPrincipalConstraints = len(e.permittedPrincipals) + len(e.excludedPrincipals) e.totalNumberOfPermittedConstraints = len(e.permittedCommonNames) + len(e.permittedDNSDomains) + len(e.permittedIPRanges) + len(e.permittedEmailAddresses) + len(e.permittedURIDomains) + len(e.permittedPrincipals) e.totalNumberOfExcludedConstraints = len(e.excludedCommonNames) + len(e.excludedDNSDomains) + len(e.excludedIPRanges) + len(e.excludedEmailAddresses) + len(e.excludedURIDomains) + len(e.excludedPrincipals) e.totalNumberOfConstraints = e.totalNumberOfPermittedConstraints + e.totalNumberOfExcludedConstraints return e, nil } // removeDuplicates returns a new slice of strings with // duplicate values removed. It retains the order of elements // in the source slice. func removeDuplicates(items []string) (ret []string) { // no need to remove dupes; return original if len(items) <= 1 { return items } keys := make(map[string]struct{}, len(items)) ret = make([]string, 0, len(items)) for _, item := range items { if _, ok := keys[item]; ok { continue } keys[item] = struct{}{} ret = append(ret, item) } return } // removeDuplicateIPNets returns a new slice of net.IPNets with // duplicate values removed. It retains the order of elements in // the source slice. An IPNet is considered duplicate if its CIDR // notation exists multiple times in the slice. func removeDuplicateIPNets(items []*net.IPNet) (ret []*net.IPNet) { // no need to remove dupes; return original if len(items) <= 1 { return items } keys := make(map[string]struct{}, len(items)) ret = make([]*net.IPNet, 0, len(items)) for _, item := range items { key := item.String() // use CIDR notation as key if _, ok := keys[key]; ok { continue } keys[key] = struct{}{} ret = append(ret, item) } // TODO(hs): implement filter of fully overlapping ranges, // so that the smaller ones are automatically removed? return } // IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed. func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) error { if err := e.validateNames(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs, []string{}); err != nil { return err } if e.verifySubjectCommonName { return e.validateCommonName(cert.Subject.CommonName) } return nil } // IsX509CertificateRequestAllowed verifies that all names in the CSR are allowed. func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error { if err := e.validateNames(csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs, []string{}); err != nil { return err } if e.verifySubjectCommonName { return e.validateCommonName(csr.Subject.CommonName) } return nil } // AreSANsAllowed verifies that all names in the slice of SANs are allowed. // The SANs are first split into DNS names, IPs, email addresses and URIs. func (e *NamePolicyEngine) AreSANsAllowed(sans []string) error { dnsNames, ips, emails, uris := x509util.SplitSANs(sans) return e.validateNames(dnsNames, ips, emails, uris, []string{}) } // IsDNSAllowed verifies a single DNS domain is allowed. func (e *NamePolicyEngine) IsDNSAllowed(dns string) error { return e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}, []string{}) } // IsIPAllowed verifies a single IP domain is allowed. func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) error { return e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}, []string{}) } // IsSSHCertificateAllowed verifies that all principals in an SSH certificate are allowed. func (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) error { dnsNames, ips, emails, principals, err := splitSSHPrincipals(cert) if err != nil { return err } return e.validateNames(dnsNames, ips, emails, []*url.URL{}, principals) } // splitPrincipals splits SSH certificate principals into DNS names, emails and usernames. func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, principals []string, err error) { dnsNames = []string{} ips = []net.IP{} emails = []string{} principals = []string{} var uris []*url.URL switch cert.CertType { case ssh.HostCert: dnsNames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) if len(uris) > 0 { err = fmt.Errorf("URL principals %v not expected in SSH host certificate ", uris) } case ssh.UserCert: // re-using SplitSANs results in anything that can't be parsed as an IP, URI or email // to be considered a username principal. This allows usernames like h.slatman to be present // in the SSH certificate. We're exluding URIs, because they can be confusing // when used in a SSH user certificate. principals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) if len(ips) > 0 { err = fmt.Errorf("IP principals %v not expected in SSH user certificate ", ips) } if len(uris) > 0 { err = fmt.Errorf("URL principals %v not expected in SSH user certificate ", uris) } default: err = fmt.Errorf("unexpected SSH certificate type %d", cert.CertType) } return }