certificates/policy/engine.go

313 lines
11 KiB
Go
Raw Normal View History

2022-01-17 22:36:13 +00:00
package policy
import (
"crypto/x509"
2022-01-03 14:32:58 +00:00
"crypto/x509/pkix"
"fmt"
"net"
"net/url"
2022-01-17 22:36:13 +00:00
"golang.org/x/crypto/ssh"
"go.step.sm/crypto/x509util"
)
type NamePolicyReason int
const (
_ NamePolicyReason = iota
// 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
// 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 (
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)
}
}
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.
2022-01-17 22:36:13 +00:00
// 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 {
2022-01-17 21:49:47 +00:00
// verifySubjectCommonName is set when Subject Common Name must be verified
2022-01-03 14:32:58 +00:00
verifySubjectCommonName bool
2022-01-17 21:49:47 +00:00
// allowLiteralWildcardNames allows literal wildcard DNS domains
allowLiteralWildcardNames bool
// permitted and exluded constraints similar to x509 Name Constraints
permittedDNSDomains []string
excludedDNSDomains []string
permittedIPRanges []*net.IPNet
excludedIPRanges []*net.IPNet
permittedEmailAddresses []string
excludedEmailAddresses []string
permittedURIDomains []string
excludedURIDomains []string
2022-01-17 22:36:13 +00:00
permittedPrincipals []string
excludedPrincipals []string
2022-01-17 21:49:47 +00:00
// some internal counts for housekeeping
numberOfDNSDomainConstraints int
numberOfIPRangeConstraints int
numberOfEmailAddressConstraints int
numberOfURIDomainConstraints int
2022-01-17 22:36:13 +00:00
numberOfPrincipalConstraints int
2022-01-17 21:49:47 +00:00
totalNumberOfPermittedConstraints int
totalNumberOfExcludedConstraints int
totalNumberOfConstraints int
}
// NewNamePolicyEngine creates a new NamePolicyEngine with NamePolicyOptions
func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) {
e := &NamePolicyEngine{}
2022-01-17 21:49:47 +00:00
for _, option := range opts {
if err := option(e); err != nil {
return nil, err
}
}
2022-01-17 21:49:47 +00:00
e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains)
e.permittedIPRanges = removeDuplicateIPNets(e.permittedIPRanges)
2022-01-17 21:49:47 +00:00
e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses)
e.permittedURIDomains = removeDuplicates(e.permittedURIDomains)
2022-01-17 22:36:13 +00:00
e.permittedPrincipals = removeDuplicates(e.permittedPrincipals)
2022-01-17 21:49:47 +00:00
e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains)
e.excludedIPRanges = removeDuplicateIPNets(e.excludedIPRanges)
2022-01-17 21:49:47 +00:00
e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses)
e.excludedURIDomains = removeDuplicates(e.excludedURIDomains)
2022-01-17 22:36:13 +00:00
e.excludedPrincipals = removeDuplicates(e.excludedPrincipals)
2022-01-17 21:49:47 +00:00
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)
2022-01-17 22:36:13 +00:00
e.numberOfPrincipalConstraints = len(e.permittedPrincipals) + len(e.excludedPrincipals)
2022-01-17 21:49:47 +00:00
e.totalNumberOfPermittedConstraints = len(e.permittedDNSDomains) + len(e.permittedIPRanges) +
2022-01-17 22:36:13 +00:00
len(e.permittedEmailAddresses) + len(e.permittedURIDomains) + len(e.permittedPrincipals)
2022-01-17 21:49:47 +00:00
e.totalNumberOfExcludedConstraints = len(e.excludedDNSDomains) + len(e.excludedIPRanges) +
2022-01-17 22:36:13 +00:00
len(e.excludedEmailAddresses) + len(e.excludedURIDomains) + len(e.excludedPrincipals)
2022-01-17 21:49:47 +00:00
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
2022-01-17 21:49:47 +00:00
}
keys := make(map[string]struct{}, len(items))
ret = make([]string, 0, len(items))
for _, item := range items {
if _, ok := keys[item]; ok {
continue
2022-01-17 21:49:47 +00:00
}
keys[item] = struct{}{}
ret = append(ret, item)
2022-01-17 21:49:47 +00:00
}
return
2022-01-17 21:49:47 +00:00
}
// 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
2022-01-17 21:49:47 +00:00
}
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
2022-01-17 21:49:47 +00:00
}
keys[key] = struct{}{}
ret = append(ret, item)
2022-01-17 21:49:47 +00:00
}
// TODO(hs): implement filter of fully overlapping ranges,
// so that the smaller ones are automatically removed?
return
2022-01-17 21:49:47 +00:00
}
2022-03-31 14:12:29 +00:00
// IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed.
func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) (bool, error) {
2022-01-03 14:32:58 +00:00
dnsNames, ips, emails, uris := cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs
// when Subject Common Name must be verified in addition to the SANs, it is
// added to the appropriate slice of names.
if e.verifySubjectCommonName {
appendSubjectCommonName(cert.Subject, &dnsNames, &ips, &emails, &uris)
}
2022-01-17 22:36:13 +00:00
if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil {
return false, err
}
return true, nil
}
2022-03-31 14:12:29 +00:00
// IsX509CertificateRequestAllowed verifies that all names in the CSR are allowed.
func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) (bool, error) {
2022-01-03 14:32:58 +00:00
dnsNames, ips, emails, uris := csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs
// when Subject Common Name must be verified in addition to the SANs, it is
// added to the appropriate slice of names.
if e.verifySubjectCommonName {
appendSubjectCommonName(csr.Subject, &dnsNames, &ips, &emails, &uris)
}
2022-01-17 22:36:13 +00:00
if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil {
return false, err
}
return true, 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) (bool, error) {
dnsNames, ips, emails, uris := x509util.SplitSANs(sans)
2022-01-17 22:36:13 +00:00
if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil {
return false, err
}
return true, nil
}
// IsDNSAllowed verifies a single DNS domain is allowed.
func (e *NamePolicyEngine) IsDNSAllowed(dns string) (bool, error) {
2022-01-17 22:36:13 +00:00
if err := e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}, []string{}); err != nil {
return false, err
}
return true, nil
}
// IsIPAllowed verifies a single IP domain is allowed.
func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) {
2022-01-17 22:36:13 +00:00
if err := e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}, []string{}); err != nil {
return false, err
}
return true, nil
}
2022-03-31 14:12:29 +00:00
// IsSSHCertificateAllowed verifies that all principals in an SSH certificate are allowed.
func (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) (bool, error) {
dnsNames, ips, emails, principals, err := splitSSHPrincipals(cert)
2022-01-25 13:59:55 +00:00
if err != nil {
return false, err
}
if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, principals); err != nil {
return false, err
}
return true, nil
}
2022-01-03 14:32:58 +00:00
// appendSubjectCommonName appends the Subject Common Name to the appropriate slice of names. The logic is
// similar as x509util.SplitSANs: if the subject can be parsed as an IP, it's added to the ips. If it can
// be parsed as an URL, it is added to the URIs. If it contains an @, it is added to emails. When it's none
// of these, it's added to the DNS names.
func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.IP, emails *[]string, uris *[]*url.URL) {
commonName := subject.CommonName
if commonName == "" {
return
}
2022-01-25 13:59:55 +00:00
subjectDNSNames, subjectIPs, subjectEmails, subjectURIs := x509util.SplitSANs([]string{commonName})
*dnsNames = append(*dnsNames, subjectDNSNames...)
*ips = append(*ips, subjectIPs...)
*emails = append(*emails, subjectEmails...)
*uris = append(*uris, subjectURIs...)
2022-01-03 14:32:58 +00:00
}
// 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) {
2022-01-17 22:36:13 +00:00
dnsNames = []string{}
ips = []net.IP{}
2022-01-17 22:36:13 +00:00
emails = []string{}
principals = []string{}
2022-01-25 13:59:55 +00:00
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)
2022-01-17 22:36:13 +00:00
}
2022-01-25 13:59:55 +00:00
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
2022-03-31 14:12:29 +00:00
// in the SSH certificate. We're exluding URIs, because they can be confusing
2022-01-25 13:59:55 +00:00
// when used in a SSH user certificate.
principals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)
2022-04-18 19:47:31 +00:00
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)
2022-01-25 13:59:55 +00:00
}
default:
err = fmt.Errorf("unexpected SSH certificate type %d", cert.CertType)
2022-01-17 22:36:13 +00:00
}
2022-01-25 13:59:55 +00:00
2022-01-17 22:36:13 +00:00
return
}