1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
|
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)
}
// splitSSHPrincipals 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
}
|