forked from TrueCloudLab/certificates
Improve error creation and testing for core policy engine
This commit is contained in:
parent
20f5d12b99
commit
76112c2da1
9 changed files with 974 additions and 409 deletions
|
@ -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),
|
||||||
|
|
|
@ -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 \"**\""),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
112
policy/engine.go
112
policy/engine.go
|
@ -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
|
@ -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"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue