Improve how policy errors are returned and used

This commit is contained in:
Herman Slatman 2022-04-04 13:58:16 +02:00
parent d8776d8f7f
commit 96f4c49b0c
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
8 changed files with 559 additions and 55 deletions

View file

@ -20,6 +20,7 @@ import (
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/admin/db/nosql"
"github.com/smallstep/certificates/authority/provisioner"
)
@ -356,3 +357,95 @@ func TestHandler_loadProvisionerByName(t *testing.T) {
})
}
}
func TestHandler_checkAction(t *testing.T) {
type test struct {
adminDB admin.DB
next http.HandlerFunc
supportedInStandalone bool
err *admin.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"standalone-mockdb-supported": func(t *testing.T) test {
err := admin.NewError(admin.ErrorNotImplementedType, "operation not supported")
err.Message = "operation not supported"
return test{
adminDB: &admin.MockDB{},
statusCode: 501,
err: err,
}
},
"standalone-nosql-supported": func(t *testing.T) test {
return test{
supportedInStandalone: true,
adminDB: &nosql.DB{},
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(nil) // mock response with status 200
},
statusCode: 200,
}
},
"standalone-nosql-not-supported": func(t *testing.T) test {
err := admin.NewError(admin.ErrorNotImplementedType, "operation not supported in standalone mode")
err.Message = "operation not supported in standalone mode"
return test{
supportedInStandalone: false,
adminDB: &nosql.DB{},
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(nil) // mock response with status 200
},
statusCode: 501,
err: err,
}
},
"standalone-no-nosql-not-supported": func(t *testing.T) test {
// TODO(hs): temporarily expects an error instead of an OK response
err := admin.NewError(admin.ErrorNotImplementedType, "operation not supported")
err.Message = "operation not supported"
return test{
supportedInStandalone: false,
adminDB: &admin.MockDB{},
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(nil) // mock response with status 200
},
statusCode: 501,
err: err,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
h := &Handler{
adminDB: tc.adminDB,
}
req := httptest.NewRequest("GET", "/foo", nil)
w := httptest.NewRecorder()
h.checkAction(tc.next, tc.supportedInStandalone)(w, req)
res := w.Result()
assert.Equals(t, tc.statusCode, res.StatusCode)
body, err := io.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 {
err := admin.Error{}
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))
assert.Equals(t, tc.err.Type, err.Type)
assert.Equals(t, tc.err.Message, err.Message)
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
assert.Equals(t, tc.err.Detail, err.Detail)
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
return
}
})
}
}

View file

@ -1,12 +1,14 @@
package api
import (
"errors"
"net/http"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/api/read"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/admin"
)
@ -43,11 +45,9 @@ func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB) *PolicyAdmin
func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
policy, err := par.auth.GetAuthorityPolicy(r.Context())
if ae, ok := err.(*admin.Error); ok {
if !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return
}
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return
}
if policy == nil {
@ -85,7 +85,14 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
var createdPolicy *linkedca.Policy
if createdPolicy, err = par.auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy"))
var pe *authority.PolicyError
if errors.As(err, &pe); pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error storing authority policy"))
return
}
render.Error(w, admin.WrapErrorISE(err, "error storing authority policy"))
return
}
@ -118,7 +125,13 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r
var updatedPolicy *linkedca.Policy
if updatedPolicy, err = par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy"))
var pe *authority.PolicyError
if errors.As(err, &pe); pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error updating authority policy"))
return
}
render.Error(w, admin.WrapErrorISE(err, "error updating authority policy"))
return
}
@ -131,11 +144,9 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r
ctx := r.Context()
policy, err := par.auth.GetAuthorityPolicy(ctx)
if ae, ok := err.(*admin.Error); ok {
if !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return
}
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return
}
if policy == nil {
@ -189,7 +200,13 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter,
err := par.auth.UpdateProvisioner(ctx, prov)
if err != nil {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy"))
var pe *authority.PolicyError
if errors.As(err, &pe); pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error creating provisioner policy"))
return
}
render.Error(w, admin.WrapErrorISE(err, "error creating provisioner policy"))
return
}
@ -215,6 +232,12 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter,
prov.Policy = newPolicy
err := par.auth.UpdateProvisioner(ctx, prov)
if err != nil {
var pe *authority.PolicyError
if errors.As(err, &pe); pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error updating provisioner policy"))
return
}
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy"))
return
}
@ -238,7 +261,7 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter,
err := par.auth.UpdateProvisioner(ctx, prov)
if err != nil {
render.Error(w, err)
render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner policy"))
return
}

View file

