Check admin subjects before changing policy

This commit is contained in:
Herman Slatman 2022-03-21 15:53:59 +01:00
parent 81b0c6c37c
commit 101ca6a2d3
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
16 changed files with 255 additions and 91 deletions

View file

@ -105,6 +105,8 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) {
// management of allowed/denied names based on just the name, without having bound to EAB. Still, // management of allowed/denied names based on just the name, without having bound to EAB. Still,
// EAB is not illogical, because that's the way Accounts are connected to an external system and // EAB is not illogical, because that's the way Accounts are connected to an external system and
// thus make sense to also set the allowed/denied names based on that info. // thus make sense to also set the allowed/denied names based on that info.
// TODO: also perform check on the authority level here already, so that challenges are not performed
// and after that the CA fails to sign it. (i.e. h.ca function?)
for _, identifier := range nor.Identifiers { for _, identifier := range nor.Identifiers {
// TODO: gather all errors, so that we can build subproblems; include the nor.Validate() error here too, like in example? // TODO: gather all errors, so that we can build subproblems; include the nor.Validate() error here too, like in example?

View file

@ -14,7 +14,7 @@ import (
const ( const (
// provisionerContextKey provisioner key // provisionerContextKey provisioner key
provisionerContextKey = ContextKey("provisioner") provisionerContextKey = admin.ContextKey("provisioner")
) )
// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests

View file

@ -26,8 +26,8 @@ type adminAuthority interface {
UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error
RemoveProvisioner(ctx context.Context, id string) error RemoveProvisioner(ctx context.Context, id string) error
GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error)
StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error StoreAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error
UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error
RemoveAuthorityPolicy(ctx context.Context) error RemoveAuthorityPolicy(ctx context.Context) error
} }

View file

@ -139,11 +139,11 @@ func (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca.
return nil, errors.New("not implemented yet") return nil, errors.New("not implemented yet")
} }
func (m *mockAdminAuthority) StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { func (m *mockAdminAuthority) StoreAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error {
return errors.New("not implemented yet") return errors.New("not implemented yet")
} }
func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error {
return errors.New("not implemented yet") return errors.New("not implemented yet")
} }

View file

@ -30,8 +30,7 @@ func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeRespo
func (h *Handler) Route(r api.Router) { func (h *Handler) Route(r api.Router) {
authnz := func(next nextHTTP) nextHTTP { authnz := func(next nextHTTP) nextHTTP {
//return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next))
return h.requireAPIEnabled(next) // TODO(hs): remove this; temporarily no auth checks for simple testing...
} }
requireEABEnabled := func(next nextHTTP) nextHTTP { requireEABEnabled := func(next nextHTTP) nextHTTP {

View file

@ -7,6 +7,7 @@ import (
"github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/admin/db/nosql" "github.com/smallstep/certificates/authority/admin/db/nosql"
"go.step.sm/linkedca"
) )
type nextHTTP = func(http.ResponseWriter, *http.Request) type nextHTTP = func(http.ResponseWriter, *http.Request)
@ -27,6 +28,7 @@ func (h *Handler) requireAPIEnabled(next nextHTTP) nextHTTP {
// extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token. // extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token.
func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
tok := r.Header.Get("Authorization") tok := r.Header.Get("Authorization")
if tok == "" { if tok == "" {
api.WriteError(w, admin.NewError(admin.ErrorUnauthorizedType, api.WriteError(w, admin.NewError(admin.ErrorUnauthorizedType,
@ -40,7 +42,7 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP {
return return
} }
ctx := context.WithValue(r.Context(), adminContextKey, adm) ctx := context.WithValue(r.Context(), admin.AdminContextKey, adm)
next(w, r.WithContext(ctx)) next(w, r.WithContext(ctx))
} }
} }
@ -49,13 +51,14 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP {
func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTTP { func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// actions allowed in standalone mode are always allowed // actions allowed in standalone mode are always supported
if supportedInStandalone { if supportedInStandalone {
next(w, r) next(w, r)
return return
} }
// when in standalone mode, actions are not supported // when not in standalone mode and using a nosql.DB backend,
// actions are not supported
if _, ok := h.adminDB.(*nosql.DB); ok { if _, ok := h.adminDB.(*nosql.DB); ok {
api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType,
"operation not supported in standalone mode")) "operation not supported in standalone mode"))
@ -67,11 +70,12 @@ func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTT
} }
} }
// ContextKey is the key type for storing and searching for ACME request // adminFromContext searches the context for a *linkedca.Admin.
// essentials in the context of a request. // Returns the admin or an error.
type ContextKey string func adminFromContext(ctx context.Context) (*linkedca.Admin, error) {
val, ok := ctx.Value(admin.AdminContextKey).(*linkedca.Admin)
const ( if !ok || val == nil {
// adminContextKey account key return nil, admin.NewError(admin.ErrorBadRequestType, "admin not in context")
adminContextKey = ContextKey("admin") }
) return val, nil
}

View file

@ -152,7 +152,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
req.Header["Authorization"] = []string{"token"} req.Header["Authorization"] = []string{"token"}
createdAt := time.Now() createdAt := time.Now()
var deletedAt time.Time var deletedAt time.Time
admin := &linkedca.Admin{ adm := &linkedca.Admin{
Id: "adminID", Id: "adminID",
AuthorityId: "authorityID", AuthorityId: "authorityID",
Subject: "admin", Subject: "admin",
@ -164,20 +164,20 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
auth := &mockAdminAuthority{ auth := &mockAdminAuthority{
MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) { MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) {
assert.Equals(t, "token", token) assert.Equals(t, "token", token)
return admin, nil return adm, nil
}, },
} }
next := func(w http.ResponseWriter, r *http.Request) { next := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
a := ctx.Value(adminContextKey) // verifying that the context now has a linkedca.Admin a := ctx.Value(admin.AdminContextKey) // verifying that the context now has a linkedca.Admin
adm, ok := a.(*linkedca.Admin) adm, ok := a.(*linkedca.Admin)
if !ok { if !ok {
t.Errorf("expected *linkedca.Admin; got %T", a) t.Errorf("expected *linkedca.Admin; got %T", a)
return return
} }
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}
if !cmp.Equal(admin, adm, opts...) { if !cmp.Equal(adm, adm, opts...) {
t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(admin, adm, opts...)) t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(adm, adm, opts...))
} }
w.Write(nil) // mock response with status 200 w.Write(nil) // mock response with status 200
} }

