Improve error creation and testing for core policy engine

This commit is contained in:
Herman Slatman 2022-04-26 01:47:07 +02:00
parent 20f5d12b99
commit 76112c2da1
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
9 changed files with 974 additions and 409 deletions

View file

@ -274,7 +274,7 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error {
if allowed, err = engine.AreSANsAllowed(sans); err != nil { if allowed, err = engine.AreSANsAllowed(sans); err != nil {
var policyErr *policy.NamePolicyError var policyErr *policy.NamePolicyError
isNamePolicyError := errors.As(err, &policyErr) isNamePolicyError := errors.As(err, &policyErr)
if isNamePolicyError && policyErr.Reason == policy.NotAuthorizedForThisName { if isNamePolicyError && policyErr.Reason == policy.NotAllowed {
return &PolicyError{ return &PolicyError{
Typ: AdminLockOut, Typ: AdminLockOut,
Err: fmt.Errorf("the provided policy would lock out %s from the CA. Please update your policy to include %s as an allowed name", sans, sans), Err: fmt.Errorf("the provided policy would lock out %s from the CA. Please update your policy to include %s as an allowed name", sans, sans),

View file

@ -58,7 +58,7 @@ func TestAuthority_checkPolicy(t *testing.T) {
}, },
err: &PolicyError{ err: &PolicyError{
Typ: EvaluationFailure, Typ: EvaluationFailure,
Err: errors.New("cannot parse domain: dns \"*\" cannot be converted to ASCII"), Err: errors.New("cannot parse dns domain \"*\""),
}, },
} }
}, },
@ -105,7 +105,7 @@ func TestAuthority_checkPolicy(t *testing.T) {
}, },
err: &PolicyError{ err: &PolicyError{
Typ: EvaluationFailure, Typ: EvaluationFailure,
Err: errors.New("cannot parse domain: dns \"**\" cannot be converted to ASCII"), Err: errors.New("cannot parse dns domain \"**\""),
}, },
} }
}, },

View file