@ -6,6 +6,7 @@ import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"fmt"
"log"
"strings"
"sync"
@ -235,7 +236,7 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx)
if err != nil {
return admin.WrapErrorISE(err, "error getting policy to (re)load policy engines")
return fmt.Errorf("error getting policy to (re)load policy engines: %w", err)
}
policyOptions = policyToCertificates(linkedPolicy)
} else {

View file

@ -7,11 +7,31 @@ import (
"go.step.sm/linkedca"
"github.com/smallstep/certificates/authority/admin"
authPolicy "github.com/smallstep/certificates/authority/policy"
policy "github.com/smallstep/certificates/policy"
)
type policyErrorType int
const (
_ policyErrorType = iota
AdminLockOut
StoreFailure
ReloadFailure
ConfigurationFailure
EvaluationFailure
InternalFailure
)
type PolicyError struct {
Typ policyErrorType
err error
}
func (p *PolicyError) Error() string {
return p.err.Error()
}
func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
@ -28,16 +48,25 @@ func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
if err := a.checkPolicy(ctx, adm, p); err != nil {
return nil, err
if err := a.checkAuthorityPolicy(ctx, adm, p); err != nil {
return nil, &PolicyError{
Typ: AdminLockOut,
err: err,
}
}
if err := a.adminDB.CreateAuthorityPolicy(ctx, p); err != nil {
return nil, err
return nil, &PolicyError{
Typ: StoreFailure,
err: err,
}
}
if err := a.reloadPolicyEngines(ctx); err != nil {
return nil, admin.WrapErrorISE(err, "error reloading policy engines when creating authority policy")
return nil, &PolicyError{
Typ: ReloadFailure,
err: fmt.Errorf("error reloading policy engines when creating authority policy: %w", err),
}
}
return p, nil // TODO: return the newly stored policy
@ -47,16 +76,22 @@ func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
if err := a.checkPolicy(ctx, adm, p); err != nil {
if err := a.checkAuthorityPolicy(ctx, adm, p); err != nil {
return nil, err
}
if err := a.adminDB.UpdateAuthorityPolicy(ctx, p); err != nil {
return nil, err
return nil, &PolicyError{
Typ: StoreFailure,
err: err,
}
}
if err := a.reloadPolicyEngines(ctx); err != nil {
return nil, admin.WrapErrorISE(err, "error reloading policy engines when updating authority policy")
return nil, &PolicyError{
Typ: ReloadFailure,
err: fmt.Errorf("error reloading policy engines when updating authority policy %w", err),
}
}
return p, nil // TODO: return the updated stored policy
@ -67,19 +102,63 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
defer a.adminMutex.Unlock()
if err := a.adminDB.DeleteAuthorityPolicy(ctx); err != nil {
return err
return &PolicyError{
Typ: StoreFailure,
err: err,
}
}
if err := a.reloadPolicyEngines(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading policy engines when deleting authority policy")
return &PolicyError{
Typ: ReloadFailure,
err: fmt.Errorf("error reloading policy engines when deleting authority policy %w", err),
}
}
return nil
}
func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error {
// no policy and thus nothing to evaluate; return early
if p == nil {
return nil
}
// get all current admins from the database
allAdmins, err := a.adminDB.GetAdmins(ctx)
if err != nil {
return &PolicyError{
Typ: InternalFailure,
err: fmt.Errorf("error retrieving admins: %w", err),
}
}
return a.checkPolicy(ctx, currentAdmin, allAdmins, p)
}
func (a *Authority) checkProvisionerPolicy(ctx context.Context, currentAdmin *linkedca.Admin, provName string, p *linkedca.Policy) error {
// no policy and thus nothing to evaluate; return early
if p == nil {
return nil
}
// get all admins for the provisioner
allProvisionerAdmins, ok := a.admins.LoadByProvisioner(provName)
if !ok {
return &PolicyError{
Typ: InternalFailure,
err: errors.New("error retrieving admins by provisioner"),
}
}
return a.checkPolicy(ctx, currentAdmin, allProvisionerAdmins, p)
}
// checkPolicy checks if a new or updated policy configuration results in the user
// locking themselves or other admins out of the CA.
func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) error {
func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
// convert the policy; return early if nil
policyOptions := policyToCertificates(p)
@ -89,10 +168,13 @@ func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *lin
engine, err := authPolicy.NewX509PolicyEngine(policyOptions.GetX509Options())
if err != nil {
return admin.WrapErrorISE(err, "error creating temporary policy engine")
return &PolicyError{
Typ: ConfigurationFailure,
err: fmt.Errorf("error creating temporary policy engine: %w", err),
}
}
// when an empty policy is provided, the resulting engine is nil
// when an empty X.509 policy is provided, the resulting engine is nil
// and there's no policy to evaluate.
if engine == nil {
return nil
@ -102,23 +184,17 @@ func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *lin
// check if the admin user that instructed the authority policy to be
// created or updated, would still be allowed when the provided policy
// would be applied to the authority.
sans := []string{adm.GetSubject()}
// would be applied.
sans := []string{currentAdmin.GetSubject()}
if err := isAllowed(engine, sans); err != nil {
return err
}
// get all current admins from the database
admins, err := a.adminDB.GetAdmins(ctx)
if err != nil {
return err
}
// loop through admins to verify that none of them would be
// locked out when the new policy were to be applied. Returns
// an error with a message that includes the admin subject that
// would be locked out
for _, adm := range admins {
// would be locked out.
for _, adm := range otherAdmins {
sans = []string{adm.GetSubject()}
if err := isAllowed(engine, sans); err != nil {
return err
@ -137,14 +213,23 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error {
)
if allowed, err = engine.AreSANsAllowed(sans); err != nil {
var policyErr *policy.NamePolicyError
if isPolicyErr := errors.As(err, &policyErr); isPolicyErr && 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)
if errors.As(err, &policyErr); policyErr.Reason == policy.NotAuthorizedForThisName {
return &PolicyError{
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),
}
}
return &PolicyError{
Typ: EvaluationFailure,
err: err,
}
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 &PolicyError{
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),
}
}
return nil

295
authority/policy_test.go Normal file
View file

@ -0,0 +1,295 @@
package authority
import (
"context"
"errors"
"testing"
"github.com/google/go-cmp/cmp"
"go.step.sm/linkedca"
"github.com/smallstep/assert"
authPolicy "github.com/smallstep/certificates/authority/policy"
)
func TestAuthority_checkPolicy(t *testing.T) {
type test struct {
ctx context.Context
currentAdmin *linkedca.Admin
otherAdmins []*linkedca.Admin
policy *linkedca.Policy
err *PolicyError
}
tests := map[string]func(t *testing.T) test{
"fail/NewX509PolicyEngine-error": func(t *testing.T) test {
return test{
ctx: context.Background(),
policy: &linkedca.Policy{
X509: &linkedca.X509Policy{
Allow: &linkedca.X509Names{
Dns: []string{"**.local"},
},
},
},
err: &PolicyError{
Typ: ConfigurationFailure,
err: errors.New("error creating temporary policy engine: cannot parse permitted domain constraint \"**.local\""),
},
}
},
"fail/currentAdmin-evaluation-error": func(t *testing.T) test {
return test{
ctx: context.Background(),
currentAdmin: &linkedca.Admin{Subject: "*"},
otherAdmins: []*linkedca.Admin{},
policy: &linkedca.Policy{
X509: &linkedca.X509Policy{
Allow: &linkedca.X509Names{
Dns: []string{".local"},
},
},
},
err: &PolicyError{
Typ: EvaluationFailure,
err: errors.New("cannot parse domain: dns \"*\" cannot be converted to ASCII"),
},
}
},
"fail/currentAdmin-lockout": func(t *testing.T) test {
return test{
ctx: context.Background(),
currentAdmin: &linkedca.Admin{Subject: "step"},
otherAdmins: []*linkedca.Admin{
{
Subject: "otherAdmin",
},
},
policy: &linkedca.Policy{
X509: &linkedca.X509Policy{
Allow: &linkedca.X509Names{
Dns: []string{".local"},
},
},
},
err: &PolicyError{
Typ: AdminLockOut,
err: errors.New("the provided policy would lock out [step] from the CA. Please update your policy to include [step] as an allowed name"),
},
}
},
"fail/otherAdmins-evaluation-error": func(t *testing.T) test {
return test{
ctx: context.Background(),
currentAdmin: &linkedca.Admin{Subject: "step"},
otherAdmins: []*linkedca.Admin{
{
Subject: "other",
},
{
Subject: "**",
},
},
policy: &linkedca.Policy{
X509: &linkedca.X509Policy{
Allow: &linkedca.X509Names{
Dns: []string{"step", "other", "*.local"},
},
},
},
err: &PolicyError{
Typ: EvaluationFailure,
err: errors.New("cannot parse domain: dns \"**\" cannot be converted to ASCII"),
},
}
},
"fail/otherAdmins-lockout": func(t *testing.T) test {
return test{
ctx: context.Background(),
currentAdmin: &linkedca.Admin{Subject: "step"},
otherAdmins: []*linkedca.Admin{
{
Subject: "otherAdmin",
},
},
policy: &linkedca.Policy{
X509: &linkedca.X509Policy{
Allow: &linkedca.X509Names{
Dns: []string{"step"},
},
},
},
err: &PolicyError{
Typ: AdminLockOut,
err: errors.New("the provided policy would lock out [otherAdmin] from the CA. Please update your policy to include [otherAdmin] as an allowed name"),
},
}
},
"ok/no-policy": func(t *testing.T) test {
return test{
ctx: context.Background(),
currentAdmin: &linkedca.Admin{Subject: "step"},
otherAdmins: []*linkedca.Admin{},
policy: nil,
}
},
"ok/empty-policy": func(t *testing.T) test {
return test{
ctx: context.Background(),
currentAdmin: &linkedca.Admin{Subject: "step"},
otherAdmins: []*linkedca.Admin{},
policy: &linkedca.Policy{
X509: &linkedca.X509Policy{
Allow: &linkedca.X509Names{
Dns: []string{},
},
},
},
}
},
"ok/policy": func(t *testing.T) test {
return test{
ctx: context.Background(),
currentAdmin: &linkedca.Admin{Subject: "step"},
otherAdmins: []*linkedca.Admin{
{
Subject: "otherAdmin",
},
},
policy: &linkedca.Policy{
X509: &linkedca.X509Policy{
Allow: &linkedca.X509Names{
Dns: []string{"step", "otherAdmin"},
},
},
},
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
a := &Authority{}
err := a.checkPolicy(tc.ctx, tc.currentAdmin, tc.otherAdmins, tc.policy)
if tc.err == nil {
assert.Nil(t, err)
} else {
assert.Type(t, &PolicyError{}, err)
pe, ok := err.(*PolicyError)
assert.Fatal(t, ok)
assert.Equals(t, tc.err.Typ, pe.Typ)
assert.Equals(t, tc.err.Error(), pe.Error())
}
})
}
}
func Test_policyToCertificates(t *testing.T) {
tests := []struct {
name string
policy *linkedca.Policy
want *authPolicy.Options
}{
{
name: "no-policy",
policy: nil,
want: nil,
},
{
name: "full-policy",
policy: &linkedca.Policy{
X509: &linkedca.X509Policy{
Allow: &linkedca.X509Names{
Dns: []string{"step"},
Ips: []string{"127.0.0.1/24"},
Emails: []string{"*.example.com"},
Uris: []string{"https://*.local"},
},
Deny: &linkedca.X509Names{
Dns: []string{"bad"},
Ips: []string{"127.0.0.30"},
Emails: []string{"badhost.example.com"},
Uris: []string{"https://badhost.local"},
},
},
Ssh: &linkedca.SSHPolicy{
Host: &linkedca.SSHHostPolicy{
Allow: &linkedca.SSHHostNames{
Dns: []string{"*.localhost"},
Ips: []string{"127.0.0.1/24"},
Principals: []string{"user"},
},
Deny: &linkedca.SSHHostNames{
Dns: []string{"badhost.localhost"},
Ips: []string{"127.0.0.40"},
Principals: []string{"root"},
},
},
User: &linkedca.SSHUserPolicy{
Allow: &linkedca.SSHUserNames{
Emails: []string{"@work"},
Principals: []string{"user"},
},
Deny: &linkedca.SSHUserNames{
Emails: []string{"root@work"},
Principals: []string{"root"},
},
},
},
},
want: &authPolicy.Options{
X509: &authPolicy.X509PolicyOptions{
AllowedNames: &authPolicy.X509NameOptions{
DNSDomains: []string{"step"},
IPRanges: []string{"127.0.0.1/24"},
EmailAddresses: []string{"*.example.com"},
URIDomains: []string{"https://*.local"},
},
DeniedNames: &authPolicy.X509NameOptions{
DNSDomains: []string{"bad"},
IPRanges: []string{"127.0.0.30"},
EmailAddresses: []string{"badhost.example.com"},
URIDomains: []string{"https://badhost.local"},
},
},
SSH: &authPolicy.SSHPolicyOptions{
Host: &authPolicy.SSHHostCertificateOptions{
AllowedNames: &authPolicy.SSHNameOptions{
DNSDomains: []string{"*.localhost"},
IPRanges: []string{"127.0.0.1/24"},
Principals: []string{"user"},
},
DeniedNames: &authPolicy.SSHNameOptions{
DNSDomains: []string{"badhost.localhost"},
IPRanges: []string{"127.0.0.40"},
Principals: []string{"root"},
},
},
User: &authPolicy.SSHUserCertificateOptions{
AllowedNames: &authPolicy.SSHNameOptions{
EmailAddresses: []string{"@work"},
Principals: []string{"user"},
},
DeniedNames: &authPolicy.SSHNameOptions{
EmailAddresses: []string{"root@work"},
Principals: []string{"root"},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := policyToCertificates(tt.policy)
if !cmp.Equal(tt.want, got) {
t.Errorf("policyToCertificates() diff=\n%s", cmp.Diff(tt.want, got))
}
})
}
}

View file

@ -141,6 +141,12 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi
return admin.WrapErrorISE(err, "error generating provisioner config")
}
adm := linkedca.AdminFromContext(ctx)
if err := a.checkProvisionerPolicy(ctx, adm, prov.Name, prov.Policy); err != nil {
return err
}
if err := certProv.Init(provisionerConfig); err != nil {
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %s", prov.Name)
}
@ -186,6 +192,12 @@ func (a *Authority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisio
return admin.WrapErrorISE(err, "error generating provisioner config")
}
adm := linkedca.AdminFromContext(ctx)
if err := a.checkProvisionerPolicy(ctx, adm, nu.Name, nu.Policy); err != nil {
return err
}
if err := certProv.Init(provisionerConfig); err != nil {
return admin.WrapErrorISE(err, "error initializing provisioner %s", nu.Name)
}
@ -424,12 +436,14 @@ func optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options {
ops.SSH.Host.AllowedNames = &policy.SSHNameOptions{
DNSDomains: p.Policy.Ssh.Host.Allow.Dns,
IPRanges: p.Policy.Ssh.Host.Allow.Ips,
Principals: p.Policy.Ssh.Host.Allow.Principals,
}
}
if p.Policy.Ssh.Host.Deny != nil {
ops.SSH.Host.DeniedNames = &policy.SSHNameOptions{
DNSDomains: p.Policy.Ssh.Host.Deny.Dns,
IPRanges: p.Policy.Ssh.Host.Deny.Ips,
Principals: p.Policy.Ssh.Host.Deny.Principals,
}
}
}

View file

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

View file

@ -1,6 +1,9 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// The code in this file is an adapted version of the code in
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
package policy
import (
@ -15,8 +18,6 @@ import (
)
// validateNames verifies that all names are allowed.
// Its logic follows that of (a large part of) the (c *Certificate) isValid() function
// in https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailAddresses []string, uris []*url.URL, principals []string) error {
// nothing to compare against; return early
@ -160,7 +161,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
// checkNameConstraints checks that a name, of type nameType is permitted.
// The argument parsedName contains the parsed form of name, suitable for passing
// to the match function.
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
func checkNameConstraints(
nameType string,
name string,
@ -218,7 +218,6 @@ func checkNameConstraints(
// domainToReverseLabels converts a textual domain name like foo.example.com to
// the list of labels in reverse order, e.g. ["com", "example", "foo"].
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
for len(domain) > 0 {
if i := strings.LastIndexByte(domain, '.'); i == -1 {
@ -255,7 +254,6 @@ func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
// rfc2821Mailbox represents a “mailbox” (which is an email address to most
// people) by breaking it into the “local” (i.e. before the '@') and “domain”
// parts.
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
type rfc2821Mailbox struct {
local, domain string
}
@ -264,7 +262,6 @@ type rfc2821Mailbox struct {
// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,
// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The
// format of an rfc822Name is a "Mailbox" as defined in RFC 2821, Section 4.1.2”.
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
if in == "" {
return mailbox, false
@ -401,7 +398,7 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
return mailbox, true
}
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
// matchDomainConstraint matches a domain agains the given constraint
func (e *NamePolicyEngine) matchDomainConstraint(domain, constraint string) (bool, error) {
// The meaning of zero length constraints is not specified, but this
// code follows NSS and accepts them as matching everything.
@ -462,10 +459,6 @@ func (e *NamePolicyEngine) matchDomainConstraint(domain, constraint string) (boo
return false, fmt.Errorf("cannot parse domain constraint %q", constraint)
}
// fmt.Println(mustHaveSubdomains)
// fmt.Println(constraintLabels)
// fmt.Println(domainLabels)
expectedNumberOfLabels := len(constraintLabels)
if mustHaveSubdomains {
// we expect exactly one more label if it starts with the "canonical" x509 "wildcard": "."
@ -532,7 +525,7 @@ func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constrai
return e.matchDomainConstraint(mailbox.domain, constraint)
}
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
// matchURIConstraint matches an URL against a constraint
func (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
// From RFC 5280, Section 4.2.1.10:
// “a uniformResourceIdentifier that does not include an authority