View file

@ -87,8 +87,14 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
return return
} }
if err := par.auth.StoreAuthorityPolicy(ctx, newPolicy); err != nil { adm, err := adminFromContext(ctx)
api.WriteError(w, admin.WrapErrorISE(err, "error storing authority policy")) if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving admin from context"))
return
}
if err := par.auth.StoreAuthorityPolicy(ctx, adm, newPolicy); err != nil {
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy"))
return return
} }
@ -103,25 +109,49 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
// UpdateAuthorityPolicy handles the PUT /admin/authority/policy request // UpdateAuthorityPolicy handles the PUT /admin/authority/policy request
func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) { func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
var policy = new(linkedca.Policy)
if err := api.ReadProtoJSON(r.Body, policy); err != nil { ctx := r.Context()
policy, err := par.auth.GetAuthorityPolicy(ctx)
shouldWriteError := false
if ae, ok := err.(*admin.Error); ok {
shouldWriteError = !ae.IsType(admin.ErrorNotFoundType)
}
if shouldWriteError {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
return
}
if policy == nil {
api.JSONNotFound(w)
return
}
var newPolicy = new(linkedca.Policy)
if err := api.ReadProtoJSON(r.Body, newPolicy); err != nil {
api.WriteError(w, err) api.WriteError(w, err)
return return
} }
ctx := r.Context() adm, err := adminFromContext(ctx)
if err := par.auth.UpdateAuthorityPolicy(ctx, policy); err != nil { if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error updating authority policy")) api.WriteError(w, admin.WrapErrorISE(err, "error retrieving admin from context"))
return return
} }
newPolicy, err := par.auth.GetAuthorityPolicy(ctx) if err := par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy"))
return
}
newlyStoredPolicy, err := par.auth.GetAuthorityPolicy(ctx)
if err != nil { if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating")) api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating"))
return return
} }
api.ProtoJSONStatus(w, newPolicy, http.StatusOK) api.ProtoJSONStatus(w, newlyStoredPolicy, http.StatusOK)
} }
// DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request // DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request

View file

@ -0,0 +1,10 @@
package admin
// ContextKey is the key type for storing and searching for
// Admin API objects in request contexts.
type ContextKey string
const (
// AdminContextKey account key
AdminContextKey = ContextKey("admin")
)

View file