@ -6,6 +6,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -256,10 +257,14 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
allowed, err := a.sshUserPolicy.IsSSHCertificateAllowed(certTpl) allowed, err := a.sshUserPolicy.IsSSHCertificateAllowed(certTpl)
if err != nil { if err != nil {
var pe *policy.NamePolicyError var pe *policy.NamePolicyError
if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { if errors.As(err, &pe) && pe.Reason == policy.NotAllowed {
return nil, errs.ApplyOptions( return nil, &errs.Error{
errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: %s", err.Error()), // NOTE: custom forbidden error, so that denied name is sent to client
) // as well as shown in the logs.
Status: http.StatusForbidden,
Err: fmt.Errorf("authority not allowed to sign: %w", err),
Msg: fmt.Sprintf("The request was forbidden by the certificate authority: %s", err.Error()),
}
} }
return nil, errs.InternalServerErr(err, return nil, errs.InternalServerErr(err,
errs.WithMessage("authority.SignSSH: error creating ssh user certificate"), errs.WithMessage("authority.SignSSH: error creating ssh user certificate"),
@ -279,11 +284,14 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
allowed, err := a.sshHostPolicy.IsSSHCertificateAllowed(certTpl) allowed, err := a.sshHostPolicy.IsSSHCertificateAllowed(certTpl)
if err != nil { if err != nil {
var pe *policy.NamePolicyError var pe *policy.NamePolicyError
if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { if errors.As(err, &pe) && pe.Reason == policy.NotAllowed {
return nil, errs.ApplyOptions( return nil, &errs.Error{
// TODO: show which names were not allowed; they are in the err // NOTE: custom forbidden error, so that denied name is sent to client
errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: %s", err.Error()), // as well as shown in the logs.
) Status: http.StatusForbidden,
Err: fmt.Errorf("authority not allowed to sign: %w", err),
Msg: fmt.Sprintf("The request was forbidden by the certificate authority: %s", err.Error()),
}
} }
return nil, errs.InternalServerErr(err, return nil, errs.InternalServerErr(err,
errs.WithMessage("authority.SignSSH: error creating ssh host certificate"), errs.WithMessage("authority.SignSSH: error creating ssh host certificate"),

View file

@ -203,11 +203,14 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
var allowedToSign bool var allowedToSign bool
if allowedToSign, err = a.isAllowedToSign(leaf); err != nil { if allowedToSign, err = a.isAllowedToSign(leaf); err != nil {
var pe *policy.NamePolicyError var pe *policy.NamePolicyError
if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { if errors.As(err, &pe) && pe.Reason == policy.NotAllowed {
return nil, errs.ApplyOptions( return nil, errs.ApplyOptions(&errs.Error{
errs.ForbiddenErr(errors.New("authority not allowed to sign"), err.Error()), // NOTE: custom forbidden error, so that denied name is sent to client
opts..., // as well as shown in the logs.
) Status: http.StatusForbidden,
Err: fmt.Errorf("authority not allowed to sign: %w", err),
Msg: fmt.Sprintf("The request was forbidden by the certificate authority: %s", err.Error()),
}, opts...)
} }
return nil, errs.InternalServerErr(err, return nil, errs.InternalServerErr(err,
errs.WithKeyVal("csr", csr), errs.WithKeyVal("csr", csr),

View file

@ -13,15 +13,17 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
adminAPI "github.com/smallstep/certificates/authority/admin/api" "google.golang.org/protobuf/encoding/protojson"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"go.step.sm/cli-utils/token" "go.step.sm/cli-utils/token"
"go.step.sm/cli-utils/token/provision" "go.step.sm/cli-utils/token/provision"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/randutil" "go.step.sm/crypto/randutil"
"go.step.sm/linkedca" "go.step.sm/linkedca"
"google.golang.org/protobuf/encoding/protojson"
adminAPI "github.com/smallstep/certificates/authority/admin/api"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
) )
const ( const (
@ -818,7 +820,7 @@ retry:
func (c *AdminClient) GetProvisionerPolicy(provisionerName string) (*linkedca.Policy, error) { func (c *AdminClient) GetProvisionerPolicy(provisionerName string) (*linkedca.Policy, error) {
var retried bool var retried bool
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "policy")})
tok, err := c.generateAdminToken(u) tok, err := c.generateAdminToken(u)
if err != nil { if err != nil {
return nil, fmt.Errorf("error generating admin token: %w", err) return nil, fmt.Errorf("error generating admin token: %w", err)
@ -853,7 +855,7 @@ func (c *AdminClient) CreateProvisionerPolicy(provisionerName string, p *linkedc
if err != nil { if err != nil {
return nil, fmt.Errorf("error marshaling request: %w", err) return nil, fmt.Errorf("error marshaling request: %w", err)
} }
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "policy")})
tok, err := c.generateAdminToken(u) tok, err := c.generateAdminToken(u)
if err != nil { if err != nil {
return nil, fmt.Errorf("error generating admin token: %w", err) return nil, fmt.Errorf("error generating admin token: %w", err)
@ -888,7 +890,7 @@ func (c *AdminClient) UpdateProvisionerPolicy(provisionerName string, p *linkedc
if err != nil { if err != nil {
return nil, fmt.Errorf("error marshaling request: %w", err) return nil, fmt.Errorf("error marshaling request: %w", err)
} }
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "policy")})
tok, err := c.generateAdminToken(u) tok, err := c.generateAdminToken(u)
if err != nil { if err != nil {
return nil, fmt.Errorf("error generating admin token: %w", err) return nil, fmt.Errorf("error generating admin token: %w", err)
@ -919,7 +921,7 @@ retry:
func (c *AdminClient) RemoveProvisionerPolicy(provisionerName string) error { func (c *AdminClient) RemoveProvisionerPolicy(provisionerName string) error {
var retried bool var retried bool
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "policy")})
tok, err := c.generateAdminToken(u) tok, err := c.generateAdminToken(u)
if err != nil { if err != nil {
return fmt.Errorf("error generating admin token: %w", err) return fmt.Errorf("error generating admin token: %w", err)

View file

@ -15,11 +15,11 @@ import (
type NamePolicyReason int type NamePolicyReason int
const ( const (
// NotAuthorizedForThisName results when an instance of _ NamePolicyReason = iota
// NamePolicyEngine determines that there's a constraint which // NotAllowed results when an instance of NamePolicyEngine
// doesn't permit a DNS or another type of SAN to be signed // determines that there's a constraint which doesn't permit
// (or otherwise used). // a DNS or another type of SAN to be signed (or otherwise used).
NotAuthorizedForThisName NamePolicyReason = iota NotAllowed
// CannotParseDomain is returned when an error occurs // CannotParseDomain is returned when an error occurs
// when parsing the domain part of SAN or subject. // when parsing the domain part of SAN or subject.
CannotParseDomain CannotParseDomain
@ -31,26 +31,42 @@ const (
CannotMatchNameToConstraint CannotMatchNameToConstraint
) )
type NameType string
const (
DNSNameType NameType = "dns"
IPNameType NameType = "ip"
EmailNameType NameType = "email"
URINameType NameType = "uri"
PrincipalNameType NameType = "principal"
)
type NamePolicyError struct { type NamePolicyError struct {
Reason NamePolicyReason Reason NamePolicyReason
Detail string NameType NameType
Name string
detail string
} }
func (e *NamePolicyError) Error() string { func (e *NamePolicyError) Error() string {
switch e.Reason { switch e.Reason {
case NotAuthorizedForThisName: case NotAllowed:
return "not authorized to sign for this name: " + e.Detail return fmt.Sprintf("%s name %q not allowed", e.NameType, e.Name)
case CannotParseDomain: case CannotParseDomain:
return "cannot parse domain: " + e.Detail return fmt.Sprintf("cannot parse %s domain %q", e.NameType, e.Name)
case CannotParseRFC822Name: case CannotParseRFC822Name:
return "cannot parse rfc822Name: " + e.Detail return fmt.Sprintf("cannot parse %s rfc822Name %q", e.NameType, e.Name)
case CannotMatchNameToConstraint: case CannotMatchNameToConstraint:
return "error matching name to constraint: " + e.Detail return fmt.Sprintf("error matching %s name %q to constraint", e.NameType, e.Name)
default: default:
return "unknown error: " + e.Detail 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 // 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. // 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): the X509 RFC also defines name checks on directory name; support that?
@ -98,13 +114,13 @@ func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) {
} }
e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains) e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains)
e.permittedIPRanges = removeDuplicateIPRanges(e.permittedIPRanges) e.permittedIPRanges = removeDuplicateIPNets(e.permittedIPRanges)
e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses) e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses)
e.permittedURIDomains = removeDuplicates(e.permittedURIDomains) e.permittedURIDomains = removeDuplicates(e.permittedURIDomains)
e.permittedPrincipals = removeDuplicates(e.permittedPrincipals) e.permittedPrincipals = removeDuplicates(e.permittedPrincipals)
e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains) e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains)
e.excludedIPRanges = removeDuplicateIPRanges(e.excludedIPRanges) e.excludedIPRanges = removeDuplicateIPNets(e.excludedIPRanges)
e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses) e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses)
e.excludedURIDomains = removeDuplicates(e.excludedURIDomains) e.excludedURIDomains = removeDuplicates(e.excludedURIDomains)
e.excludedPrincipals = removeDuplicates(e.excludedPrincipals) e.excludedPrincipals = removeDuplicates(e.excludedPrincipals)
@ -126,35 +142,59 @@ func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) {
return e, nil return e, nil
} }
func removeDuplicates(strSlice []string) []string { // removeDuplicates returns a new slice of strings with
if len(strSlice) == 0 { // duplicate values removed. It retains the order of elements
return nil // 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]bool)
result := []string{} keys := make(map[string]struct{}, len(items))
for _, item := range strSlice {
if _, value := keys[item]; !value && item != "" { // skip empty constraints ret = make([]string, 0, len(items))
keys[item] = true for _, item := range items {
result = append(result, item) if _, ok := keys[item]; ok {
continue
} }
keys[item] = struct{}{}
ret = append(ret, item)
} }
return result
return
} }
func removeDuplicateIPRanges(ipRanges []*net.IPNet) []*net.IPNet { // removeDuplicateIPNets returns a new slice of net.IPNets with
if len(ipRanges) == 0 { // duplicate values removed. It retains the order of elements in
return nil // 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]bool)
result := []*net.IPNet{} keys := make(map[string]struct{}, len(items))
for _, item := range ipRanges {
key := item.String() ret = make([]*net.IPNet, 0, len(items))
if _, value := keys[key]; !value { for _, item := range items {
keys[key] = true key := item.String() // use CIDR notation as key
result = append(result, item) if _, ok := keys[key]; ok {
continue
} }
keys[key] = struct{}{}
ret = append(ret, item)
} }
return result
// 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. // IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed.

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,7 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/smallstep/assert"
) )
func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) {
@ -368,9 +367,9 @@ func TestNew(t *testing.T) {
}, },
"ok/with-permitted-ip-ranges": func(t *testing.T) test { "ok/with-permitted-ip-ranges": func(t *testing.T) test {
_, nw1, err := net.ParseCIDR("127.0.0.1/24") _, nw1, err := net.ParseCIDR("127.0.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
_, nw2, err := net.ParseCIDR("192.168.0.1/24") _, nw2, err := net.ParseCIDR("192.168.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
options := []NamePolicyOption{ options := []NamePolicyOption{
WithPermittedIPRanges(nw1, nw2), WithPermittedIPRanges(nw1, nw2),
} }
@ -389,9 +388,9 @@ func TestNew(t *testing.T) {
}, },
"ok/with-excluded-ip-ranges": func(t *testing.T) test { "ok/with-excluded-ip-ranges": func(t *testing.T) test {
_, nw1, err := net.ParseCIDR("127.0.0.1/24") _, nw1, err := net.ParseCIDR("127.0.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
_, nw2, err := net.ParseCIDR("192.168.0.1/24") _, nw2, err := net.ParseCIDR("192.168.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
options := []NamePolicyOption{ options := []NamePolicyOption{
WithExcludedIPRanges(nw1, nw2), WithExcludedIPRanges(nw1, nw2),
} }
@ -410,9 +409,9 @@ func TestNew(t *testing.T) {
}, },
"ok/with-permitted-cidrs": func(t *testing.T) test { "ok/with-permitted-cidrs": func(t *testing.T) test {
_, nw1, err := net.ParseCIDR("127.0.0.1/24") _, nw1, err := net.ParseCIDR("127.0.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
_, nw2, err := net.ParseCIDR("192.168.0.1/24") _, nw2, err := net.ParseCIDR("192.168.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
options := []NamePolicyOption{ options := []NamePolicyOption{
WithPermittedCIDRs("127.0.0.1/24", "192.168.0.1/24"), WithPermittedCIDRs("127.0.0.1/24", "192.168.0.1/24"),
} }
@ -431,9 +430,9 @@ func TestNew(t *testing.T) {
}, },
"ok/with-excluded-cidrs": func(t *testing.T) test { "ok/with-excluded-cidrs": func(t *testing.T) test {
_, nw1, err := net.ParseCIDR("127.0.0.1/24") _, nw1, err := net.ParseCIDR("127.0.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
_, nw2, err := net.ParseCIDR("192.168.0.1/24") _, nw2, err := net.ParseCIDR("192.168.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
options := []NamePolicyOption{ options := []NamePolicyOption{
WithExcludedCIDRs("127.0.0.1/24", "192.168.0.1/24"), WithExcludedCIDRs("127.0.0.1/24", "192.168.0.1/24"),
} }
@ -452,11 +451,11 @@ func TestNew(t *testing.T) {
}, },
"ok/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test { "ok/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test {
_, nw1, err := net.ParseCIDR("127.0.0.1/24") _, nw1, err := net.ParseCIDR("127.0.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
_, nw2, err := net.ParseCIDR("192.168.0.31/32") _, nw2, err := net.ParseCIDR("192.168.0.31/32")
assert.FatalError(t, err) assert.NoError(t, err)
_, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") _, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")
assert.FatalError(t, err) assert.NoError(t, err)
options := []NamePolicyOption{ options := []NamePolicyOption{
WithPermittedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"), WithPermittedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
} }
@ -475,11 +474,11 @@ func TestNew(t *testing.T) {
}, },
"ok/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test { "ok/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test {
_, nw1, err := net.ParseCIDR("127.0.0.1/24") _, nw1, err := net.ParseCIDR("127.0.0.1/24")
assert.FatalError(t, err) assert.NoError(t, err)
_, nw2, err := net.ParseCIDR("192.168.0.31/32") _, nw2, err := net.ParseCIDR("192.168.0.31/32")
assert.FatalError(t, err) assert.NoError(t, err)
_, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") _, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")
assert.FatalError(t, err) assert.NoError(t, err)
options := []NamePolicyOption{ options := []NamePolicyOption{
WithExcludedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"), WithExcludedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
} }

View file

@ -25,8 +25,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
return nil return nil
} }
// TODO: implement check that requires at least a single name in all of the SANs + subject?
// TODO: set limit on total of all names validated? In x509 there's a limit on the number of comparisons // TODO: set limit on total of all names validated? In x509 there's a limit on the number of comparisons
// that protects the CA from a DoS (i.e. many heavy comparisons). The x509 implementation takes // that protects the CA from a DoS (i.e. many heavy comparisons). The x509 implementation takes
// this number as a total of all checks and keeps a (pointer to a) counter of the number of checks // this number as a total of all checks and keeps a (pointer to a) counter of the number of checks
@ -40,29 +38,37 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
// (other) excluded constraints, we'll allow a DNS (implicit allow; currently). // (other) excluded constraints, we'll allow a DNS (implicit allow; currently).
if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
return &NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAllowed,
Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), NameType: DNSNameType,
Name: dns,
detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns),
} }
} }
didCutWildcard := false didCutWildcard := false
if strings.HasPrefix(dns, "*.") { parsedDNS := dns
dns = dns[1:] if strings.HasPrefix(parsedDNS, "*.") {
parsedDNS = parsedDNS[1:]
didCutWildcard = true didCutWildcard = true
} }
parsedDNS, err := idna.Lookup.ToASCII(dns) // TODO(hs): fix this above; we need separate rule for Subject Common Name?
parsedDNS, err := idna.Lookup.ToASCII(parsedDNS)
if err != nil { if err != nil {
return &NamePolicyError{ return &NamePolicyError{
Reason: CannotParseDomain, Reason: CannotParseDomain,
Detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns), NameType: DNSNameType,
Name: dns,
detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns),
} }
} }
if didCutWildcard { if didCutWildcard {
parsedDNS = "*" + parsedDNS parsedDNS = "*" + parsedDNS
} }
if _, ok := domainToReverseLabels(parsedDNS); !ok { if _, ok := domainToReverseLabels(parsedDNS); !ok { // TODO(hs): this also fails with spaces
return &NamePolicyError{ return &NamePolicyError{
Reason: CannotParseDomain, Reason: CannotParseDomain,
Detail: fmt.Sprintf("cannot parse dns %q", dns), NameType: DNSNameType,
Name: dns,
detail: fmt.Sprintf("cannot parse dns %q", dns),
} }
} }
if err := checkNameConstraints("dns", dns, parsedDNS, if err := checkNameConstraints("dns", dns, parsedDNS,
@ -76,8 +82,10 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
for _, ip := range ips { for _, ip := range ips {
if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
return &NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAllowed,
Detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()), NameType: IPNameType,
Name: ip.String(),
detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()),
} }
} }
if err := checkNameConstraints("ip", ip.String(), ip, if err := checkNameConstraints("ip", ip.String(), ip,
@ -91,15 +99,19 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
for _, email := range emailAddresses { for _, email := range emailAddresses {
if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
return &NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAllowed,
Detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email), NameType: EmailNameType,
Name: email,
detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email),
} }
} }
mailbox, ok := parseRFC2821Mailbox(email) mailbox, ok := parseRFC2821Mailbox(email)
if !ok { if !ok {
return &NamePolicyError{ return &NamePolicyError{
Reason: CannotParseRFC822Name, Reason: CannotParseRFC822Name,
Detail: fmt.Sprintf("invalid rfc822Name %q", mailbox), NameType: EmailNameType,
Name: email,
detail: fmt.Sprintf("invalid rfc822Name %q", mailbox),
} }
} }
// According to RFC 5280, section 7.5, emails are considered to match if the local part is // According to RFC 5280, section 7.5, emails are considered to match if the local part is
@ -108,8 +120,10 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
domainASCII, err := idna.ToASCII(mailbox.domain) domainASCII, err := idna.ToASCII(mailbox.domain)
if err != nil { if err != nil {
return &NamePolicyError{ return &NamePolicyError{
Reason: CannotParseDomain, Reason: CannotParseDomain,
Detail: fmt.Sprintf("cannot parse email domain %q", email), NameType: EmailNameType,
Name: email,
detail: fmt.Sprintf("cannot parse email domain %q", email),
} }
} }
mailbox.domain = domainASCII mailbox.domain = domainASCII
@ -126,10 +140,14 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
for _, uri := range uris { for _, uri := range uris {
if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
return &NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAllowed,
Detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()), NameType: URINameType,
Name: uri.String(),
detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()),
} }
} }
// TODO(hs): ideally we'd like the uri.String() to be the original contents; now
// it's transformed into ASCII. Prevent that here?
if err := checkNameConstraints("uri", uri.String(), uri, if err := checkNameConstraints("uri", uri.String(), uri,
func(parsedName, constraint interface{}) (bool, error) { func(parsedName, constraint interface{}) (bool, error) {
return e.matchURIConstraint(parsedName.(*url.URL), constraint.(string)) return e.matchURIConstraint(parsedName.(*url.URL), constraint.(string))
@ -141,8 +159,10 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
for _, principal := range principals { for _, principal := range principals {
if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
return &NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAllowed,
Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal), NameType: PrincipalNameType,
Name: principal,
detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal),
} }
} }
// TODO: some validation? I.e. allowed characters? // TODO: some validation? I.e. allowed characters?
@ -175,15 +195,19 @@ func checkNameConstraints(
match, err := match(parsedName, constraint) match, err := match(parsedName, constraint)
if err != nil { if err != nil {
return &NamePolicyError{ return &NamePolicyError{
Reason: CannotMatchNameToConstraint, Reason: CannotMatchNameToConstraint,
Detail: err.Error(), NameType: NameType(nameType),
Name: name,
detail: err.Error(),
} }
} }
if match { if match {
return &NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAllowed,
Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), NameType: NameType(nameType),
Name: name,
detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint),
} }
} }
} }
@ -196,8 +220,10 @@ func checkNameConstraints(
var err error var err error
if ok, err = match(parsedName, constraint); err != nil { if ok, err = match(parsedName, constraint); err != nil {
return &NamePolicyError{ return &NamePolicyError{
Reason: CannotMatchNameToConstraint, Reason: CannotMatchNameToConstraint,
Detail: err.Error(), NameType: NameType(nameType),
Name: name,
detail: err.Error(),
} }
} }
@ -208,8 +234,10 @@ func checkNameConstraints(
if !ok { if !ok {
return &NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAllowed,
Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), NameType: NameType(nameType),
Name: name,
detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name),
} }
} }