@ -78,7 +78,7 @@ func (c *Collection) LoadByProvisioner(provName string) ([]*linkedca.Admin, bool
} }
// Store adds an admin to the collection and enforces the uniqueness of // Store adds an admin to the collection and enforces the uniqueness of
// admin IDs and amdin subject <-> provisioner name combos. // admin IDs and admin subject <-> provisioner name combos.
func (c *Collection) Store(adm *linkedca.Admin, prov provisioner.Interface) error { func (c *Collection) Store(adm *linkedca.Admin, prov provisioner.Interface) error {
// Input validation. // Input validation.
if adm.ProvisionerId != prov.GetID() { if adm.ProvisionerId != prov.GetID() {

View file

@ -219,15 +219,19 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
if a.config.AuthorityConfig.EnableAdmin { if a.config.AuthorityConfig.EnableAdmin {
linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx)
if err != nil { if err != nil {
return admin.WrapErrorISE(err, "error getting policy to initialize authority") return admin.WrapErrorISE(err, "error getting policy to (re)load policy engines")
} }
policyOptions = policyToCertificates(linkedPolicy) policyOptions = policyToCertificates(linkedPolicy)
} else { } else {
policyOptions = a.config.AuthorityConfig.Policy policyOptions = a.config.AuthorityConfig.Policy
} }
// return early if no policy options set // if no new or updated policy option is set, clear policy engines that (may have)
// been configured before and return early
if policyOptions == nil { if policyOptions == nil {
a.x509Policy = nil
a.sshHostPolicy = nil
a.sshUserPolicy = nil
return nil return nil
} }
@ -574,7 +578,7 @@ func (a *Authority) init() error {
return err return err
} }
// Load Policy Engines // Load x509 and SSH Policy Engines
if err := a.reloadPolicyEngines(context.Background()); err != nil { if err := a.reloadPolicyEngines(context.Background()); err != nil {
return err return err
} }

View file

@ -2,10 +2,15 @@ package authority
import ( import (
"context" "context"
"fmt"
"github.com/pkg/errors"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/policy" authPolicy "github.com/smallstep/certificates/authority/policy"
"go.step.sm/linkedca" policy "github.com/smallstep/certificates/policy"
) )
func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
@ -20,31 +25,39 @@ func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, e
return policy, nil return policy, nil
} }
func (a *Authority) StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { func (a *Authority) StoreAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) error {
a.adminMutex.Lock() a.adminMutex.Lock()
defer a.adminMutex.Unlock() defer a.adminMutex.Unlock()
if err := a.checkPolicy(ctx, adm, policy); err != nil {
return err
}
if err := a.adminDB.CreateAuthorityPolicy(ctx, policy); err != nil { if err := a.adminDB.CreateAuthorityPolicy(ctx, policy); err != nil {
return err return err
} }
if err := a.reloadPolicyEngines(ctx); err != nil { if err := a.reloadPolicyEngines(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources when creating authority policy") return admin.WrapErrorISE(err, "error reloading policy engines when creating authority policy")
} }
return nil return nil
} }
func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) error {
a.adminMutex.Lock() a.adminMutex.Lock()
defer a.adminMutex.Unlock() defer a.adminMutex.Unlock()
if err := a.checkPolicy(ctx, adm, policy); err != nil {
return err
}
if err := a.adminDB.UpdateAuthorityPolicy(ctx, policy); err != nil { if err := a.adminDB.UpdateAuthorityPolicy(ctx, policy); err != nil {
return err return err
} }
if err := a.reloadPolicyEngines(ctx); err != nil { if err := a.reloadPolicyEngines(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources when updating authority policy") return admin.WrapErrorISE(err, "error reloading policy engines when updating authority policy")
} }
return nil return nil
@ -59,34 +72,84 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
} }
if err := a.reloadPolicyEngines(ctx); err != nil { if err := a.reloadPolicyEngines(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources when deleting authority policy") return admin.WrapErrorISE(err, "error reloading policy engines when deleting authority policy")
} }
return nil return nil
} }
func policyToCertificates(p *linkedca.Policy) *policy.Options { func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) error {
// convert the policy; return early if nil
policyOptions := policyToCertificates(p)
if policyOptions == nil {
return nil
}
engine, err := authPolicy.NewX509PolicyEngine(policyOptions.GetX509Options())
if err != nil {
return admin.WrapErrorISE(err, "error creating temporary policy engine")
}
// TODO(hs): Provide option to force the policy, even when the admin subject would be locked out?
sans := []string{adm.Subject}
if err := isAllowed(engine, sans); err != nil {
return err
}
// TODO(hs): perform the check for other admin subjects too?
// What logic to use for that: do all admins need access? Only super admins? At least one?
return nil
}
func isAllowed(engine authPolicy.X509Policy, sans []string) error {
var (
allowed bool
err error
)
if allowed, err = engine.AreSANsAllowed(sans); err != nil {
var policyErr *policy.NamePolicyError
if errors.As(err, &policyErr); policyErr.Reason == policy.NotAuthorizedForThisName {
return 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)
} else {
return err
}
}
if !allowed {
return 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)
}
return nil
}
func policyToCertificates(p *linkedca.Policy) *authPolicy.Options {
// return early // return early
if p == nil { if p == nil {
return nil return nil
} }
// prepare full policy struct // prepare full policy struct
opts := &policy.Options{ opts := &authPolicy.Options{
X509: &policy.X509PolicyOptions{ X509: &authPolicy.X509PolicyOptions{
AllowedNames: &policy.X509NameOptions{}, AllowedNames: &authPolicy.X509NameOptions{},
DeniedNames: &policy.X509NameOptions{}, DeniedNames: &authPolicy.X509NameOptions{},
}, },
SSH: &policy.SSHPolicyOptions{ SSH: &authPolicy.SSHPolicyOptions{
Host: &policy.SSHHostCertificateOptions{ Host: &authPolicy.SSHHostCertificateOptions{
AllowedNames: &policy.SSHNameOptions{}, AllowedNames: &authPolicy.SSHNameOptions{},
DeniedNames: &policy.SSHNameOptions{}, DeniedNames: &authPolicy.SSHNameOptions{},
}, },
User: &policy.SSHUserCertificateOptions{ User: &authPolicy.SSHUserCertificateOptions{
AllowedNames: &policy.SSHNameOptions{}, AllowedNames: &authPolicy.SSHNameOptions{},
DeniedNames: &policy.SSHNameOptions{}, DeniedNames: &authPolicy.SSHNameOptions{},
}, },
}, },
} }
// fill x509 policy configuration // fill x509 policy configuration
if p.X509 != nil { if p.X509 != nil {
if p.X509.Allow != nil { if p.X509.Allow != nil {
@ -102,6 +165,7 @@ func policyToCertificates(p *linkedca.Policy) *policy.Options {
opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris
} }
} }
// fill ssh policy configuration // fill ssh policy configuration
if p.Ssh != nil { if p.Ssh != nil {
if p.Ssh.Host != nil { if p.Ssh.Host != nil {

View file

@ -191,26 +191,20 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
} }
} }
// If a policy is configured, perform allow/deny policy check on authority level // Check if authority is allowed to sign the certificate
// TODO: policy currently also applies to admin token certs; how to circumvent? var allowedToSign bool
// Allow any name of an admin in the DB? Or in the admin collection? if allowedToSign, err = a.isAllowedToSign(leaf); err != nil {
todoRemoveThis := false return nil, errs.InternalServerErr(err,
if todoRemoveThis && a.x509Policy != nil { errs.WithKeyVal("csr", csr),
allowed, err := a.x509Policy.AreCertificateNamesAllowed(leaf) errs.WithKeyVal("signOptions", signOpts),
if err != nil { errs.WithMessage("error creating certificate"),
return nil, errs.InternalServerErr(err, )
errs.WithKeyVal("csr", csr), }
errs.WithKeyVal("signOptions", signOpts), if !allowedToSign {
errs.WithMessage("error creating certificate"), return nil, errs.ApplyOptions(
) errs.ForbiddenErr(errors.New("authority not allowed to sign"), "error creating certificate"),
} opts...,
if !allowed { )
// TODO: include SANs in error message?
return nil, errs.ApplyOptions(
errs.ForbiddenErr(errors.New("authority not allowed to sign"), "error creating certificate"),
opts...,
)
}
} }
// Sign certificate // Sign certificate
@ -236,6 +230,61 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
return fullchain, nil return fullchain, nil
} }
// isAllowedToSign checks if the Authority is allowed to sign the X.509 certificate.
// It first checks if the certificate contains an admin subject that exists in the
// collection of admins. The CA is always allowed to sign those. If the cert contains
// different names and a policy is configured, the policy will be executed against
// the cert to see if the CA is allowed to sign it.
func (a *Authority) isAllowedToSign(cert *x509.Certificate) (bool, error) {
// // check if certificate is an admin identity token certificate and the admin subject exists
// b := isAdminIdentityTokenCertificate(cert)
// _ = b
// if isAdminIdentityTokenCertificate(cert) && a.admins.HasSubject(cert.Subject.CommonName) {
// return true, nil
// }
// if no policy is configured, the cert is implicitly allowed
if a.x509Policy == nil {
return true, nil
}
return a.x509Policy.AreCertificateNamesAllowed(cert)
}
func isAdminIdentityTokenCertificate(cert *x509.Certificate) bool {
// TODO: remove this check
if cert.Issuer.CommonName != "" {
return false
}
subject := cert.Subject.CommonName
if subject == "" {
return false
}
dnsNames := cert.DNSNames
if len(dnsNames) != 1 {
return false
}
if dnsNames[0] != subject {
return false
}
extras := cert.ExtraExtensions
if len(extras) != 1 {
return false
}
extra := extras[0]
return extra.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1})
}
// Renew creates a new Certificate identical to the old certificate, except // Renew creates a new Certificate identical to the old certificate, except
// with a validity window that begins 'now'. // with a validity window that begins 'now'.
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {

View file

@ -10,9 +10,10 @@ import (
"reflect" "reflect"
"strings" "strings"
"go.step.sm/crypto/x509util"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/net/idna" "golang.org/x/net/idna"
"go.step.sm/crypto/x509util"
) )
type NamePolicyReason int type NamePolicyReason int
@ -39,7 +40,7 @@ type NamePolicyError struct {
Detail string Detail string
} }
func (e NamePolicyError) Error() string { func (e *NamePolicyError) Error() string {
switch e.Reason { switch e.Reason {
case NotAuthorizedForThisName: case NotAuthorizedForThisName:
return "not authorized to sign for this name: " + e.Detail return "not authorized to sign for this name: " + e.Detail
@ -295,7 +296,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
// then return error, because DNS should be explicitly configured to be allowed in that case. In case there are // then return error, because DNS should be explicitly configured to be allowed in that case. In case there are
// (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: NotAuthorizedForThisName,
Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns),
} }
@ -307,7 +308,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
} }
parsedDNS, err := idna.Lookup.ToASCII(dns) parsedDNS, err := idna.Lookup.ToASCII(dns)
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), Detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns),
} }
@ -316,7 +317,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
parsedDNS = "*" + parsedDNS parsedDNS = "*" + parsedDNS
} }
if _, ok := domainToReverseLabels(parsedDNS); !ok { if _, ok := domainToReverseLabels(parsedDNS); !ok {
return NamePolicyError{ return &NamePolicyError{
Reason: CannotParseDomain, Reason: CannotParseDomain,
Detail: fmt.Sprintf("cannot parse dns %q", dns), Detail: fmt.Sprintf("cannot parse dns %q", dns),
} }
@ -331,7 +332,7 @@ 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: NotAuthorizedForThisName,
Detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()), Detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()),
} }
@ -346,14 +347,14 @@ 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: NotAuthorizedForThisName,
Detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", 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), Detail: fmt.Sprintf("invalid rfc822Name %q", mailbox),
} }
@ -363,7 +364,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
// https://datatracker.ietf.org/doc/html/rfc5280#section-7.5 // https://datatracker.ietf.org/doc/html/rfc5280#section-7.5
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), Detail: fmt.Sprintf("cannot parse email domain %q", email),
} }
@ -381,7 +382,7 @@ 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: NotAuthorizedForThisName,
Detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()), Detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()),
} }
@ -396,7 +397,7 @@ 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: NotAuthorizedForThisName,
Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal), Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal),
} }
@ -431,14 +432,14 @@ func checkNameConstraints(
constraint := excludedValue.Index(i).Interface() constraint := excludedValue.Index(i).Interface()
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(), Detail: err.Error(),
} }
} }
if match { if match {
return NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAuthorizedForThisName,
Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint),
} }
@ -452,7 +453,7 @@ func checkNameConstraints(
constraint := permittedValue.Index(i).Interface() constraint := permittedValue.Index(i).Interface()
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(), Detail: err.Error(),
} }
@ -464,7 +465,7 @@ func checkNameConstraints(
} }
if !ok { if !ok {
return NamePolicyError{ return &NamePolicyError{
Reason: NotAuthorizedForThisName, Reason: NotAuthorizedForThisName,
Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name),
} }

View file

@ -13,7 +13,7 @@ import (
) )
// TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on // TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on
// TODO(hs): more complex uses cases that combine multiple names and permitted/excluded entries // TODO(hs): more complex use cases that combine multiple names and permitted/excluded entries
func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) {
tests := []struct { tests := []struct {

View file

@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/smallstep/assert" "github.com/smallstep/assert"
) )