From 9539729bd9b47f6581d59a7399d5fae8a3cf92f5 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 3 Jan 2022 12:25:24 +0100 Subject: [PATCH 01/78] Add initial implementation of x509 and SSH allow/deny policy engine --- .golangci.yml | 1 + acme/api/middleware.go | 2 +- acme/api/order.go | 19 + acme/common.go | 26 +- authority/authorize_test.go | 4 +- authority/provisioner/acme.go | 52 +- authority/provisioner/acme_test.go | 4 +- authority/provisioner/aws.go | 14 + authority/provisioner/aws_test.go | 12 +- authority/provisioner/azure.go | 14 + authority/provisioner/azure_test.go | 8 +- authority/provisioner/gcp.go | 14 + authority/provisioner/gcp_test.go | 8 +- authority/provisioner/jwk.go | 14 + authority/provisioner/jwk_test.go | 4 +- authority/provisioner/k8sSA.go | 14 + authority/provisioner/k8sSA_test.go | 12 +- authority/provisioner/oidc.go | 15 + authority/provisioner/oidc_test.go | 4 +- authority/provisioner/options.go | 58 +++ authority/provisioner/policy.go | 68 +++ authority/provisioner/provisioner.go | 7 +- authority/provisioner/scep.go | 8 +- authority/provisioner/sign_options.go | 27 ++ authority/provisioner/sign_ssh_options.go | 30 ++ authority/provisioner/ssh_options.go | 54 +++ authority/provisioner/sshpop.go | 3 + authority/provisioner/utils_test.go | 9 + authority/provisioner/x5c.go | 14 + authority/provisioner/x5c_test.go | 11 +- policy/ssh/options.go | 99 ++++ policy/ssh/ssh.go | 472 ++++++++++++++++++ policy/x509/options.go | 506 +++++++++++++++++++ policy/x509/x509.go | 565 ++++++++++++++++++++++ policy/x509/x509_test.go | 299 ++++++++++++ 35 files changed, 2431 insertions(+), 40 deletions(-) mode change 100644 => 100755 acme/api/order.go mode change 100644 => 100755 authority/provisioner/acme.go mode change 100644 => 100755 authority/provisioner/jwk.go mode change 100644 => 100755 authority/provisioner/options.go create mode 100644 authority/provisioner/policy.go mode change 100644 => 100755 authority/provisioner/sign_options.go create mode 100644 policy/ssh/options.go create mode 100644 policy/ssh/ssh.go create mode 100755 policy/x509/options.go create mode 100755 policy/x509/x509.go create mode 100755 policy/x509/x509_test.go diff --git a/.golangci.yml b/.golangci.yml index 67aac2df..59c58490 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -73,3 +73,4 @@ issues: - error strings should not be capitalized or end with punctuation or a newline - Wrapf call needs 1 arg but has 2 args - cs.NegotiatedProtocolIsMutual is deprecated + - rewrite if-else to switch statement diff --git a/acme/api/middleware.go b/acme/api/middleware.go index d701f240..b826d1fa 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -283,7 +283,7 @@ func (h *Handler) extractJWK(next nextHTTP) nextHTTP { } // lookupProvisioner loads the provisioner associated with the request. -// Responsds 404 if the provisioner does not exist. +// Responds 404 if the provisioner does not exist. func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/acme/api/order.go b/acme/api/order.go old mode 100644 new mode 100755 index 9cf2c1eb..3d22ec0f --- a/acme/api/order.go +++ b/acme/api/order.go @@ -35,6 +35,8 @@ func (n *NewOrderRequest) Validate() error { if id.Type == acme.IP && net.ParseIP(id.Value) == nil { return acme.NewError(acme.ErrorMalformedType, "invalid IP address: %s", id.Value) } + // TODO: add some validations for DNS domains? + // TODO: combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1 } return nil } @@ -83,6 +85,7 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { api.WriteError(w, err) return } + var nor NewOrderRequest if err := json.Unmarshal(payload.value, &nor); err != nil { api.WriteError(w, acme.WrapError(acme.ErrorMalformedType, err, @@ -95,6 +98,22 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { return } + // TODO(hs): this should also verify rules set in the Account (i.e. allowed/denied + // DNS and IPs; it's probably good to connect those to the EAB credentials and management? Or + // should we do it fully properly and connect them to the Account directly? The latter would allow + // 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 + // thus make sense to also set the allowed/denied names based on that info. + + 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? + err = prov.AuthorizeOrderIdentifier(ctx, identifier.Value) + if err != nil { + api.WriteError(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) + return + } + } + now := clock.Now() // New order. o := &acme.Order{ diff --git a/acme/common.go b/acme/common.go index 0c9e83dc..4b086dd7 100644 --- a/acme/common.go +++ b/acme/common.go @@ -30,6 +30,7 @@ var clock Clock // Provisioner is an interface that implements a subset of the provisioner.Interface -- // only those methods required by the ACME api/authority. type Provisioner interface { + AuthorizeOrderIdentifier(ctx context.Context, identifier string) error AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) AuthorizeRevoke(ctx context.Context, token string) error GetID() string @@ -40,14 +41,15 @@ type Provisioner interface { // MockProvisioner for testing type MockProvisioner struct { - Mret1 interface{} - Merr error - MgetID func() string - MgetName func() string - MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error) - MauthorizeRevoke func(ctx context.Context, token string) error - MdefaultTLSCertDuration func() time.Duration - MgetOptions func() *provisioner.Options + Mret1 interface{} + Merr error + MgetID func() string + MgetName func() string + MauthorizeOrderIdentifier func(ctx context.Context, identifier string) error + MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error) + MauthorizeRevoke func(ctx context.Context, token string) error + MdefaultTLSCertDuration func() time.Duration + MgetOptions func() *provisioner.Options } // GetName mock @@ -58,6 +60,14 @@ func (m *MockProvisioner) GetName() string { return m.Mret1.(string) } +// AuthorizeOrderIdentifiers mock +func (m *MockProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier string) error { + if m.MauthorizeOrderIdentifier != nil { + return m.MauthorizeOrderIdentifier(ctx, identifier) + } + return m.Merr +} + // AuthorizeSign mock func (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) { if m.MauthorizeSign != nil { diff --git a/authority/authorize_test.go b/authority/authorize_test.go index 6d524a25..08090e22 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -483,7 +483,7 @@ func TestAuthority_authorizeSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Len(t, 7, got) + assert.Len(t, 8, got) // number of provisioner.SignOptions returned } } }) @@ -995,7 +995,7 @@ func TestAuthority_authorizeSSHSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Len(t, 7, got) + assert.Len(t, 8, got) // number of provisioner.SignOptions returned } } }) diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go old mode 100644 new mode 100755 index c8950568..c6cadf51 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -3,6 +3,7 @@ package provisioner import ( "context" "crypto/x509" + "net" "time" "github.com/pkg/errors" @@ -67,8 +68,9 @@ func (p *ACME) DefaultTLSCertDuration() time.Duration { return p.claimer.DefaultTLSCertDuration() } -// Init initializes and validates the fields of a JWK type. +// Init initializes and validates the fields of an ACME type. func (p *ACME) Init(config Config) (err error) { + p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -81,6 +83,47 @@ func (p *ACME) Init(config Config) (err error) { return err } + // Initialize the x509 allow/deny policy engine + // TODO(hs): ensure no race conditions happen when reloading settings and requesting certs? + // TODO(hs): implement memoization strategy, so that reloading is not required when no changes were made to allow/deny? + if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + return err + } + + return nil +} + +// ACMEIdentifierType encodes ACME Identifier types +type ACMEIdentifierType string + +const ( + // IP is the ACME ip identifier type + IP ACMEIdentifierType = "ip" + // DNS is the ACME dns identifier type + DNS ACMEIdentifierType = "dns" +) + +// ACMEIdentifier encodes ACME Order Identifiers +type ACMEIdentifier struct { + Type ACMEIdentifierType + Value string +} + +// AuthorizeOrderIdentifiers verifies the provisioner is authorized to issue a +// certificate for the Identifiers provided in an Order. +func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier string) error { + + if p.x509PolicyEngine == nil { + return nil + } + + var err error + if ip := net.ParseIP(identifier); ip != nil { + _, err = p.x509PolicyEngine.IsIPAllowed(ip) + } else { + _, err = p.x509PolicyEngine.IsDNSAllowed(identifier) + } + return err } @@ -88,7 +131,7 @@ func (p *ACME) Init(config Config) (err error) { // in the ACME protocol. This method returns a list of modifiers / constraints // on the resulting certificate. func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { - return []SignOption{ + opts := []SignOption{ // modifiers / withOptions newProvisionerExtensionOption(TypeACME, p.Name, ""), newForceCNOption(p.ForceCN), @@ -96,7 +139,10 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - }, nil + newX509NamePolicyValidator(p.x509PolicyEngine), + } + + return opts, nil } // AuthorizeRevoke is called just before the certificate is to be revoked by diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index bd173f87..b9f52253 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -168,7 +168,7 @@ func TestACME_AuthorizeSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) && assert.NotNil(t, opts) { - assert.Len(t, 5, opts) + assert.Len(t, 6, opts) // number of SignOptions returned for _, o := range opts { switch v := o.(type) { case *provisionerExtensionOption: @@ -184,6 +184,8 @@ func TestACME_AuthorizeSign(t *testing.T) { case *validityValidator: assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration()) assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration()) + case *x509NamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index fdad7b4a..9f542873 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -392,6 +392,7 @@ func (p *AWS) GetIdentityToken(subject, caURL string) (string, error) { // Init validates and initializes the AWS provisioner. func (p *AWS) Init(config Config) (err error) { + p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -425,6 +426,16 @@ func (p *AWS) Init(config Config) (err error) { } } + // Initialize the x509 allow/deny policy engine + if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine + if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + return nil } @@ -478,6 +489,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, commonNameValidator(payload.Claims.Subject), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(p.x509PolicyEngine), ), nil } @@ -759,5 +771,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, + // Ensure that all principal names are allowed + newSSHNamePolicyValidator(p.sshPolicyEngine), ), nil } diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 0d2786db..beef8642 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -641,11 +641,11 @@ func TestAWS_AuthorizeSign(t *testing.T) { code int wantErr bool }{ - {"ok", p1, args{t1, "foo.local"}, 6, http.StatusOK, false}, - {"ok", p2, args{t2, "instance-id"}, 10, http.StatusOK, false}, - {"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 10, http.StatusOK, false}, - {"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 10, http.StatusOK, false}, - {"ok", p1, args{t4, "instance-id"}, 6, http.StatusOK, false}, + {"ok", p1, args{t1, "foo.local"}, 7, http.StatusOK, false}, + {"ok", p2, args{t2, "instance-id"}, 11, http.StatusOK, false}, + {"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 11, http.StatusOK, false}, + {"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 11, http.StatusOK, false}, + {"ok", p1, args{t4, "instance-id"}, 7, http.StatusOK, false}, {"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true}, {"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true}, {"fail subject", p1, args{token: failSubject}, 0, http.StatusUnauthorized, true}, @@ -697,6 +697,8 @@ func TestAWS_AuthorizeSign(t *testing.T) { assert.Equals(t, v, nil) case dnsNamesValidator: assert.Equals(t, []string(v), []string{"ip-127-0-0-1.us-west-1.compute.internal"}) + case *x509NamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 55d77f49..b8bbe143 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -191,6 +191,7 @@ func (p *Azure) GetIdentityToken(subject, caURL string) (string, error) { // Init validates and initializes the Azure provisioner. func (p *Azure) Init(config Config) (err error) { + p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -221,6 +222,16 @@ func (p *Azure) Init(config Config) (err error) { return err } + // Initialize the x509 allow/deny policy engine + if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine + if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + return nil } @@ -328,6 +339,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // validators defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(p.x509PolicyEngine), ), nil } @@ -396,6 +408,8 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, + // Ensure that all principal names are allowed + newSSHNamePolicyValidator(p.sshPolicyEngine), ), nil } diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index 7f8d6017..7e184a27 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -431,9 +431,9 @@ func TestAzure_AuthorizeSign(t *testing.T) { code int wantErr bool }{ - {"ok", p1, args{t1}, 5, http.StatusOK, false}, - {"ok", p2, args{t2}, 10, http.StatusOK, false}, - {"ok", p1, args{t11}, 5, http.StatusOK, false}, + {"ok", p1, args{t1}, 6, http.StatusOK, false}, + {"ok", p2, args{t2}, 11, http.StatusOK, false}, + {"ok", p1, args{t11}, 6, http.StatusOK, false}, {"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true}, {"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true}, {"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true}, @@ -480,6 +480,8 @@ func TestAzure_AuthorizeSign(t *testing.T) { assert.Equals(t, v, nil) case dnsNamesValidator: assert.Equals(t, []string(v), []string{"virtualMachine"}) + case *x509NamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index e46f4ce4..4c7f2046 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -195,6 +195,7 @@ func (p *GCP) GetIdentityToken(subject, caURL string) (string, error) { // Init validates and initializes the GCP provisioner. func (p *GCP) Init(config Config) error { + p.base = &base{} // prevent nil pointers var err error switch { case p.Type == "": @@ -216,6 +217,16 @@ func (p *GCP) Init(config Config) error { return err } + // Initialize the x509 allow/deny policy engine + if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine + if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + p.audiences = config.Audiences.WithFragment(p.GetIDForToken()) return nil } @@ -273,6 +284,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // validators defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(p.x509PolicyEngine), ), nil } @@ -438,5 +450,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, + // Ensure that all principal names are allowed + newSSHNamePolicyValidator(p.sshPolicyEngine), ), nil } diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index 5f6f9bc7..8c54c4c5 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -515,9 +515,9 @@ func TestGCP_AuthorizeSign(t *testing.T) { code int wantErr bool }{ - {"ok", p1, args{t1}, 5, http.StatusOK, false}, - {"ok", p2, args{t2}, 10, http.StatusOK, false}, - {"ok", p3, args{t3}, 5, http.StatusOK, false}, + {"ok", p1, args{t1}, 6, http.StatusOK, false}, + {"ok", p2, args{t2}, 11, http.StatusOK, false}, + {"ok", p3, args{t3}, 6, http.StatusOK, false}, {"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true}, {"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true}, {"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true}, @@ -569,6 +569,8 @@ func TestGCP_AuthorizeSign(t *testing.T) { assert.Equals(t, v, nil) case dnsNamesValidator: assert.Equals(t, []string(v), []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}) + case *x509NamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go old mode 100644 new mode 100755 index 137915c8..081eb60c --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -89,6 +89,7 @@ func (p *JWK) GetEncryptedKey() (string, string, bool) { // Init initializes and validates the fields of a JWK type. func (p *JWK) Init(config Config) (err error) { + p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -103,6 +104,16 @@ func (p *JWK) Init(config Config) (err error) { return err } + // Initialize the x509 allow/deny policy engine + if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine + if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + p.audiences = config.Audiences return err } @@ -185,6 +196,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, defaultSANsValidator(claims.SANs), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(p.x509PolicyEngine), }, nil } @@ -268,6 +280,8 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshCertValidityValidator{p.claimer}, // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, + // Ensure that all principal names are allowed + newSSHNamePolicyValidator(p.sshPolicyEngine), ), nil } diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index deae8f7a..cb43627b 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -295,7 +295,7 @@ func TestJWK_AuthorizeSign(t *testing.T) { } } else { if assert.NotNil(t, got) { - assert.Len(t, 7, got) + assert.Len(t, 8, got) for _, o := range got { switch v := o.(type) { case certificateOptionsFunc: @@ -314,6 +314,8 @@ func TestJWK_AuthorizeSign(t *testing.T) { assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) case defaultSANsValidator: assert.Equals(t, []string(v), tt.sans) + case *x509NamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index d260f5ec..707e141e 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -92,6 +92,7 @@ func (p *K8sSA) GetEncryptedKey() (string, string, bool) { // Init initializes and validates the fields of a K8sSA type. func (p *K8sSA) Init(config Config) (err error) { + p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -143,6 +144,16 @@ func (p *K8sSA) Init(config Config) (err error) { return err } + // Initialize the x509 allow/deny policy engine + if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine + if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + p.audiences = config.Audiences return err } @@ -244,6 +255,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // validators defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(p.x509PolicyEngine), }, nil } @@ -289,6 +301,8 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio &sshCertValidityValidator{p.claimer}, // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, + // Ensure that all principal names are allowed + newSSHNamePolicyValidator(p.sshPolicyEngine), ), nil } diff --git a/authority/provisioner/k8sSA_test.go b/authority/provisioner/k8sSA_test.go index 176cdfd3..3ccce461 100644 --- a/authority/provisioner/k8sSA_test.go +++ b/authority/provisioner/k8sSA_test.go @@ -271,7 +271,6 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { } else { if assert.Nil(t, tc.err) { if assert.NotNil(t, opts) { - tot := 0 for _, o := range opts { switch v := o.(type) { case certificateOptionsFunc: @@ -286,12 +285,13 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { case *validityValidator: assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration()) assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration()) + case *x509NamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } - tot++ } - assert.Equals(t, tot, 5) + assert.Len(t, 6, opts) } } } @@ -358,7 +358,7 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) { } else { if assert.Nil(t, tc.err) { if assert.NotNil(t, opts) { - tot := 0 + assert.Len(t, 7, opts) for _, o := range opts { switch v := o.(type) { case sshCertificateOptionsFunc: @@ -370,12 +370,12 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) { case *sshCertDefaultValidator: case *sshDefaultDuration: assert.Equals(t, v.Claimer, tc.p.claimer) + case *sshNamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } - tot++ } - assert.Equals(t, tot, 6) } } } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index ac1f2a25..707f8228 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -154,6 +154,7 @@ func (o *OIDC) GetEncryptedKey() (kid, key string, ok bool) { // Init validates and initializes the OIDC provider. func (o *OIDC) Init(config Config) (err error) { + o.base = &base{} // prevent nil pointers switch { case o.Type == "": return errors.New("type cannot be empty") @@ -207,6 +208,17 @@ func (o *OIDC) Init(config Config) (err error) { } else { o.getIdentityFunc = config.GetIdentityFunc } + + // Initialize the x509 allow/deny policy engine + if o.x509PolicyEngine, err = newX509PolicyEngine(o.Options.GetX509Options()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine + if o.sshPolicyEngine, err = newSSHPolicyEngine(o.Options.GetSSHOptions()); err != nil { + return err + } + return nil } @@ -363,6 +375,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators defaultPublicKeyValidator{}, newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(o.x509PolicyEngine), }, nil } @@ -452,6 +465,8 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption &sshCertValidityValidator{o.claimer}, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, + // Ensure that all principal names are allowed + newSSHNamePolicyValidator(o.sshPolicyEngine), ), nil } diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index 7bf6ad7a..92d4ca95 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -322,7 +322,7 @@ func TestOIDC_AuthorizeSign(t *testing.T) { assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else if assert.NotNil(t, got) { - assert.Len(t, 5, got) + assert.Len(t, 6, got) for _, o := range got { switch v := o.(type) { case certificateOptionsFunc: @@ -339,6 +339,8 @@ func TestOIDC_AuthorizeSign(t *testing.T) { assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) case emailOnlyIdentity: assert.Equals(t, string(v), "name@smallstep.com") + case *x509NamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go old mode 100644 new mode 100755 index f86c4863..7c516f6d --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -56,6 +56,12 @@ type X509Options struct { // TemplateData is a JSON object with variables that can be used in custom // templates. TemplateData json.RawMessage `json:"templateData,omitempty"` + + // AllowedNames contains the SANs the provisioner is authorized to sign + AllowedNames *AllowedX509NameOptions `json:"allow,omitempty"` + + // DeniedNames contains the SANs the provisioner is not authorized to sign + DeniedNames *DeniedX509NameOptions `json:"deny,omitempty"` } // HasTemplate returns true if a template is defined in the provisioner options. @@ -63,6 +69,58 @@ func (o *X509Options) HasTemplate() bool { return o != nil && (o.Template != "" || o.TemplateFile != "") } +// GetAllowedNameOptions returns the AllowedNameOptions, which models the +// SANs that a provisioner is authorized to sign x509 certificates for. +func (o *X509Options) GetAllowedNameOptions() *AllowedX509NameOptions { + if o == nil { + return nil + } + return o.AllowedNames +} + +// GetDeniedNameOptions returns the DeniedNameOptions, which models the +// SANs that a provisioner is NOT authorized to sign x509 certificates for. +func (o *X509Options) GetDeniedNameOptions() *DeniedX509NameOptions { + if o == nil { + return nil + } + return o.DeniedNames +} + +// AllowedX509NameOptions models the allowed names +type AllowedX509NameOptions struct { + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` // TODO(hs): support IPs as well as ranges + EmailAddresses []string `json:"email,omitempty"` + URIDomains []string `json:"uri,omitempty"` +} + +// DeniedX509NameOptions models the denied names +type DeniedX509NameOptions struct { + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` // TODO(hs): support IPs as well as ranges + EmailAddresses []string `json:"email,omitempty"` + URIDomains []string `json:"uri,omitempty"` +} + +// HasNames checks if the AllowedNameOptions has one or more +// names configured. +func (o *AllowedX509NameOptions) HasNames() bool { + return len(o.DNSDomains) > 0 || + len(o.IPRanges) > 0 || + len(o.EmailAddresses) > 0 || + len(o.URIDomains) > 0 +} + +// HasNames checks if the DeniedNameOptions has one or more +// names configured. +func (o *DeniedX509NameOptions) HasNames() bool { + return len(o.DNSDomains) > 0 || + len(o.IPRanges) > 0 || + len(o.EmailAddresses) > 0 || + len(o.URIDomains) > 0 +} + // TemplateOptions generates a CertificateOptions with the template and data // defined in the ProvisionerOptions, the provisioner generated data, and the // user data provided in the request. If no template has been provided, diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go new file mode 100644 index 00000000..cf436d70 --- /dev/null +++ b/authority/provisioner/policy.go @@ -0,0 +1,68 @@ +package provisioner + +import ( + sshpolicy "github.com/smallstep/certificates/policy/ssh" + x509policy "github.com/smallstep/certificates/policy/x509" +) + +// newX509PolicyEngine creates a new x509 name policy engine +func newX509PolicyEngine(x509Opts *X509Options) (*x509policy.NamePolicyEngine, error) { + + if x509Opts == nil { + return nil, nil + } + + options := []x509policy.NamePolicyOption{} + + allowed := x509Opts.GetAllowedNameOptions() + if allowed != nil && allowed.HasNames() { + options = append(options, + x509policy.WithPermittedDNSDomains(allowed.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. + x509policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges + x509policy.WithPermittedEmailAddresses(allowed.EmailAddresses), + x509policy.WithPermittedURIDomains(allowed.URIDomains), + ) + } + + denied := x509Opts.GetDeniedNameOptions() + if denied != nil && denied.HasNames() { + options = append(options, + x509policy.WithExcludedDNSDomains(denied.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. + x509policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges + x509policy.WithExcludedEmailAddresses(denied.EmailAddresses), + x509policy.WithExcludedURIDomains(denied.URIDomains), + ) + } + + return x509policy.New(options...) +} + +// newSSHPolicyEngine creates a new SSH name policy engine +func newSSHPolicyEngine(sshOpts *SSHOptions) (*sshpolicy.NamePolicyEngine, error) { + + if sshOpts == nil { + return nil, nil + } + + options := []sshpolicy.NamePolicyOption{} + + allowed := sshOpts.GetAllowedNameOptions() + if allowed != nil && allowed.HasNames() { + options = append(options, + sshpolicy.WithPermittedDNSDomains(allowed.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. + sshpolicy.WithPermittedEmailAddresses(allowed.EmailAddresses), + sshpolicy.WithPermittedPrincipals(allowed.Principals), + ) + } + + denied := sshOpts.GetDeniedNameOptions() + if denied != nil && denied.HasNames() { + options = append(options, + sshpolicy.WithExcludedDNSDomains(denied.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. + sshpolicy.WithExcludedEmailAddresses(denied.EmailAddresses), + sshpolicy.WithExcludedPrincipals(denied.Principals), + ) + } + + return sshpolicy.New(options...) +} diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 5d6b2f80..34ea8c4d 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -12,6 +12,8 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" + sshpolicy "github.com/smallstep/certificates/policy/ssh" + x509policy "github.com/smallstep/certificates/policy/x509" "golang.org/x/crypto/ssh" ) @@ -298,7 +300,10 @@ func SanitizeSSHUserPrincipal(email string) string { }, strings.ToLower(email)) } -type base struct{} +type base struct { + x509PolicyEngine *x509policy.NamePolicyEngine + sshPolicyEngine *sshpolicy.NamePolicyEngine +} // AuthorizeSign returns an unimplemented error. Provisioners should overwrite // this method if they will support authorizing tokens for signing x509 Certificates. diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index 145a1920..7c78d14b 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -74,7 +74,7 @@ func (s *SCEP) DefaultTLSCertDuration() time.Duration { // Init initializes and validates the fields of a SCEP type. func (s *SCEP) Init(config Config) (err error) { - + s.base = &base{} // prevent nil pointers switch { case s.Type == "": return errors.New("provisioner type cannot be empty") @@ -102,6 +102,11 @@ func (s *SCEP) Init(config Config) (err error) { // TODO: add other, SCEP specific, options? + // Initialize the x509 allow/deny policy engine + if s.x509PolicyEngine, err = newX509PolicyEngine(s.Options.GetX509Options()); err != nil { + return err + } + return err } @@ -117,6 +122,7 @@ func (s *SCEP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength), newValidityValidator(s.claimer.MinTLSCertDuration(), s.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(s.x509PolicyEngine), }, nil } diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go old mode 100644 new mode 100755 index 34b2e99b..ccc55435 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -16,6 +16,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + x509policy "github.com/smallstep/certificates/policy/x509" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" ) @@ -404,6 +405,32 @@ func (v *validityValidator) Valid(cert *x509.Certificate, o SignOptions) error { return nil } +// x509NamePolicyValidator validates that the certificate (to be signed) +// contains only allowed SANs. +type x509NamePolicyValidator struct { + policyEngine *x509policy.NamePolicyEngine +} + +// newX509NamePolicyValidator return a new SANs allow/deny validator. +func newX509NamePolicyValidator(engine *x509policy.NamePolicyEngine) *x509NamePolicyValidator { + return &x509NamePolicyValidator{ + policyEngine: engine, + } +} + +// Valid validates validates that the certificate (to be signed) +// contains only allowed SANs. +func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) error { + if v.policyEngine == nil { + return nil + } + _, err := v.policyEngine.AreCertificateNamesAllowed(cert) + if err != nil { + return err + } + return nil +} + var ( stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} stepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 1)...) diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index a2ca78b1..e5bd2121 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + sshpolicy "github.com/smallstep/certificates/policy/ssh" "go.step.sm/crypto/keyutil" "golang.org/x/crypto/ssh" ) @@ -444,6 +445,35 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SignSSHOpti } } +// sshNamePolicyValidator validates that the certificate (to be signed) +// contains only allowed principals. +type sshNamePolicyValidator struct { + policyEngine *sshpolicy.NamePolicyEngine +} + +// newSSHNamePolicyValidator return a new SSH allow/deny validator. +func newSSHNamePolicyValidator(engine *sshpolicy.NamePolicyEngine) *sshNamePolicyValidator { + return &sshNamePolicyValidator{ + policyEngine: engine, + } +} + +// Valid validates validates that the certificate (to be signed) +// contains only allowed principals. +func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error { + if v.policyEngine == nil { + return nil + } + // TODO(hs): should this perform checks only for hosts vs. user certs depending on context? + // The current best practice is to have separate provisioners for hosts and users, and thus + // separate policy engines for the principals that are allowed. + _, err := v.policyEngine.ArePrincipalsAllowed(cert) + if err != nil { + return err + } + return nil +} + // sshCertTypeUInt32 func sshCertTypeUInt32(ct string) uint32 { switch ct { diff --git a/authority/provisioner/ssh_options.go b/authority/provisioner/ssh_options.go index 7ee236d1..ada26d7d 100644 --- a/authority/provisioner/ssh_options.go +++ b/authority/provisioner/ssh_options.go @@ -33,6 +33,26 @@ type SSHOptions struct { // TemplateData is a JSON object with variables that can be used in custom // templates. TemplateData json.RawMessage `json:"templateData,omitempty"` + + // AllowedNames contains the names the provisioner is authorized to sign + AllowedNames *AllowedSSHNameOptions `json:"allow,omitempty"` + + // DeniedNames contains the names the provisioner is not authorized to sign + DeniedNames *DeniedSSHNameOptions `json:"deny,omitempty"` +} + +// AllowedSSHNameOptions models the allowed names +type AllowedSSHNameOptions struct { + DNSDomains []string `json:"dns,omitempty"` + EmailAddresses []string `json:"email,omitempty"` + Principals []string `json:"principal,omitempty"` +} + +// DeniedSSHNameOptions models the denied names +type DeniedSSHNameOptions struct { + DNSDomains []string `json:"dns,omitempty"` + EmailAddresses []string `json:"email,omitempty"` + Principals []string `json:"principal,omitempty"` } // HasTemplate returns true if a template is defined in the provisioner options. @@ -40,6 +60,40 @@ func (o *SSHOptions) HasTemplate() bool { return o != nil && (o.Template != "" || o.TemplateFile != "") } +// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the +// names that a provisioner is authorized to sign SSH certificates for. +func (o *SSHOptions) GetAllowedNameOptions() *AllowedSSHNameOptions { + if o == nil { + return nil + } + return o.AllowedNames +} + +// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the +// names that a provisioner is NOT authorized to sign SSH certificates for. +func (o *SSHOptions) GetDeniedNameOptions() *DeniedSSHNameOptions { + if o == nil { + return nil + } + return o.DeniedNames +} + +// HasNames checks if the AllowedSSHNameOptions has one or more +// names configured. +func (o *AllowedSSHNameOptions) HasNames() bool { + return len(o.DNSDomains) > 0 || + len(o.EmailAddresses) > 0 || + len(o.Principals) > 0 +} + +// HasNames checks if the DeniedSSHNameOptions has one or more +// names configured. +func (o *DeniedSSHNameOptions) HasNames() bool { + return len(o.DNSDomains) > 0 || + len(o.EmailAddresses) > 0 || + len(o.Principals) > 0 +} + // TemplateSSHOptions generates a SSHCertificateOptions with the template and // data defined in the ProvisionerOptions, the provisioner generated data, and // the user data provided in the request. If no template has been provided, diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index 3039d2a3..b41f512e 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -84,6 +84,7 @@ func (p *SSHPOP) GetEncryptedKey() (string, string, bool) { // Init initializes and validates the fields of a SSHPOP type. func (p *SSHPOP) Init(config Config) error { + p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -99,6 +100,8 @@ func (p *SSHPOP) Init(config Config) error { return err } + // TODO(hs): initialize the policy engine and add it as an SSH cert validator + p.audiences = config.Audiences.WithFragment(p.GetIDForToken()) p.sshPubKeys = config.SSHKeys return nil diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index fe2678fc..ea0890ae 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -177,6 +177,7 @@ func generateJWK() (*JWK, error) { return nil, err } return &JWK{ + base: &base{}, Name: name, Type: "JWK", Key: &public, @@ -215,6 +216,7 @@ func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) { } return &K8sSA{ + base: &base{}, Name: K8sSAName, Type: "K8sSA", Claims: &globalProvisionerClaims, @@ -252,6 +254,7 @@ func generateSSHPOP() (*SSHPOP, error) { } return &SSHPOP{ + base: &base{}, Name: name, Type: "SSHPOP", Claims: &globalProvisionerClaims, @@ -306,6 +309,7 @@ M46l92gdOozT rootPool.AddCert(cert) } return &X5C{ + base: &base{}, Name: name, Type: "X5C", Roots: root, @@ -338,6 +342,7 @@ func generateOIDC() (*OIDC, error) { return nil, err } return &OIDC{ + base: &base{}, Name: name, Type: "OIDC", ClientID: clientID, @@ -373,6 +378,7 @@ func generateGCP() (*GCP, error) { return nil, err } return &GCP{ + base: &base{}, Type: "GCP", Name: name, ServiceAccounts: []string{serviceAccount}, @@ -409,6 +415,7 @@ func generateAWS() (*AWS, error) { return nil, errors.Wrap(err, "error parsing AWS certificate") } return &AWS{ + base: &base{}, Type: "AWS", Name: name, Accounts: []string{accountID}, @@ -518,6 +525,7 @@ func generateAWSV1Only() (*AWS, error) { return nil, errors.Wrap(err, "error parsing AWS certificate") } return &AWS{ + base: &base{}, Type: "AWS", Name: name, Accounts: []string{accountID}, @@ -609,6 +617,7 @@ func generateAzure() (*Azure, error) { return nil, err } return &Azure{ + base: &base{}, Type: "Azure", Name: name, TenantID: tenantID, diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 8710acb5..a87e4392 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -87,6 +87,7 @@ func (p *X5C) GetEncryptedKey() (string, string, bool) { // Init initializes and validates the fields of a X5C type. func (p *X5C) Init(config Config) error { + p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -125,6 +126,16 @@ func (p *X5C) Init(config Config) error { return err } + // Initialize the x509 allow/deny policy engine + if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine + if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + p.audiences = config.Audiences.WithFragment(p.GetIDForToken()) return nil } @@ -229,6 +240,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultSANsValidator(claims.SANs), defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(p.x509PolicyEngine), }, nil } @@ -311,5 +323,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, + // Ensure that all principal names are allowed + newSSHNamePolicyValidator(p.sshPolicyEngine), ), nil } diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 2959f8c6..5d2a3566 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -463,7 +463,7 @@ func TestX5C_AuthorizeSign(t *testing.T) { } else { if assert.Nil(t, tc.err) { if assert.NotNil(t, opts) { - assert.Equals(t, len(opts), 7) + assert.Len(t, 8, opts) for _, o := range opts { switch v := o.(type) { case certificateOptionsFunc: @@ -474,7 +474,6 @@ func TestX5C_AuthorizeSign(t *testing.T) { assert.Len(t, 0, v.KeyValuePairs) case profileLimitDuration: assert.Equals(t, v.def, tc.p.claimer.DefaultTLSCertDuration()) - claims, err := tc.p.authorizeToken(tc.token, tc.p.audiences.Sign) assert.FatalError(t, err) assert.Equals(t, v.notAfter, claims.chains[0][0].NotAfter) @@ -486,6 +485,8 @@ func TestX5C_AuthorizeSign(t *testing.T) { case *validityValidator: assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration()) assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration()) + case *x509NamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } @@ -778,6 +779,8 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter) case *sshCertValidityValidator: assert.Equals(t, v.Claimer, tc.p.claimer) + case *sshNamePolicyValidator: + assert.Equals(t, nil, v.policyEngine) case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc: default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) @@ -785,9 +788,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { tot++ } if len(tc.claims.Step.SSH.CertType) > 0 { - assert.Equals(t, tot, 9) + assert.Equals(t, tot, 10) } else { - assert.Equals(t, tot, 7) + assert.Equals(t, tot, 8) } } } diff --git a/policy/ssh/options.go b/policy/ssh/options.go new file mode 100644 index 00000000..30b68a1d --- /dev/null +++ b/policy/ssh/options.go @@ -0,0 +1,99 @@ +package sshpolicy + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" +) + +type NamePolicyOption func(g *NamePolicyEngine) error + +func WithPermittedDNSDomains(domains []string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + for _, domain := range domains { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) + } + } + g.permittedDNSDomains = domains + return nil + } +} + +func WithExcludedDNSDomains(domains []string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + for _, domain := range domains { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse excluded domain constraint %q", domain) + } + } + g.excludedDNSDomains = domains + return nil + } +} + +func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + for _, email := range emailAddresses { + if err := validateEmailConstraint(email); err != nil { + return err + } + } + g.permittedEmailAddresses = emailAddresses + return nil + } +} + +func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + for _, email := range emailAddresses { + if err := validateEmailConstraint(email); err != nil { + return err + } + } + g.excludedEmailAddresses = emailAddresses + return nil + } +} + +func WithPermittedPrincipals(principals []string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + // for _, principal := range principals { + // // TODO: validation? + // } + g.permittedPrincipals = principals + return nil + } +} + +func WithExcludedPrincipals(principals []string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + // for _, principal := range principals { + // // TODO: validation? + // } + g.excludedPrincipals = principals + return nil + } +} + +func validateDNSDomainConstraint(domain string) error { + if _, ok := domainToReverseLabels(domain); !ok { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) + } + return nil +} + +func validateEmailConstraint(constraint string) error { + if strings.Contains(constraint, "@") { + _, ok := parseRFC2821Mailbox(constraint) + if !ok { + return fmt.Errorf("cannot parse email constraint %q", constraint) + } + } + _, ok := domainToReverseLabels(constraint) + if !ok { + return fmt.Errorf("cannot parse email domain constraint %q", constraint) + } + return nil +} diff --git a/policy/ssh/ssh.go b/policy/ssh/ssh.go new file mode 100644 index 00000000..95e7d471 --- /dev/null +++ b/policy/ssh/ssh.go @@ -0,0 +1,472 @@ +package sshpolicy + +import ( + "bytes" + "crypto/x509" + "fmt" + "reflect" + "strings" + + "github.com/pkg/errors" + "golang.org/x/crypto/ssh" +) + +type CertificateInvalidError struct { + Reason x509.InvalidReason + Detail string +} + +func (e CertificateInvalidError) Error() string { + switch e.Reason { + // TODO: include logical errors for this package; exlude ones that don't make sense for its current use case? + // TODO: currently only CANotAuthorizedForThisName is used by this package; we're not checking the other things in CSRs in this package. + case x509.NotAuthorizedToSign: + return "not authorized to sign other certificates" // TODO: this one doesn't make sense for this pkg + case x509.Expired: + return "csr has expired or is not yet valid: " + e.Detail + case x509.CANotAuthorizedForThisName: + return "not authorized to sign for this name: " + e.Detail + case x509.CANotAuthorizedForExtKeyUsage: + return "not authorized for an extended key usage: " + e.Detail + case x509.TooManyIntermediates: + return "too many intermediates for path length constraint" + case x509.IncompatibleUsage: + return "csr specifies an incompatible key usage" + case x509.NameMismatch: + return "issuer name does not match subject from issuing certificate" + case x509.NameConstraintsWithoutSANs: + return "issuer has name constraints but csr doesn't have a SAN extension" + case x509.UnconstrainedName: + return "issuer has name constraints but csr contains unknown or unconstrained name: " + e.Detail + } + return "unknown error" +} + +type NamePolicyEngine struct { + options []NamePolicyOption + permittedDNSDomains []string + excludedDNSDomains []string + permittedEmailAddresses []string + excludedEmailAddresses []string + permittedPrincipals []string // TODO: rename to usernames, as principals can be host, user@ (like mail) and usernames? + excludedPrincipals []string +} + +func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { + + e := &NamePolicyEngine{} // TODO: embed an x509 engine instead of building it again? + e.options = append(e.options, opts...) + for _, option := range e.options { + if err := option(e); err != nil { + return nil, err + } + } + + return e, nil +} + +func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) { + dnsNames, emails, userNames := splitPrincipals(cert.ValidPrincipals) + if err := e.validateNames(dnsNames, emails, userNames); err != nil { + return false, err + } + return true, nil +} + +func (e *NamePolicyEngine) validateNames(dnsNames, emails, userNames []string) error { + //"dns": ["*.smallstep.com"], + //"email": ["@smallstep.com", "@google.com"], + //"principal": ["max", "mariano", "mike"] + /* No regexes for now. But if we ever implement them, they'd probably look like this */ + /*"principal": ["foo.smallstep.com", "/^*\.smallstep\.com$/"]*/ + + // Principals can be single user names (mariano, max, mike, ...), hostnames/domains (*.smallstep.com, host.smallstep.com, ...) and emails (max@smallstep.com, @smallstep.com, ...) + // All ValidPrincipals can thus be any one of those, and they can be mixed (mike@smallstep.com, mike, ...); we need to split this? + // Should we assume a generic engine, or can we do it host vs. user based? If host vs. user based, then it becomes easier w.r.t. dns; hosts will only be DNS, right? + // If we assume generic, we _may_ have a harder time distinguishing host vs. user certs. We propose to use host + user specific provisioners, though... + // Perhaps we can do some heuristics on the principal names vs. hostnames (i.e. when only a single label and no dot, then it's a user principal) + + for _, dns := range dnsNames { + if _, ok := domainToReverseLabels(dns); !ok { + return errors.Errorf("cannot parse dns %q", dns) + } + if err := checkNameConstraints("dns", dns, dns, + func(parsedName, constraint interface{}) (bool, error) { + return matchDomainConstraint(parsedName.(string), constraint.(string)) + }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { + return err + } + } + + for _, email := range emails { + mailbox, ok := parseRFC2821Mailbox(email) + if !ok { + return fmt.Errorf("cannot parse rfc822Name %q", mailbox) + } + if err := checkNameConstraints("email", email, mailbox, + func(parsedName, constraint interface{}) (bool, error) { + return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) + }, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil { + return err + } + } + + for _, userName := range userNames { + // TODO: some validation? I.e. allowed characters? + if err := checkNameConstraints("username", userName, userName, + func(parsedName, constraint interface{}) (bool, error) { + return matchUserNameConstraint(parsedName.(string), constraint.(string)) + }, e.permittedPrincipals, e.excludedPrincipals); err != nil { + return err + } + } + + return nil +} + +// splitPrincipals splits SSH certificate principals into DNS names, emails and user names. +func splitPrincipals(principals []string) (dnsNames, emails, userNames []string) { + dnsNames = []string{} + emails = []string{} + userNames = []string{} + for _, principal := range principals { + if strings.Contains(principal, "@") { + emails = append(emails, principal) + } else if len(strings.Split(principal, ".")) > 1 { + dnsNames = append(dnsNames, principal) + } else { + userNames = append(userNames, principal) + } + } + return +} + +// checkNameConstraints checks that c permits a child certificate to claim the +// given name, of type nameType. The argument parsedName contains the parsed +// form of name, suitable for passing to the match function. The total number +// of comparisons is tracked in the given count and should not exceed the given +// limit. +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func checkNameConstraints( + nameType string, + name string, + parsedName interface{}, + match func(parsedName, constraint interface{}) (match bool, err error), + permitted, excluded interface{}) error { + + excludedValue := reflect.ValueOf(excluded) + + // *count += excludedValue.Len() + // if *count > maxConstraintComparisons { + // return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""} + // } + + // TODO: fix the errors; return our own, because we don't have cert ... + + for i := 0; i < excludedValue.Len(); i++ { + constraint := excludedValue.Index(i).Interface() + match, err := match(parsedName, constraint) + if err != nil { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: err.Error(), + } + } + + if match { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), + } + } + } + + permittedValue := reflect.ValueOf(permitted) + + // *count += permittedValue.Len() + // if *count > maxConstraintComparisons { + // return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""} + // } + + ok := true + for i := 0; i < permittedValue.Len(); i++ { + constraint := permittedValue.Index(i).Interface() + var err error + if ok, err = match(parsedName, constraint); err != nil { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: err.Error(), + } + } + + if ok { + break + } + } + + if !ok { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), + } + } + + return nil +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func 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. + if constraint == "" { + return true, nil + } + + domainLabels, ok := domainToReverseLabels(domain) + if !ok { + return false, fmt.Errorf("cannot parse domain %q", domain) + } + + // RFC 5280 says that a leading period in a domain name means that at + // least one label must be prepended, but only for URI and email + // constraints, not DNS constraints. The code also supports that + // behavior for DNS constraints. + + mustHaveSubdomains := false + if constraint[0] == '.' { + mustHaveSubdomains = true + constraint = constraint[1:] + } + + constraintLabels, ok := domainToReverseLabels(constraint) + if !ok { + return false, fmt.Errorf("cannot parse domain %q", constraint) + } + + if len(domainLabels) < len(constraintLabels) || + (mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) { + return false, nil + } + + for i, constraintLabel := range constraintLabels { + if !strings.EqualFold(constraintLabel, domainLabels[i]) { + return false, nil + } + } + + return true, nil +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { + // If the constraint contains an @, then it specifies an exact mailbox name. + if strings.Contains(constraint, "@") { + constraintMailbox, ok := parseRFC2821Mailbox(constraint) + if !ok { + return false, fmt.Errorf("cannot parse constraint %q", constraint) + } + return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil + } + + // Otherwise the constraint is like a DNS constraint of the domain part + // of the mailbox. + return matchDomainConstraint(mailbox.domain, constraint) +} + +// matchUserNameConstraint performs a string literal match against a constraint +func matchUserNameConstraint(userName, constraint string) (bool, error) { + return userName == constraint, nil +} + +// TODO: decrease code duplication: single policy engine again, with principals added, but not used in x509? +// Not sure how I'd like to model that in Go, though: use (embedded) structs? interfaces? An x509 name policy engine +// interface could expose the methods that are useful to x509; the SSH name policy engine interfaces could do the +// same for SSH ones. One interface for both (with no methods?); then two, so that not all name policy options +// can be executed on both types? The shared ones could then maybe use the one with no methods? But we need protect +// it from being applied to just any type, of course. Not sure if Go allows us to do something like that, though. +// Maybe some kind of dummy function helps there? + +// 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 { + reverseLabels = append(reverseLabels, domain) + domain = "" + } else { + reverseLabels = append(reverseLabels, domain[i+1:]) + domain = domain[:i] + } + } + + if len(reverseLabels) > 0 && reverseLabels[0] == "" { + // An empty label at the end indicates an absolute value. + return nil, false + } + + for _, label := range reverseLabels { + if label == "" { + // Empty labels are otherwise invalid. + return nil, false + } + + for _, c := range label { + if c < 33 || c > 126 { + // Invalid character. + return nil, false + } + } + } + + return reverseLabels, true +} + +// 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 +} + +// parseRFC2821Mailbox parses an email address into local and domain parts, +// 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 + } + + localPartBytes := make([]byte, 0, len(in)/2) + + if in[0] == '"' { + // Quoted-string = DQUOTE *qcontent DQUOTE + // non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127 + // qcontent = qtext / quoted-pair + // qtext = non-whitespace-control / + // %d33 / %d35-91 / %d93-126 + // quoted-pair = ("\" text) / obs-qp + // text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text + // + // (Names beginning with “obs-” are the obsolete syntax from RFC 2822, + // Section 4. Since it has been 16 years, we no longer accept that.) + in = in[1:] + QuotedString: + for { + if in == "" { + return mailbox, false + } + c := in[0] + in = in[1:] + + switch { + case c == '"': + break QuotedString + + case c == '\\': + // quoted-pair + if in == "" { + return mailbox, false + } + if in[0] == 11 || + in[0] == 12 || + (1 <= in[0] && in[0] <= 9) || + (14 <= in[0] && in[0] <= 127) { + localPartBytes = append(localPartBytes, in[0]) + in = in[1:] + } else { + return mailbox, false + } + + case c == 11 || + c == 12 || + // Space (char 32) is not allowed based on the + // BNF, but RFC 3696 gives an example that + // assumes that it is. Several “verified” + // errata continue to argue about this point. + // We choose to accept it. + c == 32 || + c == 33 || + c == 127 || + (1 <= c && c <= 8) || + (14 <= c && c <= 31) || + (35 <= c && c <= 91) || + (93 <= c && c <= 126): + // qtext + localPartBytes = append(localPartBytes, c) + + default: + return mailbox, false + } + } + } else { + // Atom ("." Atom)* + NextChar: + for len(in) > 0 { + // atext from RFC 2822, Section 3.2.4 + c := in[0] + + switch { + case c == '\\': + // Examples given in RFC 3696 suggest that + // escaped characters can appear outside of a + // quoted string. Several “verified” errata + // continue to argue the point. We choose to + // accept it. + in = in[1:] + if in == "" { + return mailbox, false + } + fallthrough + + case ('0' <= c && c <= '9') || + ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + c == '!' || c == '#' || c == '$' || c == '%' || + c == '&' || c == '\'' || c == '*' || c == '+' || + c == '-' || c == '/' || c == '=' || c == '?' || + c == '^' || c == '_' || c == '`' || c == '{' || + c == '|' || c == '}' || c == '~' || c == '.': + localPartBytes = append(localPartBytes, in[0]) + in = in[1:] + + default: + break NextChar + } + } + + if len(localPartBytes) == 0 { + return mailbox, false + } + + // From RFC 3696, Section 3: + // “period (".") may also appear, but may not be used to start + // or end the local part, nor may two or more consecutive + // periods appear.” + twoDots := []byte{'.', '.'} + if localPartBytes[0] == '.' || + localPartBytes[len(localPartBytes)-1] == '.' || + bytes.Contains(localPartBytes, twoDots) { + return mailbox, false + } + } + + if in == "" || in[0] != '@' { + return mailbox, false + } + in = in[1:] + + // The RFC species a format for domains, but that's known to be + // violated in practice so we accept that anything after an '@' is the + // domain part. + if _, ok := domainToReverseLabels(in); !ok { + return mailbox, false + } + + mailbox.local = string(localPartBytes) + mailbox.domain = in + return mailbox, true +} diff --git a/policy/x509/options.go b/policy/x509/options.go new file mode 100755 index 00000000..68f236cb --- /dev/null +++ b/policy/x509/options.go @@ -0,0 +1,506 @@ +package x509policy + +import ( + "fmt" + "net" + "strings" + + "github.com/pkg/errors" +) + +type NamePolicyOption func(e *NamePolicyEngine) error + +// TODO: wrap (more) errors; and prove a set of known (exported) errors + +func WithPermittedDNSDomains(domains []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, domain := range domains { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) + } + } + e.permittedDNSDomains = domains + return nil + } +} + +func AddPermittedDNSDomains(domains []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, domain := range domains { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) + } + } + e.permittedDNSDomains = append(e.permittedDNSDomains, domains...) + return nil + } +} + +func WithExcludedDNSDomains(domains []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, domain := range domains { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse excluded domain constraint %q", domain) + } + } + e.excludedDNSDomains = domains + return nil + } +} + +func AddExcludedDNSDomains(domains []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, domain := range domains { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse excluded domain constraint %q", domain) + } + } + e.excludedDNSDomains = append(e.excludedDNSDomains, domains...) + return nil + } +} + +func WithPermittedDNSDomain(domain string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) + } + e.permittedDNSDomains = []string{domain} + return nil + } +} + +func AddPermittedDNSDomain(domain string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) + } + e.permittedDNSDomains = append(e.permittedDNSDomains, domain) + return nil + } +} + +func WithExcludedDNSDomain(domain string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse excluded domain constraint %q", domain) + } + e.excludedDNSDomains = []string{domain} + return nil + } +} + +func AddExcludedDNSDomain(domain string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateDNSDomainConstraint(domain); err != nil { + return errors.Errorf("cannot parse excluded domain constraint %q", domain) + } + e.excludedDNSDomains = append(e.excludedDNSDomains, domain) + return nil + } +} + +func WithPermittedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { + return func(e *NamePolicyEngine) error { + e.permittedIPRanges = ipRanges + return nil + } +} + +func AddPermittedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { + return func(e *NamePolicyEngine) error { + e.permittedIPRanges = append(e.permittedIPRanges, ipRanges...) + return nil + } +} + +func WithPermittedCIDRs(cidrs []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + networks := []*net.IPNet{} + for _, cidr := range cidrs { + _, nw, err := net.ParseCIDR(cidr) + if err != nil { + return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) + } + networks = append(networks, nw) + } + e.permittedIPRanges = networks + return nil + } +} + +func AddPermittedCIDRs(cidrs []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + networks := []*net.IPNet{} + for _, cidr := range cidrs { + _, nw, err := net.ParseCIDR(cidr) + if err != nil { + return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) + } + networks = append(networks, nw) + } + e.permittedIPRanges = append(e.permittedIPRanges, networks...) + return nil + } +} + +func WithExcludedCIDRs(cidrs []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + networks := []*net.IPNet{} + for _, cidr := range cidrs { + _, nw, err := net.ParseCIDR(cidr) + if err != nil { + return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) + } + networks = append(networks, nw) + } + e.excludedIPRanges = networks + return nil + } +} + +func AddExcludedCIDRs(cidrs []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + networks := []*net.IPNet{} + for _, cidr := range cidrs { + _, nw, err := net.ParseCIDR(cidr) + if err != nil { + return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) + } + networks = append(networks, nw) + } + e.excludedIPRanges = append(e.excludedIPRanges, networks...) + return nil + } +} + +func WithPermittedCIDR(cidr string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + _, nw, err := net.ParseCIDR(cidr) + if err != nil { + return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) + } + e.permittedIPRanges = []*net.IPNet{nw} + return nil + } +} + +func AddPermittedCIDR(cidr string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + _, nw, err := net.ParseCIDR(cidr) + if err != nil { + return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) + } + e.permittedIPRanges = append(e.permittedIPRanges, nw) + return nil + } +} + +func WithPermittedIP(ip net.IP) NamePolicyOption { + return func(e *NamePolicyEngine) error { + var mask net.IPMask + if !isIPv4(ip) { + mask = net.CIDRMask(128, 128) + } else { + mask = net.CIDRMask(32, 32) + } + nw := &net.IPNet{ + IP: ip, + Mask: mask, + } + e.permittedIPRanges = []*net.IPNet{nw} + return nil + } +} + +func AddPermittedIP(ip net.IP) NamePolicyOption { + return func(e *NamePolicyEngine) error { + var mask net.IPMask + if !isIPv4(ip) { + mask = net.CIDRMask(128, 128) + } else { + mask = net.CIDRMask(32, 32) + } + nw := &net.IPNet{ + IP: ip, + Mask: mask, + } + e.permittedIPRanges = append(e.permittedIPRanges, nw) + return nil + } +} + +func WithExcludedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { + return func(e *NamePolicyEngine) error { + e.excludedIPRanges = ipRanges + return nil + } +} + +func AddExcludedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { + return func(e *NamePolicyEngine) error { + e.excludedIPRanges = append(e.excludedIPRanges, ipRanges...) + return nil + } +} + +func WithExcludedCIDR(cidr string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + _, nw, err := net.ParseCIDR(cidr) + if err != nil { + return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) + } + e.excludedIPRanges = []*net.IPNet{nw} + return nil + } +} + +func AddExcludedCIDR(cidr string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + _, nw, err := net.ParseCIDR(cidr) + if err != nil { + return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) + } + e.excludedIPRanges = append(e.excludedIPRanges, nw) + return nil + } +} + +func WithExcludedIP(ip net.IP) NamePolicyOption { + return func(e *NamePolicyEngine) error { + var mask net.IPMask + if !isIPv4(ip) { + mask = net.CIDRMask(128, 128) + } else { + mask = net.CIDRMask(32, 32) + } + nw := &net.IPNet{ + IP: ip, + Mask: mask, + } + e.excludedIPRanges = []*net.IPNet{nw} + return nil + } +} + +func AddExcludedIP(ip net.IP) NamePolicyOption { + return func(e *NamePolicyEngine) error { + var mask net.IPMask + if !isIPv4(ip) { + mask = net.CIDRMask(128, 128) + } else { + mask = net.CIDRMask(32, 32) + } + nw := &net.IPNet{ + IP: ip, + Mask: mask, + } + e.excludedIPRanges = append(e.excludedIPRanges, nw) + return nil + } +} + +func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, email := range emailAddresses { + if err := validateEmailConstraint(email); err != nil { + return err + } + } + e.permittedEmailAddresses = emailAddresses + return nil + } +} + +func AddPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, email := range emailAddresses { + if err := validateEmailConstraint(email); err != nil { + return err + } + } + e.permittedEmailAddresses = append(e.permittedEmailAddresses, emailAddresses...) + return nil + } +} + +func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, email := range emailAddresses { + if err := validateEmailConstraint(email); err != nil { + return err + } + } + e.excludedEmailAddresses = emailAddresses + return nil + } +} + +func AddExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, email := range emailAddresses { + if err := validateEmailConstraint(email); err != nil { + return err + } + } + e.excludedEmailAddresses = append(e.excludedEmailAddresses, emailAddresses...) + return nil + } +} + +func WithPermittedEmailAddress(emailAddress string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateEmailConstraint(emailAddress); err != nil { + return err + } + e.permittedEmailAddresses = []string{emailAddress} + return nil + } +} + +func AddPermittedEmailAddress(emailAddress string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateEmailConstraint(emailAddress); err != nil { + return err + } + e.permittedEmailAddresses = append(e.permittedEmailAddresses, emailAddress) + return nil + } +} + +func WithExcludedEmailAddress(emailAddress string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateEmailConstraint(emailAddress); err != nil { + return err + } + e.excludedEmailAddresses = []string{emailAddress} + return nil + } +} + +func AddExcludedEmailAddress(emailAddress string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateEmailConstraint(emailAddress); err != nil { + return err + } + e.excludedEmailAddresses = append(e.excludedEmailAddresses, emailAddress) + return nil + } +} + +func WithPermittedURIDomains(uriDomains []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, domain := range uriDomains { + if err := validateURIDomainConstraint(domain); err != nil { + return err + } + } + e.permittedURIDomains = uriDomains + return nil + } +} + +func AddPermittedURIDomains(uriDomains []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, domain := range uriDomains { + if err := validateURIDomainConstraint(domain); err != nil { + return err + } + } + e.permittedURIDomains = append(e.permittedURIDomains, uriDomains...) + return nil + } +} + +func WithPermittedURIDomain(uriDomain string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateURIDomainConstraint(uriDomain); err != nil { + return err + } + e.permittedURIDomains = []string{uriDomain} + return nil + } +} + +func AddPermittedURIDomain(uriDomain string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateURIDomainConstraint(uriDomain); err != nil { + return err + } + e.permittedURIDomains = append(e.permittedURIDomains, uriDomain) + return nil + } +} + +func WithExcludedURIDomains(uriDomains []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, domain := range uriDomains { + if err := validateURIDomainConstraint(domain); err != nil { + return err + } + } + e.excludedURIDomains = uriDomains + return nil + } +} + +func AddExcludedURIDomains(uriDomains []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + for _, domain := range uriDomains { + if err := validateURIDomainConstraint(domain); err != nil { + return err + } + } + e.excludedURIDomains = append(e.excludedURIDomains, uriDomains...) + return nil + } +} + +func WithExcludedURIDomain(uriDomain string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateURIDomainConstraint(uriDomain); err != nil { + return err + } + e.excludedURIDomains = []string{uriDomain} + return nil + } +} + +func AddExcludedURIDomain(uriDomain string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + if err := validateURIDomainConstraint(uriDomain); err != nil { + return err + } + e.excludedURIDomains = append(e.excludedURIDomains, uriDomain) + return nil + } +} + +func validateDNSDomainConstraint(domain string) error { + if _, ok := domainToReverseLabels(domain); !ok { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) + } + return nil +} + +func validateEmailConstraint(constraint string) error { + if strings.Contains(constraint, "@") { + _, ok := parseRFC2821Mailbox(constraint) + if !ok { + return fmt.Errorf("cannot parse email constraint %q", constraint) + } + } + _, ok := domainToReverseLabels(constraint) + if !ok { + return fmt.Errorf("cannot parse email domain constraint %q", constraint) + } + return nil +} + +func validateURIDomainConstraint(constraint string) error { + _, ok := domainToReverseLabels(constraint) + if !ok { + return fmt.Errorf("cannot parse URI domain constraint %q", constraint) + } + return nil +} diff --git a/policy/x509/x509.go b/policy/x509/x509.go new file mode 100755 index 00000000..c8d4dfb2 --- /dev/null +++ b/policy/x509/x509.go @@ -0,0 +1,565 @@ +package x509policy + +import ( + "bytes" + "crypto/x509" + "fmt" + "net" + "net/url" + "reflect" + "strings" + + "github.com/pkg/errors" + "go.step.sm/crypto/x509util" +) + +type CertificateInvalidError struct { + Reason x509.InvalidReason + Detail string +} + +func (e CertificateInvalidError) Error() string { + switch e.Reason { + // TODO: include logical errors for this package; exlude ones that don't make sense for its current use case? + // TODO: currently only CANotAuthorizedForThisName is used by this package; we're not checking the other things in CSRs in this package. + case x509.NotAuthorizedToSign: + return "not authorized to sign other certificates" // TODO: this one doesn't make sense for this pkg + case x509.Expired: + return "csr has expired or is not yet valid: " + e.Detail + case x509.CANotAuthorizedForThisName: + return "not authorized to sign for this name: " + e.Detail + case x509.CANotAuthorizedForExtKeyUsage: + return "not authorized for an extended key usage: " + e.Detail + case x509.TooManyIntermediates: + return "too many intermediates for path length constraint" + case x509.IncompatibleUsage: + return "csr specifies an incompatible key usage" + case x509.NameMismatch: + return "issuer name does not match subject from issuing certificate" + case x509.NameConstraintsWithoutSANs: + return "issuer has name constraints but csr doesn't have a SAN extension" + case x509.UnconstrainedName: + return "issuer has name constraints but csr contains unknown or unconstrained name: " + e.Detail + } + return "unknown error" +} + +// NamePolicyEngine can be used to check that a CSR or Certificate meets all allowed and +// denied names before a CA creates and/or signs the Certificate. +// TODO(hs): the x509 RFC also defines name checks on directory name; support that? +// TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine? +type NamePolicyEngine struct { + options []NamePolicyOption + permittedDNSDomains []string + excludedDNSDomains []string + permittedIPRanges []*net.IPNet + excludedIPRanges []*net.IPNet + permittedEmailAddresses []string + excludedEmailAddresses []string + permittedURIDomains []string + excludedURIDomains []string +} + +// NewNamePolicyEngine creates a new NamePolicyEngine with NamePolicyOptions +func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { + + e := &NamePolicyEngine{} + e.options = append(e.options, opts...) + for _, option := range e.options { + if err := option(e); err != nil { + return nil, err + } + } + + return e, nil +} + +// AreCertificateNamesAllowed verifies that all SANs in a Certificate are allowed. +func (e *NamePolicyEngine) AreCertificateNamesAllowed(cert *x509.Certificate) (bool, error) { + if err := e.validateNames(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs); err != nil { + return false, err + } + return true, nil +} + +// AreCSRNamesAllowed verifies that all names in the CSR are allowed. +func (e *NamePolicyEngine) AreCSRNamesAllowed(csr *x509.CertificateRequest) (bool, error) { + if err := e.validateNames(csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs); err != nil { + return false, err + } + return true, nil +} + +// AreSANSAllowed verifies that all names in the slice of SANs are allowed. +// The SANs are first split into DNS names, IPs, email addresses and URIs. +func (e *NamePolicyEngine) AreSANsAllowed(sans []string) (bool, error) { + dnsNames, ips, emails, uris := x509util.SplitSANs(sans) + if err := e.validateNames(dnsNames, ips, emails, uris); err != nil { + return false, err + } + return true, nil +} + +// IsDNSAllowed verifies a single DNS domain is allowed. +func (e *NamePolicyEngine) IsDNSAllowed(dns string) (bool, error) { + if err := e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}); err != nil { + return false, err + } + return true, nil +} + +// IsIPAllowed verifies a single IP domain is allowed. +func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) { + if err := e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}); err != nil { + return false, err + } + return true, nil +} + +// 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) error { + + // TODO: return our own type of error? + + // TODO: set limit on total of all names? 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 + // this number as a total of all checks and keeps a (pointer to a) counter of the number of checks + // executed so far. + + // TODO: gather all errors, or return early? Currently we return early on the first wrong name; check might fail for multiple names. + // Perhaps make that an option? + for _, dns := range dnsNames { + if _, ok := domainToReverseLabels(dns); !ok { + return errors.Errorf("cannot parse dns %q", dns) + } + if err := checkNameConstraints("dns", dns, dns, + func(parsedName, constraint interface{}) (bool, error) { + return matchDomainConstraint(parsedName.(string), constraint.(string)) + }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { + return err + } + } + + for _, ip := range ips { + if err := checkNameConstraints("ip", ip.String(), ip, + func(parsedName, constraint interface{}) (bool, error) { + return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet)) + }, e.permittedIPRanges, e.excludedIPRanges); err != nil { + return err + } + } + + for _, email := range emailAddresses { + mailbox, ok := parseRFC2821Mailbox(email) + if !ok { + return fmt.Errorf("cannot parse rfc822Name %q", mailbox) + } + if err := checkNameConstraints("email", email, mailbox, + func(parsedName, constraint interface{}) (bool, error) { + return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) + }, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil { + return err + } + } + + for _, uri := range uris { + if err := checkNameConstraints("uri", uri.String(), uri, + func(parsedName, constraint interface{}) (bool, error) { + return matchURIConstraint(parsedName.(*url.URL), constraint.(string)) + }, e.permittedURIDomains, e.excludedURIDomains); err != nil { + return err + } + } + + // TODO: when the error is not nil and returned up in the above, we can add + // additional context to it (i.e. the cert or csr that was inspected). + + // TODO(hs): validate other types of SANs? The Go std library skips those. + // These could be custom checkers. + + // if all checks out, all SANs are allowed + return nil +} + +// checkNameConstraints checks that c permits a child certificate to claim the +// given name, of type nameType. The argument parsedName contains the parsed +// form of name, suitable for passing to the match function. The total number +// of comparisons is tracked in the given count and should not exceed the given +// limit. +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func checkNameConstraints( + nameType string, + name string, + parsedName interface{}, + match func(parsedName, constraint interface{}) (match bool, err error), + permitted, excluded interface{}) error { + + excludedValue := reflect.ValueOf(excluded) + + // *count += excludedValue.Len() + // if *count > maxConstraintComparisons { + // return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""} + // } + + // TODO: fix the errors; return our own, because we don't have cert ... + + for i := 0; i < excludedValue.Len(); i++ { + constraint := excludedValue.Index(i).Interface() + match, err := match(parsedName, constraint) + if err != nil { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: err.Error(), + } + } + + if match { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), + } + } + } + + permittedValue := reflect.ValueOf(permitted) + + // *count += permittedValue.Len() + // if *count > maxConstraintComparisons { + // return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""} + // } + + ok := true + for i := 0; i < permittedValue.Len(); i++ { + constraint := permittedValue.Index(i).Interface() + var err error + if ok, err = match(parsedName, constraint); err != nil { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: err.Error(), + } + } + + if ok { + break + } + } + + if !ok { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), + } + } + + return nil +} + +// 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 { + reverseLabels = append(reverseLabels, domain) + domain = "" + } else { + reverseLabels = append(reverseLabels, domain[i+1:]) + domain = domain[:i] + } + } + + if len(reverseLabels) > 0 && reverseLabels[0] == "" { + // An empty label at the end indicates an absolute value. + return nil, false + } + + for _, label := range reverseLabels { + if label == "" { + // Empty labels are otherwise invalid. + return nil, false + } + + for _, c := range label { + if c < 33 || c > 126 { + // Invalid character. + return nil, false + } + } + } + + return reverseLabels, true +} + +// 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 +} + +// parseRFC2821Mailbox parses an email address into local and domain parts, +// 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 + } + + localPartBytes := make([]byte, 0, len(in)/2) + + if in[0] == '"' { + // Quoted-string = DQUOTE *qcontent DQUOTE + // non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127 + // qcontent = qtext / quoted-pair + // qtext = non-whitespace-control / + // %d33 / %d35-91 / %d93-126 + // quoted-pair = ("\" text) / obs-qp + // text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text + // + // (Names beginning with “obs-” are the obsolete syntax from RFC 2822, + // Section 4. Since it has been 16 years, we no longer accept that.) + in = in[1:] + QuotedString: + for { + if in == "" { + return mailbox, false + } + c := in[0] + in = in[1:] + + switch { + case c == '"': + break QuotedString + + case c == '\\': + // quoted-pair + if in == "" { + return mailbox, false + } + if in[0] == 11 || + in[0] == 12 || + (1 <= in[0] && in[0] <= 9) || + (14 <= in[0] && in[0] <= 127) { + localPartBytes = append(localPartBytes, in[0]) + in = in[1:] + } else { + return mailbox, false + } + + case c == 11 || + c == 12 || + // Space (char 32) is not allowed based on the + // BNF, but RFC 3696 gives an example that + // assumes that it is. Several “verified” + // errata continue to argue about this point. + // We choose to accept it. + c == 32 || + c == 33 || + c == 127 || + (1 <= c && c <= 8) || + (14 <= c && c <= 31) || + (35 <= c && c <= 91) || + (93 <= c && c <= 126): + // qtext + localPartBytes = append(localPartBytes, c) + + default: + return mailbox, false + } + } + } else { + // Atom ("." Atom)* + NextChar: + for len(in) > 0 { + // atext from RFC 2822, Section 3.2.4 + c := in[0] + + switch { + case c == '\\': + // Examples given in RFC 3696 suggest that + // escaped characters can appear outside of a + // quoted string. Several “verified” errata + // continue to argue the point. We choose to + // accept it. + in = in[1:] + if in == "" { + return mailbox, false + } + fallthrough + + case ('0' <= c && c <= '9') || + ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + c == '!' || c == '#' || c == '$' || c == '%' || + c == '&' || c == '\'' || c == '*' || c == '+' || + c == '-' || c == '/' || c == '=' || c == '?' || + c == '^' || c == '_' || c == '`' || c == '{' || + c == '|' || c == '}' || c == '~' || c == '.': + localPartBytes = append(localPartBytes, in[0]) + in = in[1:] + + default: + break NextChar + } + } + + if len(localPartBytes) == 0 { + return mailbox, false + } + + // From RFC 3696, Section 3: + // “period (".") may also appear, but may not be used to start + // or end the local part, nor may two or more consecutive + // periods appear.” + twoDots := []byte{'.', '.'} + if localPartBytes[0] == '.' || + localPartBytes[len(localPartBytes)-1] == '.' || + bytes.Contains(localPartBytes, twoDots) { + return mailbox, false + } + } + + if in == "" || in[0] != '@' { + return mailbox, false + } + in = in[1:] + + // The RFC species a format for domains, but that's known to be + // violated in practice so we accept that anything after an '@' is the + // domain part. + if _, ok := domainToReverseLabels(in); !ok { + return mailbox, false + } + + mailbox.local = string(localPartBytes) + mailbox.domain = in + return mailbox, true +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func 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. + if constraint == "" { + return true, nil + } + + domainLabels, ok := domainToReverseLabels(domain) + if !ok { + return false, fmt.Errorf("cannot parse domain %q", domain) + } + + // RFC 5280 says that a leading period in a domain name means that at + // least one label must be prepended, but only for URI and email + // constraints, not DNS constraints. The code also supports that + // behavior for DNS constraints. + + mustHaveSubdomains := false + if constraint[0] == '.' { + mustHaveSubdomains = true + constraint = constraint[1:] + } + + constraintLabels, ok := domainToReverseLabels(constraint) + if !ok { + return false, fmt.Errorf("cannot parse domain %q", constraint) + } + + if len(domainLabels) < len(constraintLabels) || + (mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) { + return false, nil + } + + for i, constraintLabel := range constraintLabels { + if !strings.EqualFold(constraintLabel, domainLabels[i]) { + return false, nil + } + } + + return true, nil +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) { + + // TODO(hs): this is code from Go library, but I got some unexpected result: + // with permitted net 127.0.0.0/24, 127.0.0.1 is NOT allowed. When parsing 127.0.0.1 as net.IP + // which is in the IPAddresses slice, the underlying length is 16. The contraint.IP has a length + // of 4 instead. I currently don't believe that this is a bug in Go now, but why is it like that? + // Is there a difference because we're not operating on a sans []string slice? Or is the Go + // implementation stricter regarding IPv4 vs. IPv6? I've been bitten by some unfortunate differences + // between the two before (i.e. IPv4 in IPv6; IP SANS in ACME) + // if len(ip) != len(constraint.IP) { + // return false, nil + // } + + // for i := range ip { + // if mask := constraint.Mask[i]; ip[i]&mask != constraint.IP[i]&mask { + // return false, nil + // } + // } + + // if isIPv4(ip) != isIPv4(constraint.IP) { // TODO(hs): this check seems to do what the above intended to do? + // return false, nil + // } + + contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior. + + return contained, nil +} + +func isIPv4(ip net.IP) bool { + return ip.To4() != nil +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { + // If the constraint contains an @, then it specifies an exact mailbox name. + if strings.Contains(constraint, "@") { + constraintMailbox, ok := parseRFC2821Mailbox(constraint) + if !ok { + return false, fmt.Errorf("cannot parse constraint %q", constraint) + } + return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil + } + + // Otherwise the constraint is like a DNS constraint of the domain part + // of the mailbox. + return matchDomainConstraint(mailbox.domain, constraint) +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { + // From RFC 5280, Section 4.2.1.10: + // “a uniformResourceIdentifier that does not include an authority + // component with a host name specified as a fully qualified domain + // name (e.g., if the URI either does not include an authority + // component or includes an authority component in which the host name + // is specified as an IP address), then the application MUST reject the + // certificate.” + + host := uri.Host + if host == "" { + return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String()) + } + + if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") { + var err error + host, _, err = net.SplitHostPort(uri.Host) + if err != nil { + return false, err + } + } + + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") || + net.ParseIP(host) != nil { + return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String()) + } + + return matchDomainConstraint(host, constraint) +} diff --git a/policy/x509/x509_test.go b/policy/x509/x509_test.go new file mode 100755 index 00000000..99c371ff --- /dev/null +++ b/policy/x509/x509_test.go @@ -0,0 +1,299 @@ +package x509policy + +import ( + "crypto/x509" + "net" + "net/url" + "testing" + + "github.com/smallstep/assert" +) + +func TestGuard_IsAllowed(t *testing.T) { + type fields struct { + permittedDNSDomains []string + excludedDNSDomains []string + permittedIPRanges []*net.IPNet + excludedIPRanges []*net.IPNet + permittedEmailAddresses []string + excludedEmailAddresses []string + permittedURIDomains []string + excludedURIDomains []string + } + tests := []struct { + name string + fields fields + csr *x509.CertificateRequest + want bool + wantErr bool + }{ + { + name: "fail/dns-permitted", + fields: fields{ + permittedDNSDomains: []string{".local"}, + }, + csr: &x509.CertificateRequest{ + DNSNames: []string{"www.example.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-excluded", + fields: fields{ + excludedDNSDomains: []string{"example.com"}, + }, + csr: &x509.CertificateRequest{ + DNSNames: []string{"www.example.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ipv4-permitted", + fields: fields{ + permittedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + }, + csr: &x509.CertificateRequest{ + IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ipv4-excluded", + fields: fields{ + excludedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + }, + csr: &x509.CertificateRequest{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ipv6-permitted", + fields: fields{ + permittedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + }, + csr: &x509.CertificateRequest{ + IPAddresses: []net.IP{net.ParseIP("3001:0db8:85a3:0000:0000:8a2e:0370:7334")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ipv6-excluded", + fields: fields{ + excludedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + }, + csr: &x509.CertificateRequest{ + IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted", + fields: fields{ + permittedEmailAddresses: []string{"example.local"}, + }, + csr: &x509.CertificateRequest{ + EmailAddresses: []string{"mail@example.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-excluded", + fields: fields{ + excludedEmailAddresses: []string{"example.local"}, + }, + csr: &x509.CertificateRequest{ + EmailAddresses: []string{"mail@example.local"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted", + fields: fields{ + permittedURIDomains: []string{".example.com"}, + }, + csr: &x509.CertificateRequest{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-excluded", + fields: fields{ + excludedURIDomains: []string{".example.local"}, + }, + csr: &x509.CertificateRequest{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "ok/no-constraints", + fields: fields{}, + csr: &x509.CertificateRequest{ + DNSNames: []string{"www.example.com"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns", + fields: fields{ + permittedDNSDomains: []string{".local"}, + }, + csr: &x509.CertificateRequest{ + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4", + fields: fields{ + permittedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + }, + csr: &x509.CertificateRequest{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv6", + fields: fields{ + permittedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + }, + csr: &x509.CertificateRequest{ + IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail", + fields: fields{ + permittedEmailAddresses: []string{"example.local"}, + }, + csr: &x509.CertificateRequest{ + EmailAddresses: []string{"mail@example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri", + fields: fields{ + permittedURIDomains: []string{".example.com"}, + }, + csr: &x509.CertificateRequest{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-simple", + fields: fields{ + permittedDNSDomains: []string{".local"}, + permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, + permittedEmailAddresses: []string{"example.local"}, + permittedURIDomains: []string{".example.local"}, + }, + csr: &x509.CertificateRequest{ + DNSNames: []string{"example.local"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + EmailAddresses: []string{"mail@example.local"}, + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: true, + wantErr: false, + }, + // TODO: more complex uses cases that combine multiple names + // TODO: check errors (reasons) are as expected + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &NamePolicyEngine{ + permittedDNSDomains: tt.fields.permittedDNSDomains, + excludedDNSDomains: tt.fields.excludedDNSDomains, + permittedIPRanges: tt.fields.permittedIPRanges, + excludedIPRanges: tt.fields.excludedIPRanges, + permittedEmailAddresses: tt.fields.permittedEmailAddresses, + excludedEmailAddresses: tt.fields.excludedEmailAddresses, + permittedURIDomains: tt.fields.permittedURIDomains, + excludedURIDomains: tt.fields.excludedURIDomains, + } + got, err := g.AreCSRNamesAllowed(tt.csr) + if (err != nil) != tt.wantErr { + t.Errorf("Guard.IsAllowed() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + assert.NotEquals(t, "", err.Error()) // TODO(hs): make this a complete equality check + } + if got != tt.want { + t.Errorf("Guard.IsAllowed() = %v, want %v", got, tt.want) + } + }) + } +} From 6bc0513468d631920723c760cd6b3ec593e48141 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 3 Jan 2022 15:32:58 +0100 Subject: [PATCH 02/78] Add more tests --- authority/provisioner/acme.go | 1 + authority/provisioner/jwk.go | 12 + authority/provisioner/policy.go | 4 +- policy/ssh/ssh.go | 2 +- policy/ssh/ssh_test.go | 261 +++++++++++ policy/x509/options.go | 7 + policy/x509/x509.go | 40 +- policy/x509/x509_test.go | 771 ++++++++++++++++++++++++++++++-- 8 files changed, 1063 insertions(+), 35 deletions(-) create mode 100644 policy/ssh/ssh_test.go diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index c6cadf51..83d35e49 100755 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -117,6 +117,7 @@ func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier string) return nil } + // assuming only valid identifiers (IP or DNS) are provided var err error if ip := net.ParseIP(identifier); ip != nil { _, err = p.x509PolicyEngine.IsIPAllowed(ip) diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 081eb60c..3ee8113f 100755 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -158,6 +158,7 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err // revoke the certificate with serial number in the `sub` property. func (p *JWK) AuthorizeRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.Revoke) + // TODO(hs): authorize the SANs using x509 name policy allow/deny rules (also for other provisioners with AuthorizeRevoke) return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeRevoke") } @@ -208,9 +209,19 @@ func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error if p.claimer.IsDisableRenewal() { return errs.Unauthorized("jwk.AuthorizeRenew; renew is disabled for jwk provisioner '%s'", p.GetName()) } + // TODO(hs): authorize the SANs using x509 name policy allow/deny rules (also for other provisioners with AuthorizeRewew and AuthorizeSSHRenew) + //return p.authorizeRenew(cert) return nil } +// func (p *JWK) authorizeRenew(cert *x509.Certificate) error { +// if p.x509PolicyEngine == nil { +// return nil +// } +// _, err := p.x509PolicyEngine.AreCertificateNamesAllowed(cert) +// return err +// } + // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { @@ -288,5 +299,6 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // AuthorizeSSHRevoke returns nil if the token is valid, false otherwise. func (p *JWK) AuthorizeSSHRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.SSHRevoke) + // TODO(hs): authorize the principals using SSH name policy allow/deny rules (also for other provisioners with AuthorizeSSHRevoke) return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSSHRevoke") } diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go index cf436d70..282eabdc 100644 --- a/authority/provisioner/policy.go +++ b/authority/provisioner/policy.go @@ -12,7 +12,9 @@ func newX509PolicyEngine(x509Opts *X509Options) (*x509policy.NamePolicyEngine, e return nil, nil } - options := []x509policy.NamePolicyOption{} + options := []x509policy.NamePolicyOption{ + x509policy.WithEnableSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default + } allowed := x509Opts.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { diff --git a/policy/ssh/ssh.go b/policy/ssh/ssh.go index 95e7d471..dcf5394f 100644 --- a/policy/ssh/ssh.go +++ b/policy/ssh/ssh.go @@ -80,7 +80,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames, emails, userNames []string) e /* No regexes for now. But if we ever implement them, they'd probably look like this */ /*"principal": ["foo.smallstep.com", "/^*\.smallstep\.com$/"]*/ - // Principals can be single user names (mariano, max, mike, ...), hostnames/domains (*.smallstep.com, host.smallstep.com, ...) and emails (max@smallstep.com, @smallstep.com, ...) + // Principals can be single user names (mariano, max, mike, ...), hostnames/domains (*.smallstep.com, host.smallstep.com, ...) and "emails" (max@smallstep.com, @smallstep.com, ...) // All ValidPrincipals can thus be any one of those, and they can be mixed (mike@smallstep.com, mike, ...); we need to split this? // Should we assume a generic engine, or can we do it host vs. user based? If host vs. user based, then it becomes easier w.r.t. dns; hosts will only be DNS, right? // If we assume generic, we _may_ have a harder time distinguishing host vs. user certs. We propose to use host + user specific provisioners, though... diff --git a/policy/ssh/ssh_test.go b/policy/ssh/ssh_test.go new file mode 100644 index 00000000..e56ce592 --- /dev/null +++ b/policy/ssh/ssh_test.go @@ -0,0 +1,261 @@ +package sshpolicy + +import ( + "testing" + + "golang.org/x/crypto/ssh" +) + +func TestNamePolicyEngine_ArePrincipalsAllowed(t *testing.T) { + type fields struct { + options []NamePolicyOption + permittedDNSDomains []string + excludedDNSDomains []string + permittedEmailAddresses []string + excludedEmailAddresses []string + permittedPrincipals []string + excludedPrincipals []string + } + tests := []struct { + name string + fields fields + cert *ssh.Certificate + want bool + wantErr bool + }{ + { + name: "fail/dns-permitted", + fields: fields{ + permittedDNSDomains: []string{".local"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"host.notlocal"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted", + fields: fields{ + excludedDNSDomains: []string{".local"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"host.local"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted", + fields: fields{ + permittedEmailAddresses: []string{"example.local"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"user@example.notlocal"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-excluded", + fields: fields{ + excludedEmailAddresses: []string{"example.local"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"user@example.local"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/principal-permitted", + fields: fields{ + permittedPrincipals: []string{"user1"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"user2"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/principal-excluded", + fields: fields{ + excludedPrincipals: []string{"user"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"user"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/combined-complex-all-badhost.local", + fields: fields{ + permittedDNSDomains: []string{".local"}, + permittedEmailAddresses: []string{"example.local"}, + permittedPrincipals: []string{"user"}, + excludedDNSDomains: []string{"badhost.local"}, + excludedEmailAddresses: []string{"badmail@example.local"}, + excludedPrincipals: []string{"baduser"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + "user@example.local", + "badhost.local", + }, + }, + want: false, + wantErr: true, + }, + { + name: "ok/no-constraints", + fields: fields{}, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"host.example.com"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-permitted", + fields: fields{ + permittedDNSDomains: []string{".local"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-excluded", + fields: fields{ + excludedDNSDomains: []string{".notlocal"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-permitted", + fields: fields{ + permittedEmailAddresses: []string{"example.local"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"user@example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded", + fields: fields{ + excludedEmailAddresses: []string{"example.notlocal"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"user@example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/principal-permitted", + fields: fields{ + permittedPrincipals: []string{"user"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"user"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/principal-excluded", + fields: fields{ + excludedPrincipals: []string{"someone"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{"user"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-simple-user-permitted", + fields: fields{ + permittedEmailAddresses: []string{"example.local"}, + permittedPrincipals: []string{"user"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + "user@example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-simple-all-permitted", + fields: fields{ + permittedDNSDomains: []string{".local"}, + permittedEmailAddresses: []string{"example.local"}, + permittedPrincipals: []string{"user"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + "user@example.local", + "host.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-complex-all", + fields: fields{ + permittedDNSDomains: []string{".local"}, + permittedEmailAddresses: []string{"example.local"}, + permittedPrincipals: []string{"user"}, + excludedDNSDomains: []string{"badhost.local"}, + excludedEmailAddresses: []string{"badmail@example.local"}, + excludedPrincipals: []string{"baduser"}, + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + "user@example.local", + "host.local", + }, + }, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &NamePolicyEngine{ + options: tt.fields.options, + permittedDNSDomains: tt.fields.permittedDNSDomains, + excludedDNSDomains: tt.fields.excludedDNSDomains, + permittedEmailAddresses: tt.fields.permittedEmailAddresses, + excludedEmailAddresses: tt.fields.excludedEmailAddresses, + permittedPrincipals: tt.fields.permittedPrincipals, + excludedPrincipals: tt.fields.excludedPrincipals, + } + got, err := e.ArePrincipalsAllowed(tt.cert) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/policy/x509/options.go b/policy/x509/options.go index 68f236cb..d3557876 100755 --- a/policy/x509/options.go +++ b/policy/x509/options.go @@ -12,6 +12,13 @@ type NamePolicyOption func(e *NamePolicyEngine) error // TODO: wrap (more) errors; and prove a set of known (exported) errors +func WithEnableSubjectCommonNameVerification() NamePolicyOption { + return func(e *NamePolicyEngine) error { + e.verifySubjectCommonName = true + return nil + } +} + func WithPermittedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { for _, domain := range domains { diff --git a/policy/x509/x509.go b/policy/x509/x509.go index c8d4dfb2..408251cd 100755 --- a/policy/x509/x509.go +++ b/policy/x509/x509.go @@ -3,6 +3,7 @@ package x509policy import ( "bytes" "crypto/x509" + "crypto/x509/pkix" "fmt" "net" "net/url" @@ -50,6 +51,7 @@ func (e CertificateInvalidError) Error() string { // TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine? type NamePolicyEngine struct { options []NamePolicyOption + verifySubjectCommonName bool permittedDNSDomains []string excludedDNSDomains []string permittedIPRanges []*net.IPNet @@ -76,7 +78,13 @@ func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { // AreCertificateNamesAllowed verifies that all SANs in a Certificate are allowed. func (e *NamePolicyEngine) AreCertificateNamesAllowed(cert *x509.Certificate) (bool, error) { - if err := e.validateNames(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs); err != nil { + dnsNames, ips, emails, uris := cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs + // when Subject Common Name must be verified in addition to the SANs, it is + // added to the appropriate slice of names. + if e.verifySubjectCommonName { + appendSubjectCommonName(cert.Subject, &dnsNames, &ips, &emails, &uris) + } + if err := e.validateNames(dnsNames, ips, emails, uris); err != nil { return false, err } return true, nil @@ -84,7 +92,13 @@ func (e *NamePolicyEngine) AreCertificateNamesAllowed(cert *x509.Certificate) (b // AreCSRNamesAllowed verifies that all names in the CSR are allowed. func (e *NamePolicyEngine) AreCSRNamesAllowed(csr *x509.CertificateRequest) (bool, error) { - if err := e.validateNames(csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs); err != nil { + dnsNames, ips, emails, uris := csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs + // when Subject Common Name must be verified in addition to the SANs, it is + // added to the appropriate slice of names. + if e.verifySubjectCommonName { + appendSubjectCommonName(csr.Subject, &dnsNames, &ips, &emails, &uris) + } + if err := e.validateNames(dnsNames, ips, emails, uris); err != nil { return false, err } return true, nil @@ -116,6 +130,26 @@ func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) { return true, nil } +// appendSubjectCommonName appends the Subject Common Name to the appropriate slice of names. The logic is +// similar as x509util.SplitSANs: if the subject can be parsed as an IP, it's added to the ips. If it can +// be parsed as an URL, it is added to the URIs. If it contains an @, it is added to emails. When it's none +// of these, it's added to the DNS names. +func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.IP, emails *[]string, uris *[]*url.URL) { + commonName := subject.CommonName + if commonName == "" { + return + } + if ip := net.ParseIP(commonName); ip != nil { + *ips = append(*ips, ip) + } else if u, err := url.Parse(commonName); err == nil && u.Scheme != "" { + *uris = append(*uris, u) + } else if strings.Contains(commonName, "@") { + *emails = append(*emails, commonName) + } else { + *dnsNames = append(*dnsNames, commonName) + } +} + // 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 @@ -508,7 +542,7 @@ func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) { // return false, nil // } - contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior. + contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior; also check IPv4-in-IPv6 (again) return contained, nil } diff --git a/policy/x509/x509_test.go b/policy/x509/x509_test.go index 99c371ff..103c6009 100755 --- a/policy/x509/x509_test.go +++ b/policy/x509/x509_test.go @@ -2,6 +2,7 @@ package x509policy import ( "crypto/x509" + "crypto/x509/pkix" "net" "net/url" "testing" @@ -9,8 +10,11 @@ import ( "github.com/smallstep/assert" ) -func TestGuard_IsAllowed(t *testing.T) { +func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { + // TODO(hs): refactor these tests into using validateNames instead of AreCertificateNamesAllowed + // TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on type fields struct { + verifySubjectCommonName bool permittedDNSDomains []string excludedDNSDomains []string permittedIPRanges []*net.IPNet @@ -23,7 +27,7 @@ func TestGuard_IsAllowed(t *testing.T) { tests := []struct { name string fields fields - csr *x509.CertificateRequest + cert *x509.Certificate want bool wantErr bool }{ @@ -32,23 +36,67 @@ func TestGuard_IsAllowed(t *testing.T) { fields: fields{ permittedDNSDomains: []string{".local"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, want: false, wantErr: true, }, + { + name: "fail/dns-permitted-single-host", + fields: fields{ + permittedDNSDomains: []string{"host.local"}, + }, + cert: &x509.Certificate{ + DNSNames: []string{"differenthost.local"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted-no-label", + fields: fields{ + permittedDNSDomains: []string{".local"}, + }, + cert: &x509.Certificate{ + DNSNames: []string{"local"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted-empty-label", + fields: fields{ + permittedDNSDomains: []string{".local"}, + }, + cert: &x509.Certificate{ + DNSNames: []string{"www..local"}, + }, + want: false, + wantErr: true, + }, { name: "fail/dns-excluded", fields: fields{ excludedDNSDomains: []string{"example.com"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, want: false, wantErr: true, }, + { + name: "fail/dns-excluded-single-host", + fields: fields{ + excludedDNSDomains: []string{"example.com"}, + }, + cert: &x509.Certificate{ + DNSNames: []string{"example.com"}, + }, + want: false, + wantErr: true, + }, { name: "fail/ipv4-permitted", fields: fields{ @@ -59,7 +107,7 @@ func TestGuard_IsAllowed(t *testing.T) { }, }, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, }, want: false, @@ -75,7 +123,7 @@ func TestGuard_IsAllowed(t *testing.T) { }, }, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, }, want: false, @@ -91,7 +139,7 @@ func TestGuard_IsAllowed(t *testing.T) { }, }, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("3001:0db8:85a3:0000:0000:8a2e:0370:7334")}, }, want: false, @@ -107,7 +155,7 @@ func TestGuard_IsAllowed(t *testing.T) { }, }, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, }, want: false, @@ -118,18 +166,29 @@ func TestGuard_IsAllowed(t *testing.T) { fields: fields{ permittedEmailAddresses: []string{"example.local"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, }, want: false, wantErr: true, }, + { + name: "fail/mail-permitted-period-domain", + fields: fields{ + permittedEmailAddresses: []string{".example.local"}, // any address in a domain, but not on the host example.local + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.local"}, + }, + want: false, + wantErr: true, + }, { name: "fail/mail-excluded", fields: fields{ excludedEmailAddresses: []string{"example.local"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.local"}, }, want: false, @@ -140,7 +199,7 @@ func TestGuard_IsAllowed(t *testing.T) { fields: fields{ permittedURIDomains: []string{".example.com"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", @@ -151,12 +210,282 @@ func TestGuard_IsAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/uri-permitted-period-host", + fields: fields{ + permittedURIDomains: []string{".example.local"}, + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "example.local", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-period-host-certificate", + fields: fields{ + permittedURIDomains: []string{".example.local"}, + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: ".example.local", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-empty-host", + fields: fields{ + permittedURIDomains: []string{".example.com"}, + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-port-missing", + fields: fields{ + permittedURIDomains: []string{".example.com"}, + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "example.local::", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-ip", + fields: fields{ + permittedURIDomains: []string{".example.com"}, + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "127.0.0.1", + }, + }, + }, + want: false, + wantErr: true, + }, { name: "fail/uri-excluded", fields: fields{ excludedURIDomains: []string{".example.local"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-dns-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedDNSDomains: []string{".local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.notlocal", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-dns-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedDNSDomains: []string{".local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.local", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-ipv4-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "10.10.10.10", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-ipv4-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "127.0.0.1", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-ipv6-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "2002:0db8:85a3:0000:0000:8a2e:0370:7339", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-ipv6-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "2001:0db8:85a3:0000:0000:8a2e:0370:7339", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-email-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedEmailAddresses: []string{"example.local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "mail@smallstep.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-email-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedEmailAddresses: []string{"example.local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "mail@example.local", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-uri-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedURIDomains: []string{".example.com"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "https://www.google.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/subject-uri-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedURIDomains: []string{".example.com"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "https://www.example.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/combined-simple-all-badhost.local", + fields: fields{ + verifySubjectCommonName: true, + permittedDNSDomains: []string{".local"}, + permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, + permittedEmailAddresses: []string{"example.local"}, + permittedURIDomains: []string{".example.local"}, + excludedDNSDomains: []string{"badhost.local"}, + excludedIPRanges: []*net.IPNet{{IP: net.ParseIP("1.1.1.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, + excludedEmailAddresses: []string{"badmail@example.local"}, + excludedURIDomains: []string{"https://badwww.example.local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "badhost.local", + }, + DNSNames: []string{"example.local"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.130")}, + EmailAddresses: []string{"mail@example.local"}, URIs: []*url.URL{ { Scheme: "https", @@ -170,25 +499,47 @@ func TestGuard_IsAllowed(t *testing.T) { { name: "ok/no-constraints", fields: fields{}, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, want: true, wantErr: false, }, { - name: "ok/dns", + name: "ok/empty-dns-constraint", fields: fields{ - permittedDNSDomains: []string{".local"}, + permittedDNSDomains: []string{""}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ DNSNames: []string{"example.local"}, }, want: true, wantErr: false, }, { - name: "ok/ipv4", + name: "ok/dns-permitted", + fields: fields{ + permittedDNSDomains: []string{".local"}, + }, + cert: &x509.Certificate{ + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-excluded", + fields: fields{ + excludedDNSDomains: []string{".notlocal"}, + }, + cert: &x509.Certificate{ + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-permitted", fields: fields{ permittedIPRanges: []*net.IPNet{ { @@ -197,14 +548,30 @@ func TestGuard_IsAllowed(t *testing.T) { }, }, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, }, want: true, wantErr: false, }, { - name: "ok/ipv6", + name: "ok/ipv4-excluded", + fields: fields{ + excludedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("10.10.10.10")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv6-permitted", fields: fields{ permittedIPRanges: []*net.IPNet{ { @@ -213,29 +580,89 @@ func TestGuard_IsAllowed(t *testing.T) { }, }, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, }, want: true, wantErr: false, }, { - name: "ok/mail", + name: "ok/ipv6-excluded", + fields: fields{ + excludedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-permitted", fields: fields{ permittedEmailAddresses: []string{"example.local"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.local"}, }, want: true, wantErr: false, }, { - name: "ok/uri", + name: "ok/mail-permitted-with-period-domain", + fields: fields{ + permittedEmailAddresses: []string{".example.local"}, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@somehost.example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-permitted-with-multiple-labels", + fields: fields{ + permittedEmailAddresses: []string{".example.local"}, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@sub.www.example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded", + fields: fields{ + excludedEmailAddresses: []string{"example.notlocal"}, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-period-domain", + fields: fields{ + excludedEmailAddresses: []string{".example.notlocal"}, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@somehost.example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-permitted", fields: fields{ permittedURIDomains: []string{".example.com"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", @@ -247,14 +674,241 @@ func TestGuard_IsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/combined-simple", + name: "ok/uri-permitted-with-port", fields: fields{ + permittedURIDomains: []string{".example.com"}, + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com:8080", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-sub-permitted", + fields: fields{ + permittedURIDomains: []string{"example.com"}, + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "sub.host.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-excluded", + fields: fields{ + excludedURIDomains: []string{".google.com"}, + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-empty", + fields: fields{ + verifySubjectCommonName: true, + permittedDNSDomains: []string{".local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "", + }, + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-dns-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedDNSDomains: []string{".local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-dns-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedDNSDomains: []string{".notlocal"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-ipv4-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "127.0.0.20", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-ipv4-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("128.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "127.0.0.1", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-ipv6-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "2001:0db8:85a3:0000:0000:8a2e:0370:7339", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-ipv6-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedIPRanges: []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "2009:0db8:85a3:0000:0000:8a2e:0370:7339", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-email-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedEmailAddresses: []string{"example.local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "mail@example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-email-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedEmailAddresses: []string{"example.notlocal"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "mail@example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-uri-permitted", + fields: fields{ + verifySubjectCommonName: true, + permittedURIDomains: []string{".example.com"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "https://www.example.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-uri-excluded", + fields: fields{ + verifySubjectCommonName: true, + excludedURIDomains: []string{".google.com"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "https://www.example.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-simple-permitted", + fields: fields{ + verifySubjectCommonName: true, permittedDNSDomains: []string{".local"}, permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, permittedEmailAddresses: []string{"example.local"}, permittedURIDomains: []string{".example.local"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "somehost.local", + }, DNSNames: []string{"example.local"}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, EmailAddresses: []string{"mail@example.local"}, @@ -268,12 +922,69 @@ func TestGuard_IsAllowed(t *testing.T) { want: true, wantErr: false, }, - // TODO: more complex uses cases that combine multiple names + { + name: "ok/combined-simple-permitted-without-subject-verification", + fields: fields{ + verifySubjectCommonName: false, + permittedDNSDomains: []string{".local"}, + permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, + permittedEmailAddresses: []string{"example.local"}, + permittedURIDomains: []string{".example.local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "forbidden-but-non-verified-domain.example.com", + }, + DNSNames: []string{"example.local"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + EmailAddresses: []string{"mail@example.local"}, + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-simple-all", + fields: fields{ + verifySubjectCommonName: true, + permittedDNSDomains: []string{".local"}, + permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, + permittedEmailAddresses: []string{"example.local"}, + permittedURIDomains: []string{".example.local"}, + excludedDNSDomains: []string{"badhost.local"}, + excludedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.128"), Mask: net.IPv4Mask(255, 255, 255, 128)}}, + excludedEmailAddresses: []string{"badmail@example.local"}, + excludedURIDomains: []string{"https://badwww.example.local"}, + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "somehost.local", + }, + DNSNames: []string{"example.local"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + EmailAddresses: []string{"mail@example.local"}, + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: true, + wantErr: false, + }, + // TODO: more complex uses cases that combine multiple names and permitted/excluded entries // TODO: check errors (reasons) are as expected } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &NamePolicyEngine{ + verifySubjectCommonName: tt.fields.verifySubjectCommonName, permittedDNSDomains: tt.fields.permittedDNSDomains, excludedDNSDomains: tt.fields.excludedDNSDomains, permittedIPRanges: tt.fields.permittedIPRanges, @@ -283,16 +994,16 @@ func TestGuard_IsAllowed(t *testing.T) { permittedURIDomains: tt.fields.permittedURIDomains, excludedURIDomains: tt.fields.excludedURIDomains, } - got, err := g.AreCSRNamesAllowed(tt.csr) + got, err := g.AreCertificateNamesAllowed(tt.cert) if (err != nil) != tt.wantErr { - t.Errorf("Guard.IsAllowed() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { assert.NotEquals(t, "", err.Error()) // TODO(hs): make this a complete equality check } if got != tt.want { - t.Errorf("Guard.IsAllowed() = %v, want %v", got, tt.want) + t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() = %v, want %v", got, tt.want) } }) } From 91d51c2b8810f277a1a65805a799b6ae592129df Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 14 Jan 2022 13:06:32 +0100 Subject: [PATCH 03/78] Add allow/deny to Nebula provisioner --- authority/provisioner/nebula.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index a77f4281..dfff8617 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -34,6 +34,7 @@ const ( // https://signal.org/docs/specifications/xeddsa/#xeddsa and implemented by // go.step.sm/crypto/x25519. type Nebula struct { + *base ID string `json:"-"` Type string `json:"type"` Name string `json:"name"` @@ -47,6 +48,7 @@ type Nebula struct { // Init verifies and initializes the Nebula provisioner. func (p *Nebula) Init(config Config) error { + p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -68,6 +70,16 @@ func (p *Nebula) Init(config Config) error { p.audiences = config.Audiences.WithFragment(p.GetIDForToken()) + // Initialize the x509 allow/deny policy engine + if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine + if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + return nil } From 6bc301339ff86a2e1c40a82ad6ebd36fef46175d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 17 Jan 2022 22:49:47 +0100 Subject: [PATCH 04/78] Improve test case and code coverage --- authority/provisioner/policy.go | 10 +- policy/x509/options.go | 277 ++-- policy/x509/options_test.go | 1339 +++++++++++++++++++ policy/x509/x509.go | 204 ++- policy/x509/x509_test.go | 2223 +++++++++++++++++++++++-------- 5 files changed, 3343 insertions(+), 710 deletions(-) create mode 100644 policy/x509/options_test.go diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go index 282eabdc..1adfd115 100644 --- a/authority/provisioner/policy.go +++ b/authority/provisioner/policy.go @@ -13,14 +13,14 @@ func newX509PolicyEngine(x509Opts *X509Options) (*x509policy.NamePolicyEngine, e } options := []x509policy.NamePolicyOption{ - x509policy.WithEnableSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default + x509policy.WithSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default } allowed := x509Opts.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { options = append(options, - x509policy.WithPermittedDNSDomains(allowed.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. - x509policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges + x509policy.WithPermittedDNSDomains(allowed.DNSDomains), + x509policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges x509policy.WithPermittedEmailAddresses(allowed.EmailAddresses), x509policy.WithPermittedURIDomains(allowed.URIDomains), ) @@ -29,8 +29,8 @@ func newX509PolicyEngine(x509Opts *X509Options) (*x509policy.NamePolicyEngine, e denied := x509Opts.GetDeniedNameOptions() if denied != nil && denied.HasNames() { options = append(options, - x509policy.WithExcludedDNSDomains(denied.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. - x509policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges + x509policy.WithExcludedDNSDomains(denied.DNSDomains), + x509policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges x509policy.WithExcludedEmailAddresses(denied.EmailAddresses), x509policy.WithExcludedURIDomains(denied.URIDomains), ) diff --git a/policy/x509/options.go b/policy/x509/options.go index d3557876..ecd793a7 100755 --- a/policy/x509/options.go +++ b/policy/x509/options.go @@ -12,97 +12,120 @@ type NamePolicyOption func(e *NamePolicyEngine) error // TODO: wrap (more) errors; and prove a set of known (exported) errors -func WithEnableSubjectCommonNameVerification() NamePolicyOption { +func WithSubjectCommonNameVerification() NamePolicyOption { return func(e *NamePolicyEngine) error { e.verifySubjectCommonName = true return nil } } +func WithAllowLiteralWildcardNames() NamePolicyOption { + return func(e *NamePolicyEngine) error { + e.allowLiteralWildcardNames = true + return nil + } +} + func WithPermittedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { + normalizedDomains := make([]string, len(domains)) + for i, domain := range domains { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { return errors.Errorf("cannot parse permitted domain constraint %q", domain) } + normalizedDomains[i] = normalizedDomain } - e.permittedDNSDomains = domains + e.permittedDNSDomains = normalizedDomains return nil } } func AddPermittedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { + normalizedDomains := make([]string, len(domains)) + for i, domain := range domains { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { return errors.Errorf("cannot parse permitted domain constraint %q", domain) } + normalizedDomains[i] = normalizedDomain } - e.permittedDNSDomains = append(e.permittedDNSDomains, domains...) + e.permittedDNSDomains = append(e.permittedDNSDomains, normalizedDomains...) return nil } } func WithExcludedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) + normalizedDomains := make([]string, len(domains)) + for i, domain := range domains { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) } + normalizedDomains[i] = normalizedDomain } - e.excludedDNSDomains = domains + e.excludedDNSDomains = normalizedDomains return nil } } func AddExcludedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) + normalizedDomains := make([]string, len(domains)) + for i, domain := range domains { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) } + normalizedDomains[i] = normalizedDomain } - e.excludedDNSDomains = append(e.excludedDNSDomains, domains...) + e.excludedDNSDomains = append(e.excludedDNSDomains, normalizedDomains...) return nil } } func WithPermittedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateDNSDomainConstraint(domain); err != nil { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { return errors.Errorf("cannot parse permitted domain constraint %q", domain) } - e.permittedDNSDomains = []string{domain} + e.permittedDNSDomains = []string{normalizedDomain} return nil } } func AddPermittedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateDNSDomainConstraint(domain); err != nil { + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { return errors.Errorf("cannot parse permitted domain constraint %q", domain) } - e.permittedDNSDomains = append(e.permittedDNSDomains, domain) + e.permittedDNSDomains = append(e.permittedDNSDomains, normalizedDomain) return nil } } func WithExcludedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) } - e.excludedDNSDomains = []string{domain} + e.excludedDNSDomains = []string{normalizedDomain} return nil } } func AddExcludedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) + normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) + if err != nil { + return errors.Errorf("cannot parse permitted domain constraint %q", domain) } - e.excludedDNSDomains = append(e.excludedDNSDomains, domain) + e.excludedDNSDomains = append(e.excludedDNSDomains, normalizedDomain) return nil } } @@ -123,13 +146,13 @@ func AddPermittedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { func WithPermittedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - networks := []*net.IPNet{} - for _, cidr := range cidrs { + networks := make([]*net.IPNet, len(cidrs)) + for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) } - networks = append(networks, nw) + networks[i] = nw } e.permittedIPRanges = networks return nil @@ -138,13 +161,13 @@ func WithPermittedCIDRs(cidrs []string) NamePolicyOption { func AddPermittedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - networks := []*net.IPNet{} - for _, cidr := range cidrs { + networks := make([]*net.IPNet, len(cidrs)) + for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) } - networks = append(networks, nw) + networks[i] = nw } e.permittedIPRanges = append(e.permittedIPRanges, networks...) return nil @@ -153,13 +176,13 @@ func AddPermittedCIDRs(cidrs []string) NamePolicyOption { func WithExcludedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - networks := []*net.IPNet{} - for _, cidr := range cidrs { + networks := make([]*net.IPNet, len(cidrs)) + for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) } - networks = append(networks, nw) + networks[i] = nw } e.excludedIPRanges = networks return nil @@ -168,13 +191,13 @@ func WithExcludedCIDRs(cidrs []string) NamePolicyOption { func AddExcludedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - networks := []*net.IPNet{} - for _, cidr := range cidrs { + networks := make([]*net.IPNet, len(cidrs)) + for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) } - networks = append(networks, nw) + networks[i] = nw } e.excludedIPRanges = append(e.excludedIPRanges, networks...) return nil @@ -309,205 +332,269 @@ func AddExcludedIP(ip net.IP) NamePolicyOption { func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { + normalizedEmailAddresses := make([]string, len(emailAddresses)) + for i, email := range emailAddresses { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) + if err != nil { return err } + normalizedEmailAddresses[i] = normalizedEmailAddress } - e.permittedEmailAddresses = emailAddresses + e.permittedEmailAddresses = normalizedEmailAddresses return nil } } func AddPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { + normalizedEmailAddresses := make([]string, len(emailAddresses)) + for i, email := range emailAddresses { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) + if err != nil { return err } + normalizedEmailAddresses[i] = normalizedEmailAddress } - e.permittedEmailAddresses = append(e.permittedEmailAddresses, emailAddresses...) + e.permittedEmailAddresses = append(e.permittedEmailAddresses, normalizedEmailAddresses...) return nil } } func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { + normalizedEmailAddresses := make([]string, len(emailAddresses)) + for i, email := range emailAddresses { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) + if err != nil { return err } + normalizedEmailAddresses[i] = normalizedEmailAddress } - e.excludedEmailAddresses = emailAddresses + e.excludedEmailAddresses = normalizedEmailAddresses return nil } } func AddExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { + normalizedEmailAddresses := make([]string, len(emailAddresses)) + for i, email := range emailAddresses { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) + if err != nil { return err } + normalizedEmailAddresses[i] = normalizedEmailAddress } - e.excludedEmailAddresses = append(e.excludedEmailAddresses, emailAddresses...) + e.excludedEmailAddresses = append(e.excludedEmailAddresses, normalizedEmailAddresses...) return nil } } func WithPermittedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateEmailConstraint(emailAddress); err != nil { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) + if err != nil { return err } - e.permittedEmailAddresses = []string{emailAddress} + e.permittedEmailAddresses = []string{normalizedEmailAddress} return nil } } func AddPermittedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateEmailConstraint(emailAddress); err != nil { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) + if err != nil { return err } - e.permittedEmailAddresses = append(e.permittedEmailAddresses, emailAddress) + e.permittedEmailAddresses = append(e.permittedEmailAddresses, normalizedEmailAddress) return nil } } func WithExcludedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateEmailConstraint(emailAddress); err != nil { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) + if err != nil { return err } - e.excludedEmailAddresses = []string{emailAddress} + e.excludedEmailAddresses = []string{normalizedEmailAddress} return nil } } func AddExcludedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateEmailConstraint(emailAddress); err != nil { + normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) + if err != nil { return err } - e.excludedEmailAddresses = append(e.excludedEmailAddresses, emailAddress) + e.excludedEmailAddresses = append(e.excludedEmailAddresses, normalizedEmailAddress) return nil } } func WithPermittedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range uriDomains { - if err := validateURIDomainConstraint(domain); err != nil { + normalizedURIDomains := make([]string, len(uriDomains)) + for i, domain := range uriDomains { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) + if err != nil { return err } + normalizedURIDomains[i] = normalizedURIDomain } - e.permittedURIDomains = uriDomains + e.permittedURIDomains = normalizedURIDomains return nil } } func AddPermittedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range uriDomains { - if err := validateURIDomainConstraint(domain); err != nil { + normalizedURIDomains := make([]string, len(uriDomains)) + for i, domain := range uriDomains { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) + if err != nil { return err } + normalizedURIDomains[i] = normalizedURIDomain } - e.permittedURIDomains = append(e.permittedURIDomains, uriDomains...) + e.permittedURIDomains = append(e.permittedURIDomains, normalizedURIDomains...) return nil } } func WithPermittedURIDomain(uriDomain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateURIDomainConstraint(uriDomain); err != nil { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + if err != nil { return err } - e.permittedURIDomains = []string{uriDomain} + e.permittedURIDomains = []string{normalizedURIDomain} return nil } } func AddPermittedURIDomain(uriDomain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateURIDomainConstraint(uriDomain); err != nil { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + if err != nil { return err } - e.permittedURIDomains = append(e.permittedURIDomains, uriDomain) + e.permittedURIDomains = append(e.permittedURIDomains, normalizedURIDomain) return nil } } func WithExcludedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range uriDomains { - if err := validateURIDomainConstraint(domain); err != nil { + normalizedURIDomains := make([]string, len(uriDomains)) + for i, domain := range uriDomains { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) + if err != nil { return err } + normalizedURIDomains[i] = normalizedURIDomain } - e.excludedURIDomains = uriDomains + e.excludedURIDomains = normalizedURIDomains return nil } } func AddExcludedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - for _, domain := range uriDomains { - if err := validateURIDomainConstraint(domain); err != nil { + normalizedURIDomains := make([]string, len(uriDomains)) + for i, domain := range uriDomains { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) + if err != nil { return err } + normalizedURIDomains[i] = normalizedURIDomain } - e.excludedURIDomains = append(e.excludedURIDomains, uriDomains...) + e.excludedURIDomains = append(e.excludedURIDomains, normalizedURIDomains...) return nil } } func WithExcludedURIDomain(uriDomain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateURIDomainConstraint(uriDomain); err != nil { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + if err != nil { return err } - e.excludedURIDomains = []string{uriDomain} + e.excludedURIDomains = []string{normalizedURIDomain} return nil } } func AddExcludedURIDomain(uriDomain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - if err := validateURIDomainConstraint(uriDomain); err != nil { + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + if err != nil { return err } - e.excludedURIDomains = append(e.excludedURIDomains, uriDomain) + e.excludedURIDomains = append(e.excludedURIDomains, normalizedURIDomain) return nil } } -func validateDNSDomainConstraint(domain string) error { - if _, ok := domainToReverseLabels(domain); !ok { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) +func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) { + normalizedConstraint := strings.TrimSpace(constraint) + if strings.Contains(normalizedConstraint, "..") { + return "", errors.Errorf("domain constraint %q cannot have empty labels", constraint) } - return nil + if strings.HasPrefix(normalizedConstraint, "*.") { + normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period + } + if strings.Contains(normalizedConstraint, "*") { + return "", errors.Errorf("domain constraint %q can only have wildcard as starting character", constraint) + } + if _, ok := domainToReverseLabels(normalizedConstraint); !ok { + return "", errors.Errorf("cannot parse permitted domain constraint %q", constraint) + } + return normalizedConstraint, nil } -func validateEmailConstraint(constraint string) error { - if strings.Contains(constraint, "@") { - _, ok := parseRFC2821Mailbox(constraint) - if !ok { - return fmt.Errorf("cannot parse email constraint %q", constraint) +func normalizeAndValidateEmailConstraint(constraint string) (string, error) { + normalizedConstraint := strings.TrimSpace(constraint) + if strings.Contains(normalizedConstraint, "*") { + return "", fmt.Errorf("email constraint %q cannot contain asterisk", constraint) + } + if strings.Count(normalizedConstraint, "@") > 1 { + return "", fmt.Errorf("email constraint %q contains too many @ characters", constraint) + } + if normalizedConstraint[0] == '@' { + normalizedConstraint = normalizedConstraint[1:] // remove the leading @ as wildcard for emails + } + if normalizedConstraint[0] == '.' { + return "", fmt.Errorf("email constraint %q cannot start with period", constraint) + } + if strings.Contains(normalizedConstraint, "@") { + if _, ok := parseRFC2821Mailbox(normalizedConstraint); !ok { + return "", fmt.Errorf("cannot parse email constraint %q", constraint) } } - _, ok := domainToReverseLabels(constraint) - if !ok { - return fmt.Errorf("cannot parse email domain constraint %q", constraint) + if _, ok := domainToReverseLabels(normalizedConstraint); !ok { + return "", fmt.Errorf("cannot parse email domain constraint %q", constraint) } - return nil + return normalizedConstraint, nil } -func validateURIDomainConstraint(constraint string) error { - _, ok := domainToReverseLabels(constraint) - if !ok { - return fmt.Errorf("cannot parse URI domain constraint %q", constraint) +func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) { + normalizedConstraint := strings.TrimSpace(constraint) + if strings.Contains(normalizedConstraint, "..") { + return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint) } - return nil + if strings.HasPrefix(normalizedConstraint, "*.") { + normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period + } + if strings.Contains(normalizedConstraint, "*") { + return "", errors.Errorf("URI domain constraint %q can only have wildcard as starting character", constraint) + } + // TODO(hs): block constraints that look like IPs too? Because hosts can't be matched to those. + _, ok := domainToReverseLabels(normalizedConstraint) + if !ok { + return "", fmt.Errorf("cannot parse URI domain constraint %q", constraint) + } + return normalizedConstraint, nil } diff --git a/policy/x509/options_test.go b/policy/x509/options_test.go new file mode 100644 index 00000000..304e208f --- /dev/null +++ b/policy/x509/options_test.go @@ -0,0 +1,1339 @@ +package x509policy + +import ( + "net" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/smallstep/assert" +) + +func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/too-many-asterisks", + constraint: "**.local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-label", + constraint: "..local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-reverse", + constraint: ".", + want: "", + wantErr: true, + }, + { + name: "ok/wildcard", + constraint: "*.local", + want: ".local", + wantErr: false, + }, + { + name: "ok/specific-domain", + constraint: "example.local", + want: "example.local", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeAndValidateDNSDomainConstraint(tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("normalizeAndValidateDNSDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("normalizeAndValidateDNSDomainConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_normalizeAndValidateEmailConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/asterisk", + constraint: "*.local", + want: "", + wantErr: true, + }, + { + name: "fail/period", + constraint: ".local", + want: "", + wantErr: true, + }, + { + name: "fail/@period", + constraint: "@.local", + want: "", + wantErr: true, + }, + { + name: "fail/too-many-@s", + constraint: "@local@example.com", + want: "", + wantErr: true, + }, + { + name: "fail/parse-mailbox", + constraint: "mail@example.com" + string([]byte{0}), + want: "", + wantErr: true, + }, + { + name: "fail/parse-domain", + constraint: "example.com" + string([]byte{0}), + want: "", + wantErr: true, + }, + { + name: "ok/wildcard", + constraint: "@local", + want: "local", + wantErr: false, + }, + { + name: "ok/specific-mail", + constraint: "mail@local", + want: "mail@local", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeAndValidateEmailConstraint(tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("normalizeAndValidateEmailConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("normalizeAndValidateEmailConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/too-many-asterisks", + constraint: "**.local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-label", + constraint: "..local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-reverse", + constraint: ".", + want: "", + wantErr: true, + }, + { + name: "ok/wildcard", + constraint: "*.local", + want: ".local", + wantErr: false, + }, + { + name: "ok/specific-domain", + constraint: "example.local", + want: "example.local", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeAndValidateURIDomainConstraint(tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNew(t *testing.T) { + type test struct { + options []NamePolicyOption + want *NamePolicyEngine + wantErr bool + } + var tests = map[string]func(t *testing.T) test{ + "fail/with-permitted-dns-domains": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedDNSDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-dns-domains": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedDNSDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-dns-domains": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedDNSDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-dns-domains": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedDNSDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-dns-domain": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedDNSDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-dns-domain": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedDNSDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-dns-domain": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedDNSDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-dns-domain": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedDNSDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-cidrs": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-cidrs": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-cidrs": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-cidrs": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1//24"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedCIDR("127.0.0.1//24"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1//24"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedCIDR("127.0.0.1//24"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-emails": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedEmailAddresses([]string{"*.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-emails": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedEmailAddresses([]string{"*.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-emails": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedEmailAddresses([]string{"*.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-emails": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedEmailAddresses([]string{"*.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-email": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedEmailAddress("*.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-email": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedEmailAddress("*.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-email": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedEmailAddress("*.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-email": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedEmailAddress("*.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-uris": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedURIDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-uris": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedURIDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-uris": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedURIDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-uris": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedURIDomains([]string{"**.local"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-uri": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedURIDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-permitted-uri": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddPermittedURIDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-uri": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedURIDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "fail/add-excluded-uri": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + AddExcludedURIDomain("**.local"), + }, + want: nil, + wantErr: true, + } + }, + "ok/default": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{}, + want: &NamePolicyEngine{}, + wantErr: false, + } + }, + "ok/subject-verification": func(t *testing.T) test { + options := []NamePolicyOption{ + WithSubjectCommonNameVerification(), + } + return test{ + options: options, + want: &NamePolicyEngine{ + verifySubjectCommonName: true, + }, + wantErr: false, + } + }, + "ok/literal-wildcards": func(t *testing.T) test { + options := []NamePolicyOption{ + WithAllowLiteralWildcardNames(), + } + return test{ + options: options, + want: &NamePolicyEngine{ + allowLiteralWildcardNames: true, + }, + wantErr: false, + } + }, + "ok/with-permitted-dns-wildcard-domains": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomains([]string{"*.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{".local", ".example.com"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-dns-wildcard-domains": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomains([]string{"*.local"}), + AddPermittedDNSDomains([]string{"*.example.com", "*.local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{".local", ".example.com"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-dns-domains": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedDNSDomains([]string{"*.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedDNSDomains: []string{".local", ".example.com"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-dns-domains": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedDNSDomains([]string{"*.local"}), + AddExcludedDNSDomains([]string{"*.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedDNSDomains: []string{".local", ".example.com"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-dns-wildcard-domain": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomain("*.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{".example.com"}, + numberOfDNSDomainConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-dns-wildcard-domain": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomain("*.example.com"), + AddPermittedDNSDomain("*.local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{".example.com", ".local"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-dns-domain": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomain("www.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{"www.example.com"}, + numberOfDNSDomainConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-dns-domain": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedDNSDomain("www.example.com"), + AddPermittedDNSDomain("host.local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedDNSDomains: []string{"www.example.com", "host.local"}, + numberOfDNSDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-ip-ranges": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIPRanges( + []*net.IPNet{ + nw1, nw2, + }, + ), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-ip-ranges": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIPRanges( + []*net.IPNet{ + nw1, + }, + ), + AddPermittedIPRanges( + []*net.IPNet{ + nw1, nw2, + }, + ), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-ip-ranges": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIPRanges( + []*net.IPNet{ + nw1, nw2, + }, + ), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-ip-ranges": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIPRanges( + []*net.IPNet{ + nw1, + }, + ), + AddExcludedIPRanges( + []*net.IPNet{ + nw1, nw2, + }, + ), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-cidrs": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-cidrs": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedCIDRs([]string{"127.0.0.1/24"}), + AddPermittedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-cidrs": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-cidrs": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedCIDRs([]string{"127.0.0.1/24"}), + AddExcludedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1/24"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1/24"), + AddPermittedCIDR("192.168.0.1/24"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.1/24") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + AddExcludedCIDR("192.168.0.1/24"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-ipv4": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.15/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIP(ip1), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-ipv4": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.45/32") + assert.FatalError(t, err) + ip2, nw2, err := net.ParseCIDR("192.168.0.55/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIP(ip1), + AddPermittedIP(ip2), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-ipv4": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.15/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIP(ip1), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-ipv4": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.45/32") + assert.FatalError(t, err) + ip2, nw2, err := net.ParseCIDR("192.168.0.55/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIP(ip1), + AddExcludedIP(ip2), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-ipv6": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIP(ip1), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-ipv6": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.10/32") + assert.FatalError(t, err) + ip2, nw2, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIP(ip1), + AddPermittedIP(ip2), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-ipv6": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIP(ip1), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, + }, + numberOfIPRangeConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-ipv6": func(t *testing.T) test { + ip1, nw1, err := net.ParseCIDR("127.0.0.10/32") + assert.FatalError(t, err) + ip2, nw2, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIP(ip1), + AddExcludedIP(ip2), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-emails": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedEmailAddresses([]string{"mail@local", "@example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-emails": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedEmailAddresses([]string{"mail@local"}), + AddPermittedEmailAddresses([]string{"@example.com", "mail@local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-emails": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedEmailAddresses([]string{"mail@local", "@example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-emails": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedEmailAddresses([]string{"mail@local"}), + AddExcludedEmailAddresses([]string{"@example.com", "mail@local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-email": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedEmailAddress("mail@local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedEmailAddresses: []string{"mail@local"}, + numberOfEmailAddressConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-email": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedEmailAddress("mail@local"), + AddPermittedEmailAddress("@example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-email": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedEmailAddress("mail@local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedEmailAddresses: []string{"mail@local"}, + numberOfEmailAddressConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-email": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedEmailAddress("mail@local"), + AddExcludedEmailAddress("@example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedEmailAddresses: []string{"mail@local", "example.com"}, + numberOfEmailAddressConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-uris": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomains([]string{"host.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-permitted-uris": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomains([]string{"host.local"}), + AddPermittedURIDomains([]string{"*.example.com", "host.local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-uris": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedURIDomains([]string{"host.local", "*.example.com"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/add-excluded-uris": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedURIDomains([]string{"host.local"}), + AddExcludedURIDomains([]string{"*.example.com", "host.local"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-permitted-uri": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomain("host.local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{"host.local"}, + numberOfURIDomainConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-permitted-uri": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomain("host.local"), + AddPermittedURIDomain("*.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-uri": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedURIDomain("host.local"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedURIDomains: []string{"host.local"}, + numberOfURIDomainConstraints: 1, + totalNumberOfExcludedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, + "ok/add-excluded-uri": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedURIDomain("host.local"), + AddExcludedURIDomain("*.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedURIDomains: []string{"host.local", ".example.com"}, + numberOfURIDomainConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + got, err := New(tc.options...) + if (err != nil) != tc.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tc.wantErr) + return + } + if !cmp.Equal(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{})) { + t.Errorf("New() diff =\n %s", cmp.Diff(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{}))) + } + }) + } +} diff --git a/policy/x509/x509.go b/policy/x509/x509.go index 408251cd..5a8337b9 100755 --- a/policy/x509/x509.go +++ b/policy/x509/x509.go @@ -50,8 +50,13 @@ func (e CertificateInvalidError) Error() string { // TODO(hs): the x509 RFC also defines name checks on directory name; support that? // TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine? type NamePolicyEngine struct { - options []NamePolicyOption + + // verifySubjectCommonName is set when Subject Common Name must be verified verifySubjectCommonName bool + // allowLiteralWildcardNames allows literal wildcard DNS domains + allowLiteralWildcardNames bool + + // permitted and exluded constraints similar to x509 Name Constraints permittedDNSDomains []string excludedDNSDomains []string permittedIPRanges []*net.IPNet @@ -60,22 +65,84 @@ type NamePolicyEngine struct { excludedEmailAddresses []string permittedURIDomains []string excludedURIDomains []string + + // some internal counts for housekeeping + numberOfDNSDomainConstraints int + numberOfIPRangeConstraints int + numberOfEmailAddressConstraints int + numberOfURIDomainConstraints int + totalNumberOfPermittedConstraints int + totalNumberOfExcludedConstraints int + totalNumberOfConstraints int } // NewNamePolicyEngine creates a new NamePolicyEngine with NamePolicyOptions func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { e := &NamePolicyEngine{} - e.options = append(e.options, opts...) - for _, option := range e.options { + for _, option := range opts { if err := option(e); err != nil { return nil, err } } + e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains) + e.permittedIPRanges = removeDuplicateIPRanges(e.permittedIPRanges) + e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses) + e.permittedURIDomains = removeDuplicates(e.permittedURIDomains) + + e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains) + e.excludedIPRanges = removeDuplicateIPRanges(e.excludedIPRanges) + e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses) + e.excludedURIDomains = removeDuplicates(e.excludedURIDomains) + + e.numberOfDNSDomainConstraints = len(e.permittedDNSDomains) + len(e.excludedDNSDomains) + e.numberOfIPRangeConstraints = len(e.permittedIPRanges) + len(e.excludedIPRanges) + e.numberOfEmailAddressConstraints = len(e.permittedEmailAddresses) + len(e.excludedEmailAddresses) + e.numberOfURIDomainConstraints = len(e.permittedURIDomains) + len(e.excludedURIDomains) + + e.totalNumberOfPermittedConstraints = len(e.permittedDNSDomains) + len(e.permittedIPRanges) + + len(e.permittedEmailAddresses) + len(e.permittedURIDomains) + + e.totalNumberOfExcludedConstraints = len(e.excludedDNSDomains) + len(e.excludedIPRanges) + + len(e.excludedEmailAddresses) + len(e.excludedURIDomains) + + e.totalNumberOfConstraints = e.totalNumberOfPermittedConstraints + e.totalNumberOfExcludedConstraints + return e, nil } +func removeDuplicates(strSlice []string) []string { + if len(strSlice) == 0 { + return nil + } + keys := make(map[string]bool) + result := []string{} + for _, item := range strSlice { + if _, value := keys[item]; !value { + keys[item] = true + result = append(result, item) + } + } + return result +} + +func removeDuplicateIPRanges(ipRanges []*net.IPNet) []*net.IPNet { + if len(ipRanges) == 0 { + return nil + } + keys := make(map[string]bool) + result := []*net.IPNet{} + for _, item := range ipRanges { + key := item.String() + if _, value := keys[key]; !value { + keys[key] = true + result = append(result, item) + } + } + return result +} + // AreCertificateNamesAllowed verifies that all SANs in a Certificate are allowed. func (e *NamePolicyEngine) AreCertificateNamesAllowed(cert *x509.Certificate) (bool, error) { dnsNames, ips, emails, uris := cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs @@ -155,28 +222,51 @@ func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.I // 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) error { - // TODO: return our own type of error? + // nothing to compare against; return early + if e.totalNumberOfConstraints == 0 { + return nil + } - // TODO: set limit on total of all names? In x509 there's a limit on the number of comparisons + // TODO: return our own type(s) of error? + // 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 // 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 // executed so far. + // TODO: implement matching URI schemes, paths, etc; not just the domain + // TODO: gather all errors, or return early? Currently we return early on the first wrong name; check might fail for multiple names. // Perhaps make that an option? for _, dns := range dnsNames { + // if there are DNS names to check, no DNS constraints set, but there are other permitted constraints, + // 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). + if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("dns %q is not permitted by any constraint", dns), // TODO(hs): change this error (message) + } + } if _, ok := domainToReverseLabels(dns); !ok { return errors.Errorf("cannot parse dns %q", dns) } if err := checkNameConstraints("dns", dns, dns, func(parsedName, constraint interface{}) (bool, error) { - return matchDomainConstraint(parsedName.(string), constraint.(string)) + return e.matchDomainConstraint(parsedName.(string), constraint.(string)) }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { return err } } for _, ip := range ips { + if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("ip %q is not permitted by any constraint", ip.String()), + } + } if err := checkNameConstraints("ip", ip.String(), ip, func(parsedName, constraint interface{}) (bool, error) { return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet)) @@ -186,22 +276,34 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } for _, email := range emailAddresses { + if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("email %q is not permitted by any constraint", email), + } + } mailbox, ok := parseRFC2821Mailbox(email) if !ok { return fmt.Errorf("cannot parse rfc822Name %q", mailbox) } if err := checkNameConstraints("email", email, mailbox, func(parsedName, constraint interface{}) (bool, error) { - return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) + return e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) }, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil { return err } } for _, uri := range uris { + if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("uri %q is not permitted by any constraint", uri.String()), + } + } if err := checkNameConstraints("uri", uri.String(), uri, func(parsedName, constraint interface{}) (bool, error) { - return matchURIConstraint(parsedName.(*url.URL), constraint.(string)) + return e.matchURIConstraint(parsedName.(*url.URL), constraint.(string)) }, e.permittedURIDomains, e.excludedURIDomains); err != nil { return err } @@ -217,11 +319,10 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA return nil } -// checkNameConstraints checks that c permits a child certificate to claim the -// given name, of type nameType. The argument parsedName contains the parsed -// form of name, suitable for passing to the match function. The total number -// of comparisons is tracked in the given count and should not exceed the given -// limit. +// 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. The total number of comparisons is tracked in the given +// count and should not exceed the given limit. // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go func checkNameConstraints( nameType string, @@ -476,13 +577,44 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) { } // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func matchDomainConstraint(domain, constraint string) (bool, error) { +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. if constraint == "" { return true, nil } + // A single whitespace seems to be considered a valid domain, but we don't allow it. + if domain == " " { + return false, nil + } + + // Block domains that start with just a period + // TODO(hs): check if we should allow domains starting with "." at all; not sure if this is allowed in x509 names and certs. + if domain[0] == '.' { + return false, nil + } + + // Block wildcard domains that don't start with exactly "*." (i.e. double wildcards and such) + if domain[0] == '*' && domain[1] != '.' { + return false, nil + } + + // Check if the domain starts with a wildcard and return early if not allowed + if strings.HasPrefix(domain, "*.") && !e.allowLiteralWildcardNames { + return false, nil + } + + // Only allow asterisk at the start of the domain; we don't allow them as part of a domain label or as a (sub)domain label (currently) + if strings.LastIndex(domain, "*") > 0 { + return false, nil + } + + // Don't allow constraints with empty labels in any position + if strings.Contains(constraint, "..") { + return false, nil + } + domainLabels, ok := domainToReverseLabels(domain) if !ok { return false, fmt.Errorf("cannot parse domain %q", domain) @@ -491,7 +623,9 @@ func matchDomainConstraint(domain, constraint string) (bool, error) { // RFC 5280 says that a leading period in a domain name means that at // least one label must be prepended, but only for URI and email // constraints, not DNS constraints. The code also supports that - // behavior for DNS constraints. + // behavior for DNS constraints. In our adaptation of the original + // Go stdlib x509 Name Constraint implementation we look for exactly + // one subdomain, currently. mustHaveSubdomains := false if constraint[0] == '.' { @@ -501,11 +635,22 @@ func matchDomainConstraint(domain, constraint string) (bool, error) { constraintLabels, ok := domainToReverseLabels(constraint) if !ok { - return false, fmt.Errorf("cannot parse domain %q", constraint) + return false, fmt.Errorf("cannot parse domain constraint %q", constraint) } - if len(domainLabels) < len(constraintLabels) || - (mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) { + // 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": "." + // in the future we could extend this to support multiple additional labels and/or more + // complex matching. + expectedNumberOfLabels++ + } + + if len(domainLabels) != expectedNumberOfLabels { return false, nil } @@ -552,8 +697,12 @@ func isIPv4(ip net.IP) bool { } // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { - // If the constraint contains an @, then it specifies an exact mailbox name. +func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { + // TODO(hs): handle literal wildcard case for emails? Does that even make sense? + // If the constraint contains an @, then it specifies an exact mailbox name (currently) + if strings.Contains(constraint, "*") { + return false, fmt.Errorf("email constraint %q cannot contain asterisk", constraint) + } if strings.Contains(constraint, "@") { constraintMailbox, ok := parseRFC2821Mailbox(constraint) if !ok { @@ -564,11 +713,11 @@ func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, erro // Otherwise the constraint is like a DNS constraint of the domain part // of the mailbox. - return matchDomainConstraint(mailbox.domain, constraint) + return e.matchDomainConstraint(mailbox.domain, constraint) } // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { +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 // component with a host name specified as a fully qualified domain @@ -582,6 +731,11 @@ func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String()) } + // Block hosts with the wildcard character; no exceptions, also not when wildcards allowed. + if strings.Contains(host, "*") { + return false, fmt.Errorf("URI host %q cannot contain asterisk", uri.String()) + } + if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") { var err error host, _, err = net.SplitHostPort(uri.Host) @@ -592,8 +746,10 @@ func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") || net.ParseIP(host) != nil { - return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String()) + return false, fmt.Errorf("URI with IP %q cannot be matched against constraints", uri.String()) } - return matchDomainConstraint(host, constraint) + // TODO(hs): add checks for scheme, path, etc.; either here, or in a different constraint matcher (to keep this one simple) + + return e.matchDomainConstraint(host, constraint) } diff --git a/policy/x509/x509_test.go b/policy/x509/x509_test.go index 103c6009..a2977214 100755 --- a/policy/x509/x509_test.go +++ b/policy/x509/x509_test.go @@ -10,31 +10,582 @@ import ( "github.com/smallstep/assert" ) -func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { - // TODO(hs): refactor these tests into using validateNames instead of AreCertificateNamesAllowed - // TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on - type fields struct { - verifySubjectCommonName bool - permittedDNSDomains []string - excludedDNSDomains []string - permittedIPRanges []*net.IPNet - excludedIPRanges []*net.IPNet - permittedEmailAddresses []string - excludedEmailAddresses []string - permittedURIDomains []string - excludedURIDomains []string +// 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): check errors (reasons) are as expected + +func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { + tests := []struct { + name string + engine *NamePolicyEngine + domain string + constraint string + want bool + wantErr bool + }{ + { + name: "fail/wildcard", + engine: &NamePolicyEngine{}, + domain: "host.local", + constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain + want: false, + wantErr: false, + }, + { + name: "fail/wildcard-literal", + engine: &NamePolicyEngine{}, + domain: "*.example.com", + constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain + want: false, + wantErr: false, + }, + { + name: "fail/specific-domain", + engine: &NamePolicyEngine{}, + domain: "www.example.com", + constraint: "host.example.com", + want: false, + wantErr: false, + }, + { + name: "fail/single-whitespace-domain", + engine: &NamePolicyEngine{}, + domain: " ", + constraint: "host.example.com", + want: false, + wantErr: false, + }, + { + name: "fail/period-domain", + engine: &NamePolicyEngine{}, + domain: ".host.example.com", + constraint: ".example.com", + want: false, + wantErr: false, + }, + { + name: "fail/wrong-asterisk-prefix", + engine: &NamePolicyEngine{}, + domain: "*Xexample.com", + constraint: ".example.com", + want: false, + wantErr: false, + }, + { + name: "fail/asterisk-in-domain", + engine: &NamePolicyEngine{}, + domain: "e*ample.com", + constraint: ".com", + want: false, + wantErr: false, + }, + { + name: "fail/asterisk-label", + engine: &NamePolicyEngine{}, + domain: "example.*.local", + constraint: ".local", + want: false, + wantErr: false, + }, + { + name: "fail/multiple-periods", + engine: &NamePolicyEngine{}, + domain: "example.local", + constraint: "..local", + want: false, + wantErr: false, + }, + { + name: "fail/error-parsing-domain", + engine: &NamePolicyEngine{}, + domain: string([]byte{0}), + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/error-parsing-constraint", + engine: &NamePolicyEngine{}, + domain: "example.local", + constraint: string([]byte{0}), + want: false, + wantErr: true, + }, + { + name: "fail/no-subdomain", + engine: &NamePolicyEngine{}, + domain: "local", + constraint: ".local", + want: false, + wantErr: false, + }, + { + name: "fail/too-many-subdomains", + engine: &NamePolicyEngine{}, + domain: "www.example.local", + constraint: ".local", + want: false, + wantErr: false, + }, + { + name: "fail/wrong-domain", + engine: &NamePolicyEngine{}, + domain: "example.notlocal", + constraint: ".local", + want: false, + wantErr: false, + }, + { + name: "ok/empty-constraint", + engine: &NamePolicyEngine{}, + domain: "www.example.com", + constraint: "", + want: true, + wantErr: false, + }, + { + name: "ok/wildcard", + engine: &NamePolicyEngine{}, + domain: "www.example.com", + constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain + want: true, + wantErr: false, + }, + { + name: "ok/wildcard-literal", + engine: &NamePolicyEngine{ + allowLiteralWildcardNames: true, + }, + domain: "*.example.com", // specifically allowed using an option on the NamePolicyEngine + constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain + want: true, + wantErr: false, + }, + { + name: "ok/specific-domain", + engine: &NamePolicyEngine{}, + domain: "www.example.com", + constraint: "www.example.com", + want: true, + wantErr: false, + }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.engine.matchDomainConstraint(tt.domain, tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.matchDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("NamePolicyEngine.matchDomainConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_matchIPConstraint(t *testing.T) { + nat64IP, nat64Net, err := net.ParseCIDR("64:ff9b::/96") + assert.FatalError(t, err) + tests := []struct { + name string + ip net.IP + constraint *net.IPNet + want bool + wantErr bool + }{ + { + name: "false/ipv4-in-ipv6-nat64", + ip: net.ParseIP("192.0.2.128"), + constraint: nat64Net, + want: false, + wantErr: false, + }, + { + name: "ok/ipv4", + ip: net.ParseIP("127.0.0.1"), + constraint: &net.IPNet{ + IP: net.ParseIP("127.0.0.0"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv6", + ip: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7335"), + constraint: &net.IPNet{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-in-ipv6", // ipv4 in ipv6 addresses are considered the same in the current implementation, because Go parses them as IPv4 + ip: net.ParseIP("::ffff:192.0.2.128"), + constraint: &net.IPNet{ + IP: net.ParseIP("192.0.2.0"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-in-ipv6-nat64-fixed-ip", + ip: nat64IP, + constraint: nat64Net, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-in-ipv6-nat64", + ip: net.ParseIP("64:ff9b::192.0.2.129"), + constraint: nat64Net, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := matchIPConstraint(tt.ip, tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("matchIPConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("matchIPConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNamePolicyEngine_matchEmailConstraint(t *testing.T) { + + tests := []struct { + name string + engine *NamePolicyEngine + mailbox rfc2821Mailbox + constraint string + want bool + wantErr bool + }{ + { + name: "fail/asterisk-prefix", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "*@example.com", + want: false, + wantErr: true, + }, + { + name: "fail/asterisk-label", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "@host.*.example.com", + want: false, + wantErr: true, + }, + { + name: "fail/asterisk-inside-local", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "m*il@local", + want: false, + wantErr: true, + }, + { + name: "fail/asterisk-inside-domain", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "@h*st.example.com", + want: false, + wantErr: true, + }, + { + name: "fail/parse-email", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "@example.com", + want: false, + wantErr: true, + }, + { + name: "fail/wildcard", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "example.com", + want: false, + wantErr: false, + }, + { + name: "fail/wildcard-x509-period", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: ".local", // "wildcard" for the local domain; requires exactly 1 subdomain + want: false, + wantErr: false, + }, + { + name: "fail/specific-mail-wrong-domain", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "mail@example.com", + want: false, + wantErr: false, + }, + { + name: "fail/specific-mail-wrong-local", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "root", + domain: "example.com", + }, + constraint: "mail@example.com", + want: false, + wantErr: false, + }, + { + name: "ok/wildcard", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "local", // "wildcard" for the local domain + want: true, + wantErr: false, + }, + { + name: "ok/wildcard-x509-period", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "example.local", + }, + constraint: ".local", // "wildcard" for the local domain; requires exactly 1 subdomain + want: true, + wantErr: false, + }, + { + name: "ok/specific-mail", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "mail@local", + want: true, + wantErr: false, + }, + { + name: "ok/wildcard-tld", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "example.com", + }, + constraint: "example.com", // "wildcard" for 'example.com' + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.engine.matchEmailConstraint(tt.mailbox, tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.matchEmailConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("NamePolicyEngine.matchEmailConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNamePolicyEngine_matchURIConstraint(t *testing.T) { + tests := []struct { + name string + engine *NamePolicyEngine + uri *url.URL + constraint string + want bool + wantErr bool + }{ + { + name: "fail/empty-host", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "", + }, + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/host-with-asterisk-prefix", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "*.local", + }, + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/host-with-asterisk-label", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "host.*.local", + }, + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/host-with-asterisk-inside", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "h*st.local", + }, + constraint: ".local", + want: false, + wantErr: true, + }, + { + name: "fail/wildcard", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.example.notlocal", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: false, + }, + { + name: "fail/wildcard-subdomains-too-deep", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.sub.example.local", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: false, + }, + { + name: "fail/host-with-port-split-error", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.example.local::8080", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: true, + }, + { + name: "fail/host-with-ipv4", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "127.0.0.1", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: true, + }, + { + name: "fail/host-with-ipv6", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: false, + wantErr: true, + }, + { + name: "ok/wildcard", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.example.local", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: true, + wantErr: false, + }, + { + name: "ok/host-with-port", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.example.local:8080", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.engine.matchURIConstraint(tt.uri, tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.matchURIConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("NamePolicyEngine.matchURIConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { tests := []struct { name string - fields fields + options []NamePolicyOption cert *x509.Certificate want bool wantErr bool }{ + // SINGLE SAN TYPE PERMITTED FAILURE TESTS { name: "fail/dns-permitted", - fields: fields{ - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -42,10 +593,23 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/dns-permitted-wildcard-literal-x509", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.x509local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "*.x509local", + }, + }, + want: false, + wantErr: true, + }, { name: "fail/dns-permitted-single-host", - fields: fields{ - permittedDNSDomains: []string{"host.local"}, + options: []NamePolicyOption{ + AddPermittedDNSDomain("host.local"), }, cert: &x509.Certificate{ DNSNames: []string{"differenthost.local"}, @@ -55,8 +619,8 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/dns-permitted-no-label", - fields: fields{ - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"local"}, @@ -66,8 +630,8 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/dns-permitted-empty-label", - fields: fields{ - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"www..local"}, @@ -75,10 +639,186 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/dns-permitted-dot-domain", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + ".local", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted-wildcard-multiple-subdomains", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "sub.example.local", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted-wildcard-literal", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "*.local", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ipv4-permitted", + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ipv6-permitted", + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + ), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("3001:0db8:85a3:0000:0000:8a2e:0370:7334")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-wildcard", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{ + "test@local.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-wildcard-x509", + options: []NamePolicyOption{ + AddPermittedEmailAddress("example.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{ + "test@local.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-specific-mailbox", + options: []NamePolicyOption{ + AddPermittedEmailAddress("test@local.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{ + "root@local.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-wildcard-subdomain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{ + "test@sub.example.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/permitted-uri-domain-wildcard", + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "example.com", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/permitted-uri", + options: []NamePolicyOption{ + AddPermittedURIDomain("test.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "bad.local", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/permitted-uri-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "*.local", + }, + }, + }, + want: false, + wantErr: true, + }, + // SINGLE SAN TYPE EXCLUDED FAILURE TESTS { name: "fail/dns-excluded", - fields: fields{ - excludedDNSDomains: []string{"example.com"}, + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.example.com"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -88,40 +828,26 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/dns-excluded-single-host", - fields: fields{ - excludedDNSDomains: []string{"example.com"}, + options: []NamePolicyOption{ + AddExcludedDNSDomain("host.example.com"), }, cert: &x509.Certificate{ - DNSNames: []string{"example.com"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/ipv4-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, - }, - }, - cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, + DNSNames: []string{"host.example.com"}, }, want: false, wantErr: true, }, { name: "fail/ipv4-excluded", - fields: fields{ - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + options: []NamePolicyOption{ + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, @@ -129,31 +855,17 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, - { - name: "fail/ipv6-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, - }, - }, - cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("3001:0db8:85a3:0000:0000:8a2e:0370:7334")}, - }, - want: false, - wantErr: true, - }, { name: "fail/ipv6-excluded", - fields: fields{ - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), + options: []NamePolicyOption{ + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, @@ -162,9 +874,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/mail-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, + name: "fail/mail-excluded", + options: []NamePolicyOption{ + AddExcludedEmailAddress("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, @@ -172,145 +884,28 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, - { - name: "fail/mail-permitted-period-domain", - fields: fields{ - permittedEmailAddresses: []string{".example.local"}, // any address in a domain, but not on the host example.local - }, - cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/mail-excluded", - fields: fields{ - excludedEmailAddresses: []string{"example.local"}, - }, - cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/uri-permitted", - fields: fields{ - permittedURIDomains: []string{".example.com"}, - }, - cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "www.example.local", - }, - }, - }, - want: false, - wantErr: true, - }, - { - name: "fail/uri-permitted-period-host", - fields: fields{ - permittedURIDomains: []string{".example.local"}, - }, - cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "example.local", - }, - }, - }, - want: false, - wantErr: true, - }, - { - name: "fail/uri-permitted-period-host-certificate", - fields: fields{ - permittedURIDomains: []string{".example.local"}, - }, - cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: ".example.local", - }, - }, - }, - want: false, - wantErr: true, - }, - { - name: "fail/uri-permitted-empty-host", - fields: fields{ - permittedURIDomains: []string{".example.com"}, - }, - cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "", - }, - }, - }, - want: false, - wantErr: true, - }, - { - name: "fail/uri-permitted-port-missing", - fields: fields{ - permittedURIDomains: []string{".example.com"}, - }, - cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "example.local::", - }, - }, - }, - want: false, - wantErr: true, - }, - { - name: "fail/uri-permitted-ip", - fields: fields{ - permittedURIDomains: []string{".example.com"}, - }, - cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "127.0.0.1", - }, - }, - }, - want: false, - wantErr: true, - }, { name: "fail/uri-excluded", - fields: fields{ - excludedURIDomains: []string{".example.local"}, + options: []NamePolicyOption{ + AddExcludedURIDomain("*.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "www.example.local", + Host: "www.example.com", }, }, }, want: false, wantErr: true, }, + // SUBJECT FAILURE TESTS { name: "fail/subject-dns-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -322,9 +917,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-dns-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -336,14 +931,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-ipv4-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -355,18 +952,20 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-ipv4-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ - CommonName: "127.0.0.1", + CommonName: "127.0.0.30", }, }, want: false, @@ -374,14 +973,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-ipv6-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -393,14 +994,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-ipv6-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, }, - }, + ), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -412,9 +1015,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-email-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedEmailAddresses: []string{"example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -426,9 +1029,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-email-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedEmailAddresses: []string{"example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -440,9 +1043,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-uri-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedURIDomains: []string{".example.com"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -454,9 +1057,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "fail/subject-uri-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedURIDomains: []string{".example.com"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -466,25 +1069,199 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, + // DIFFERENT SAN PERMITTED FAILURE TESTS + { + name: "fail/dns-permitted-with-ip-name", // when only DNS is permitted, IPs are not allowed. + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted-with-mail", // when only DNS is permitted, mails are not allowed. + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@smallstep.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/dns-permitted-with-uri", // when only DNS is permitted, URIs are not allowed. + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ip-permitted-with-dns-name", // when only IP is permitted, DNS names are not allowed. + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + DNSNames: []string{"www.example.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ip-permitted-with-mail", // when only IP is permitted, mails are not allowed. + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@smallstep.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ip-permitted-with-uri", // when only IP is permitted, URIs are not allowed. + options: []NamePolicyOption{ + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-with-dns-name", // when only mail is permitted, DNS names are not allowed. + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"www.example.com"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-with-ip", // when only mail is permitted, IPs are not allowed. + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{ + net.ParseIP("127.0.0.1"), + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-with-uri", // when only mail is permitted, URIs are not allowed. + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-with-dns-name", // when only URI is permitted, DNS names are not allowed. + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"host.local"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, IPs are not allowed. + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{ + net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, mails are not allowed. + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@smallstep.com"}, + }, + want: false, + wantErr: true, + }, + // COMBINED FAILURE TESTS { name: "fail/combined-simple-all-badhost.local", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, - permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - permittedEmailAddresses: []string{"example.local"}, - permittedURIDomains: []string{".example.local"}, - excludedDNSDomains: []string{"badhost.local"}, - excludedIPRanges: []*net.IPNet{{IP: net.ParseIP("1.1.1.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - excludedEmailAddresses: []string{"badmail@example.local"}, - excludedURIDomains: []string{"https://badwww.example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedURIDomain("*.example.local"), + WithExcludedDNSDomain("badhost.local"), + WithExcludedCIDR("127.0.0.128/25"), + WithExcludedEmailAddress("badmail@example.local"), + WithExcludedURIDomain("badwww.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ CommonName: "badhost.local", }, DNSNames: []string{"example.local"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.130")}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.40")}, EmailAddresses: []string{"mail@example.local"}, URIs: []*url.URL{ { @@ -496,9 +1273,10 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: false, wantErr: true, }, + // NO CONSTRAINT SUCCESS TESTS { - name: "ok/no-constraints", - fields: fields{}, + name: "ok/dns-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, @@ -506,162 +1284,39 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/empty-dns-constraint", - fields: fields{ - permittedDNSDomains: []string{""}, - }, + name: "ok/ipv4-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/dns-permitted", - fields: fields{ - permittedDNSDomains: []string{".local"}, - }, - cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/dns-excluded", - fields: fields{ - excludedDNSDomains: []string{".notlocal"}, - }, - cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/ipv4-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + IPAddresses: []net.IP{ + net.ParseIP("127.0.0.1"), }, }, - cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, - }, want: true, wantErr: false, }, { - name: "ok/ipv4-excluded", - fields: fields{ - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + name: "ok/ipv6-no-constraints", + options: []NamePolicyOption{}, + cert: &x509.Certificate{ + IPAddresses: []net.IP{ + net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), }, }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("10.10.10.10")}, + EmailAddresses: []string{"mail@smallstep.com"}, }, want: true, wantErr: false, }, { - name: "ok/ipv6-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, - }, - }, - cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/ipv6-excluded", - fields: fields{ - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, - }, - }, - cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/mail-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, - }, - cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/mail-permitted-with-period-domain", - fields: fields{ - permittedEmailAddresses: []string{".example.local"}, - }, - cert: &x509.Certificate{ - EmailAddresses: []string{"mail@somehost.example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/mail-permitted-with-multiple-labels", - fields: fields{ - permittedEmailAddresses: []string{".example.local"}, - }, - cert: &x509.Certificate{ - EmailAddresses: []string{"mail@sub.www.example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/mail-excluded", - fields: fields{ - excludedEmailAddresses: []string{"example.notlocal"}, - }, - cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/mail-excluded-with-period-domain", - fields: fields{ - excludedEmailAddresses: []string{".example.notlocal"}, - }, - cert: &x509.Certificate{ - EmailAddresses: []string{"mail@somehost.example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/uri-permitted", - fields: fields{ - permittedURIDomains: []string{".example.com"}, - }, + name: "ok/uri-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ URIs: []*url.URL{ { @@ -673,10 +1328,201 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/subject-no-constraints", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "www.example.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-empty-no-constraints", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "", + }, + }, + want: true, + wantErr: false, + }, + // SINGLE SAN TYPE PERMITTED SUCCESS TESTS + { + name: "ok/dns-permitted", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-permitted-wildcard", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + AddPermittedDNSDomain(".x509local"), + WithAllowLiteralWildcardNames(), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "host.local", + "test.x509local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/empty-dns-constraint", + options: []NamePolicyOption{ + AddPermittedDNSDomain(""), + }, + cert: &x509.Certificate{ + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-permitted-wildcard-literal", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + AddPermittedDNSDomain("*.x509local"), + WithAllowLiteralWildcardNames(), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "*.local", + "*.x509local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-permitted-combined", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), + AddPermittedDNSDomain("*.x509local"), + AddPermittedDNSDomain("host.example.com"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "example.local", + "example.x509local", + "host.example.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-permitted", + options: []NamePolicyOption{ + AddPermittedCIDR("127.0.0.1/24"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv6-permitted", + options: []NamePolicyOption{ + AddPermittedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-permitted-wildcard", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{ + "test@example.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-permitted-plain-domain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("example.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{ + "test@example.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-permitted-specific-mailbox", + options: []NamePolicyOption{ + AddPermittedEmailAddress("test@local.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{ + "test@local.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-permitted-domain-wildcard", + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "example.local", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-permitted-specific-uri", + options: []NamePolicyOption{ + AddPermittedURIDomain("test.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "test.local", + }, + }, + }, + want: true, + wantErr: false, + }, { name: "ok/uri-permitted-with-port", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + options: []NamePolicyOption{ + AddPermittedURIDomain(".example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -689,26 +1535,296 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: true, wantErr: false, }, + // SINGLE SAN TYPE EXCLUDED SUCCESS TESTS { - name: "ok/uri-sub-permitted", - fields: fields{ - permittedURIDomains: []string{"example.com"}, + name: "ok/dns-excluded", + options: []NamePolicyOption{ + WithExcludedDNSDomain("*.notlocal"), }, cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "sub.host.example.com", + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv4-excluded", + options: []NamePolicyOption{ + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, - }, + ), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("10.10.10.10")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/ipv6-excluded", + options: []NamePolicyOption{ + AddExcludedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded", + options: []NamePolicyOption{ + WithExcludedEmailAddress("@notlocal"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-subdomain", + options: []NamePolicyOption{ + WithExcludedEmailAddress("@local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.local"}, }, want: true, wantErr: false, }, { name: "ok/uri-excluded", - fields: fields{ - excludedURIDomains: []string{".google.com"}, + options: []NamePolicyOption{ + WithExcludedURIDomain("*.google.com"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + // SUBJECT SUCCESS TESTS + { + name: "ok/subject-empty", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "", + }, + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-dns-permitted", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-dns-excluded", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedDNSDomain("*.notlocal"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-ipv4-permitted", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "127.0.0.20", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-ipv4-excluded", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("128.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + ), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "127.0.0.1", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-ipv6-permitted", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + ), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "2001:0db8:85a3:0000:0000:8a2e:0370:7339", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-ipv6-excluded", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedIPRanges( + []*net.IPNet{ + { + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), + }, + }, + ), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "2009:0db8:85a3:0000:0000:8a2e:0370:7339", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-email-permitted", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedEmailAddress("@example.local"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "mail@example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-email-excluded", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedEmailAddress("@example.notlocal"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "mail@example.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-uri-permitted", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedURIDomain("*.example.com"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "https://www.example.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/subject-uri-excluded", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedURIDomain("*.smallstep.com"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "https://www.example.com", + }, + }, + want: true, + wantErr: false, + }, + // DIFFERENT SAN TYPE EXCLUDED SUCCESS TESTS + { + name: "ok/dns-excluded-with-ip-name", // when only DNS is exluded, we allow anything else + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.com"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -722,195 +1838,145 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/subject-empty", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, + name: "ok/ip-excluded-with-dns", // when only IP is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), }, cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "", - }, - DNSNames: []string{"example.local"}, + DNSNames: []string{"test.local"}, }, want: true, wantErr: false, }, { - name: "ok/subject-dns-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, + name: "ok/ip-excluded-with-mail", // when only IP is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), }, cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.local", - }, + EmailAddresses: []string{"mail@example.com"}, }, want: true, wantErr: false, }, { - name: "ok/subject-dns-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedDNSDomains: []string{".notlocal"}, + name: "ok/ip-excluded-with-mail", // when only IP is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), }, cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "example.local", - }, - }, - want: true, - wantErr: false, - }, - { - name: "ok/subject-ipv4-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedIPRanges: []*net.IPNet{ + URIs: []*url.URL{ { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), + Scheme: "https", + Host: "www.example.com", }, }, }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-dns", // when only mail is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedEmailAddress("@example.com"), + }, cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "127.0.0.20", + DNSNames: []string{"test.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-ip", // when only mail is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/mail-excluded-with-uri", // when only mail is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + }, }, }, want: true, wantErr: false, }, { - name: "ok/subject-ipv4-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("128.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, - }, + name: "ok/uri-excluded-with-dns", // when only URI is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedURIDomain("*.example.local"), + }, + cert: &x509.Certificate{ + DNSNames: []string{"test.example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-excluded-with-dns", // when only URI is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedURIDomain("*.example.local"), + }, + cert: &x509.Certificate{ + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-excluded-with-mail", // when only URI is exluded, we allow anything else + options: []NamePolicyOption{ + WithExcludedURIDomain("*.example.local"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-excluded-with-subject-ip-name", // when only DNS is exluded, we allow anything else + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ CommonName: "127.0.0.1", }, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, }, want: true, wantErr: false, }, - { - name: "ok/subject-ipv6-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, - }, - }, - cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "2001:0db8:85a3:0000:0000:8a2e:0370:7339", - }, - }, - want: true, - wantErr: false, - }, - { - name: "ok/subject-ipv6-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedIPRanges: []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, - }, - }, - cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "2009:0db8:85a3:0000:0000:8a2e:0370:7339", - }, - }, - want: true, - wantErr: false, - }, - { - name: "ok/subject-email-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedEmailAddresses: []string{"example.local"}, - }, - cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "mail@example.local", - }, - }, - want: true, - wantErr: false, - }, - { - name: "ok/subject-email-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedEmailAddresses: []string{"example.notlocal"}, - }, - cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "mail@example.local", - }, - }, - want: true, - wantErr: false, - }, - { - name: "ok/subject-uri-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedURIDomains: []string{".example.com"}, - }, - cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "https://www.example.com", - }, - }, - want: true, - wantErr: false, - }, - { - name: "ok/subject-uri-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedURIDomains: []string{".google.com"}, - }, - cert: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "https://www.example.com", - }, - }, - want: true, - wantErr: false, - }, + // COMBINED SUCCESS TESTS { name: "ok/combined-simple-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, - permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - permittedEmailAddresses: []string{"example.local"}, - permittedURIDomains: []string{".example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedURIDomain("*.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ CommonName: "somehost.local", }, DNSNames: []string{"example.local"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.15")}, EmailAddresses: []string{"mail@example.local"}, URIs: []*url.URL{ { @@ -924,12 +1990,11 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/combined-simple-permitted-without-subject-verification", - fields: fields{ - verifySubjectCommonName: false, - permittedDNSDomains: []string{".local"}, - permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - permittedEmailAddresses: []string{"example.local"}, - permittedURIDomains: []string{".example.local"}, + options: []NamePolicyOption{ + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedURIDomain("*.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -950,16 +2015,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/combined-simple-all", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, - permittedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.1"), Mask: net.IPv4Mask(255, 255, 255, 0)}}, - permittedEmailAddresses: []string{"example.local"}, - permittedURIDomains: []string{".example.local"}, - excludedDNSDomains: []string{"badhost.local"}, - excludedIPRanges: []*net.IPNet{{IP: net.ParseIP("127.0.0.128"), Mask: net.IPv4Mask(255, 255, 255, 128)}}, - excludedEmailAddresses: []string{"badmail@example.local"}, - excludedURIDomains: []string{"https://badwww.example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedURIDomain("*.example.local"), + WithExcludedDNSDomain("badhost.local"), + WithExcludedCIDR("127.0.0.128/25"), + WithExcludedEmailAddress("badmail@example.local"), + WithExcludedURIDomain("badwww.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -978,30 +2043,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: true, wantErr: false, }, - // TODO: more complex uses cases that combine multiple names and permitted/excluded entries - // TODO: check errors (reasons) are as expected } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - g := &NamePolicyEngine{ - verifySubjectCommonName: tt.fields.verifySubjectCommonName, - permittedDNSDomains: tt.fields.permittedDNSDomains, - excludedDNSDomains: tt.fields.excludedDNSDomains, - permittedIPRanges: tt.fields.permittedIPRanges, - excludedIPRanges: tt.fields.excludedIPRanges, - permittedEmailAddresses: tt.fields.permittedEmailAddresses, - excludedEmailAddresses: tt.fields.excludedEmailAddresses, - permittedURIDomains: tt.fields.permittedURIDomains, - excludedURIDomains: tt.fields.excludedURIDomains, - } - got, err := g.AreCertificateNamesAllowed(tt.cert) + engine, err := New(tt.options...) + assert.FatalError(t, err) + got, err := engine.AreCertificateNamesAllowed(tt.cert) // TODO: perform tests on CSR, sans, etc. too if (err != nil) != tt.wantErr { t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) return } - if err != nil { - assert.NotEquals(t, "", err.Error()) // TODO(hs): make this a complete equality check - } if got != tt.want { t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() = %v, want %v", got, tt.want) } From 1e808b61e5cf50e2a117e427d847fec1922e7529 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 17 Jan 2022 23:36:13 +0100 Subject: [PATCH 05/78] Merge logic for X509 and SSH policy --- authority/provisioner/policy.go | 45 +- authority/provisioner/provisioner.go | 7 +- authority/provisioner/sign_options.go | 6 +- authority/provisioner/sign_ssh_options.go | 6 +- policy/{x509/x509.go => engine.go} | 86 +++- policy/{x509/x509_test.go => engine_test.go} | 2 +- policy/{x509 => }/options.go | 22 +- policy/{x509 => }/options_test.go | 2 +- policy/ssh.go | 9 + policy/ssh/options.go | 99 ---- policy/ssh/ssh.go | 472 ------------------- policy/ssh/ssh_test.go | 261 ---------- policy/x509.go | 14 + 13 files changed, 153 insertions(+), 878 deletions(-) rename policy/{x509/x509.go => engine.go} (86%) rename policy/{x509/x509_test.go => engine_test.go} (99%) rename policy/{x509 => }/options.go (97%) rename policy/{x509 => }/options_test.go (99%) create mode 100644 policy/ssh.go delete mode 100644 policy/ssh/options.go delete mode 100644 policy/ssh/ssh.go delete mode 100644 policy/ssh/ssh_test.go create mode 100644 policy/x509.go diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go index 1adfd115..5fe31935 100644 --- a/authority/provisioner/policy.go +++ b/authority/provisioner/policy.go @@ -1,70 +1,69 @@ package provisioner import ( - sshpolicy "github.com/smallstep/certificates/policy/ssh" - x509policy "github.com/smallstep/certificates/policy/x509" + "github.com/smallstep/certificates/policy" ) // newX509PolicyEngine creates a new x509 name policy engine -func newX509PolicyEngine(x509Opts *X509Options) (*x509policy.NamePolicyEngine, error) { +func newX509PolicyEngine(x509Opts *X509Options) (policy.X509NamePolicyEngine, error) { if x509Opts == nil { return nil, nil } - options := []x509policy.NamePolicyOption{ - x509policy.WithSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default + options := []policy.NamePolicyOption{ + policy.WithSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default } allowed := x509Opts.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { options = append(options, - x509policy.WithPermittedDNSDomains(allowed.DNSDomains), - x509policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges - x509policy.WithPermittedEmailAddresses(allowed.EmailAddresses), - x509policy.WithPermittedURIDomains(allowed.URIDomains), + policy.WithPermittedDNSDomains(allowed.DNSDomains), + policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges + policy.WithPermittedEmailAddresses(allowed.EmailAddresses), + policy.WithPermittedURIDomains(allowed.URIDomains), ) } denied := x509Opts.GetDeniedNameOptions() if denied != nil && denied.HasNames() { options = append(options, - x509policy.WithExcludedDNSDomains(denied.DNSDomains), - x509policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges - x509policy.WithExcludedEmailAddresses(denied.EmailAddresses), - x509policy.WithExcludedURIDomains(denied.URIDomains), + policy.WithExcludedDNSDomains(denied.DNSDomains), + policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges + policy.WithExcludedEmailAddresses(denied.EmailAddresses), + policy.WithExcludedURIDomains(denied.URIDomains), ) } - return x509policy.New(options...) + return policy.New(options...) } // newSSHPolicyEngine creates a new SSH name policy engine -func newSSHPolicyEngine(sshOpts *SSHOptions) (*sshpolicy.NamePolicyEngine, error) { +func newSSHPolicyEngine(sshOpts *SSHOptions) (policy.SSHNamePolicyEngine, error) { if sshOpts == nil { return nil, nil } - options := []sshpolicy.NamePolicyOption{} + options := []policy.NamePolicyOption{} allowed := sshOpts.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { options = append(options, - sshpolicy.WithPermittedDNSDomains(allowed.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. - sshpolicy.WithPermittedEmailAddresses(allowed.EmailAddresses), - sshpolicy.WithPermittedPrincipals(allowed.Principals), + policy.WithPermittedDNSDomains(allowed.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. + policy.WithPermittedEmailAddresses(allowed.EmailAddresses), + policy.WithPermittedPrincipals(allowed.Principals), ) } denied := sshOpts.GetDeniedNameOptions() if denied != nil && denied.HasNames() { options = append(options, - sshpolicy.WithExcludedDNSDomains(denied.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. - sshpolicy.WithExcludedEmailAddresses(denied.EmailAddresses), - sshpolicy.WithExcludedPrincipals(denied.Principals), + policy.WithExcludedDNSDomains(denied.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. + policy.WithExcludedEmailAddresses(denied.EmailAddresses), + policy.WithExcludedPrincipals(denied.Principals), ) } - return sshpolicy.New(options...) + return policy.New(options...) } diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 2b98f0cf..a7d6e01d 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -12,8 +12,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" - sshpolicy "github.com/smallstep/certificates/policy/ssh" - x509policy "github.com/smallstep/certificates/policy/x509" + "github.com/smallstep/certificates/policy" "golang.org/x/crypto/ssh" ) @@ -307,8 +306,8 @@ func SanitizeSSHUserPrincipal(email string) string { } type base struct { - x509PolicyEngine *x509policy.NamePolicyEngine - sshPolicyEngine *sshpolicy.NamePolicyEngine + x509PolicyEngine policy.X509NamePolicyEngine + sshPolicyEngine policy.SSHNamePolicyEngine } // AuthorizeSign returns an unimplemented error. Provisioners should overwrite diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index ccc55435..a0e27f6d 100755 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -16,7 +16,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - x509policy "github.com/smallstep/certificates/policy/x509" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" ) @@ -408,11 +408,11 @@ func (v *validityValidator) Valid(cert *x509.Certificate, o SignOptions) error { // x509NamePolicyValidator validates that the certificate (to be signed) // contains only allowed SANs. type x509NamePolicyValidator struct { - policyEngine *x509policy.NamePolicyEngine + policyEngine policy.X509NamePolicyEngine } // newX509NamePolicyValidator return a new SANs allow/deny validator. -func newX509NamePolicyValidator(engine *x509policy.NamePolicyEngine) *x509NamePolicyValidator { +func newX509NamePolicyValidator(engine policy.X509NamePolicyEngine) *x509NamePolicyValidator { return &x509NamePolicyValidator{ policyEngine: engine, } diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index e5bd2121..e52d3aa7 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - sshpolicy "github.com/smallstep/certificates/policy/ssh" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/keyutil" "golang.org/x/crypto/ssh" ) @@ -448,11 +448,11 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SignSSHOpti // sshNamePolicyValidator validates that the certificate (to be signed) // contains only allowed principals. type sshNamePolicyValidator struct { - policyEngine *sshpolicy.NamePolicyEngine + policyEngine policy.SSHNamePolicyEngine } // newSSHNamePolicyValidator return a new SSH allow/deny validator. -func newSSHNamePolicyValidator(engine *sshpolicy.NamePolicyEngine) *sshNamePolicyValidator { +func newSSHNamePolicyValidator(engine policy.SSHNamePolicyEngine) *sshNamePolicyValidator { return &sshNamePolicyValidator{ policyEngine: engine, } diff --git a/policy/x509/x509.go b/policy/engine.go similarity index 86% rename from policy/x509/x509.go rename to policy/engine.go index 5a8337b9..10da6bc9 100755 --- a/policy/x509/x509.go +++ b/policy/engine.go @@ -1,4 +1,4 @@ -package x509policy +package policy import ( "bytes" @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "go.step.sm/crypto/x509util" + "golang.org/x/crypto/ssh" ) type CertificateInvalidError struct { @@ -47,7 +48,7 @@ func (e CertificateInvalidError) Error() string { // NamePolicyEngine can be used to check that a CSR or Certificate meets all allowed and // denied names before a CA creates and/or signs the Certificate. -// TODO(hs): the x509 RFC also defines name checks on directory name; support that? +// TODO(hs): the X509 RFC also defines name checks on directory name; support that? // TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine? type NamePolicyEngine struct { @@ -65,12 +66,15 @@ type NamePolicyEngine struct { excludedEmailAddresses []string permittedURIDomains []string excludedURIDomains []string + permittedPrincipals []string + excludedPrincipals []string // some internal counts for housekeeping numberOfDNSDomainConstraints int numberOfIPRangeConstraints int numberOfEmailAddressConstraints int numberOfURIDomainConstraints int + numberOfPrincipalConstraints int totalNumberOfPermittedConstraints int totalNumberOfExcludedConstraints int totalNumberOfConstraints int @@ -90,22 +94,25 @@ func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { e.permittedIPRanges = removeDuplicateIPRanges(e.permittedIPRanges) e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses) e.permittedURIDomains = removeDuplicates(e.permittedURIDomains) + e.permittedPrincipals = removeDuplicates(e.permittedPrincipals) e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains) e.excludedIPRanges = removeDuplicateIPRanges(e.excludedIPRanges) e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses) e.excludedURIDomains = removeDuplicates(e.excludedURIDomains) + e.excludedPrincipals = removeDuplicates(e.excludedPrincipals) e.numberOfDNSDomainConstraints = len(e.permittedDNSDomains) + len(e.excludedDNSDomains) e.numberOfIPRangeConstraints = len(e.permittedIPRanges) + len(e.excludedIPRanges) e.numberOfEmailAddressConstraints = len(e.permittedEmailAddresses) + len(e.excludedEmailAddresses) e.numberOfURIDomainConstraints = len(e.permittedURIDomains) + len(e.excludedURIDomains) + e.numberOfPrincipalConstraints = len(e.permittedPrincipals) + len(e.excludedPrincipals) e.totalNumberOfPermittedConstraints = len(e.permittedDNSDomains) + len(e.permittedIPRanges) + - len(e.permittedEmailAddresses) + len(e.permittedURIDomains) + len(e.permittedEmailAddresses) + len(e.permittedURIDomains) + len(e.permittedPrincipals) e.totalNumberOfExcludedConstraints = len(e.excludedDNSDomains) + len(e.excludedIPRanges) + - len(e.excludedEmailAddresses) + len(e.excludedURIDomains) + len(e.excludedEmailAddresses) + len(e.excludedURIDomains) + len(e.excludedPrincipals) e.totalNumberOfConstraints = e.totalNumberOfPermittedConstraints + e.totalNumberOfExcludedConstraints @@ -151,7 +158,7 @@ func (e *NamePolicyEngine) AreCertificateNamesAllowed(cert *x509.Certificate) (b if e.verifySubjectCommonName { appendSubjectCommonName(cert.Subject, &dnsNames, &ips, &emails, &uris) } - if err := e.validateNames(dnsNames, ips, emails, uris); err != nil { + if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil { return false, err } return true, nil @@ -165,7 +172,7 @@ func (e *NamePolicyEngine) AreCSRNamesAllowed(csr *x509.CertificateRequest) (boo if e.verifySubjectCommonName { appendSubjectCommonName(csr.Subject, &dnsNames, &ips, &emails, &uris) } - if err := e.validateNames(dnsNames, ips, emails, uris); err != nil { + if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil { return false, err } return true, nil @@ -175,7 +182,7 @@ func (e *NamePolicyEngine) AreCSRNamesAllowed(csr *x509.CertificateRequest) (boo // The SANs are first split into DNS names, IPs, email addresses and URIs. func (e *NamePolicyEngine) AreSANsAllowed(sans []string) (bool, error) { dnsNames, ips, emails, uris := x509util.SplitSANs(sans) - if err := e.validateNames(dnsNames, ips, emails, uris); err != nil { + if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil { return false, err } return true, nil @@ -183,7 +190,7 @@ func (e *NamePolicyEngine) AreSANsAllowed(sans []string) (bool, error) { // IsDNSAllowed verifies a single DNS domain is allowed. func (e *NamePolicyEngine) IsDNSAllowed(dns string) (bool, error) { - if err := e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}); err != nil { + if err := e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}, []string{}); err != nil { return false, err } return true, nil @@ -191,7 +198,16 @@ func (e *NamePolicyEngine) IsDNSAllowed(dns string) (bool, error) { // IsIPAllowed verifies a single IP domain is allowed. func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) { - if err := e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}); err != nil { + if err := e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}, []string{}); err != nil { + return false, err + } + return true, nil +} + +// ArePrincipalsAllowed verifies that all principals in an SSH certificate are allowed. +func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) { + dnsNames, emails, usernames := splitPrincipals(cert.ValidPrincipals) + if err := e.validateNames(dnsNames, []net.IP{}, emails, []*url.URL{}, usernames); err != nil { return false, err } return true, nil @@ -217,10 +233,27 @@ func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.I } } +// splitPrincipals splits SSH certificate principals into DNS names, emails and user names. +func splitPrincipals(principals []string) (dnsNames, emails, usernames []string) { + dnsNames = []string{} + emails = []string{} + usernames = []string{} + for _, principal := range principals { + if strings.Contains(principal, "@") { + emails = append(emails, principal) + } else if len(strings.Split(principal, ".")) > 1 { + dnsNames = append(dnsNames, principal) + } else { + usernames = append(usernames, principal) + } + } + return +} + // 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) error { +func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailAddresses []string, uris []*url.URL, usernames []string) error { // nothing to compare against; return early if e.totalNumberOfConstraints == 0 { @@ -309,6 +342,34 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } + //"dns": ["*.smallstep.com"], + //"email": ["@smallstep.com", "@google.com"], + //"principal": ["max", "mariano", "mike"] + /* No regexes for now. But if we ever implement them, they'd probably look like this */ + /*"principal": ["foo.smallstep.com", "/^*\.smallstep\.com$/"]*/ + + // Principals can be single user names (mariano, max, mike, ...), hostnames/domains (*.smallstep.com, host.smallstep.com, ...) and "emails" (max@smallstep.com, @smallstep.com, ...) + // All ValidPrincipals can thus be any one of those, and they can be mixed (mike@smallstep.com, mike, ...); we need to split this? + // Should we assume a generic engine, or can we do it host vs. user based? If host vs. user based, then it becomes easier w.r.t. dns; hosts will only be DNS, right? + // If we assume generic, we _may_ have a harder time distinguishing host vs. user certs. We propose to use host + user specific provisioners, though... + // Perhaps we can do some heuristics on the principal names vs. hostnames (i.e. when only a single label and no dot, then it's a user principal) + + for _, username := range usernames { + if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return CertificateInvalidError{ + Reason: x509.CANotAuthorizedForThisName, + Detail: fmt.Sprintf("username principal %q is not permitted by any constraint", username), + } + } + // TODO: some validation? I.e. allowed characters? + if err := checkNameConstraints("username", username, username, + func(parsedName, constraint interface{}) (bool, error) { + return matchUsernameConstraint(parsedName.(string), constraint.(string)) + }, e.permittedPrincipals, e.excludedPrincipals); err != nil { + return err + } + } + // TODO: when the error is not nil and returned up in the above, we can add // additional context to it (i.e. the cert or csr that was inspected). @@ -753,3 +814,8 @@ func (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) ( return e.matchDomainConstraint(host, constraint) } + +// matchUsernameConstraint performs a string literal match against a constraint. +func matchUsernameConstraint(username, constraint string) (bool, error) { + return strings.EqualFold(username, constraint), nil +} diff --git a/policy/x509/x509_test.go b/policy/engine_test.go similarity index 99% rename from policy/x509/x509_test.go rename to policy/engine_test.go index a2977214..2b3484ab 100755 --- a/policy/x509/x509_test.go +++ b/policy/engine_test.go @@ -1,4 +1,4 @@ -package x509policy +package policy import ( "crypto/x509" diff --git a/policy/x509/options.go b/policy/options.go similarity index 97% rename from policy/x509/options.go rename to policy/options.go index ecd793a7..f628a083 100755 --- a/policy/x509/options.go +++ b/policy/options.go @@ -1,4 +1,4 @@ -package x509policy +package policy import ( "fmt" @@ -538,6 +538,26 @@ func AddExcludedURIDomain(uriDomain string) NamePolicyOption { } } +func WithPermittedPrincipals(principals []string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + // for _, principal := range principals { + // // TODO: validation? + // } + g.permittedPrincipals = principals + return nil + } +} + +func WithExcludedPrincipals(principals []string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + // for _, principal := range principals { + // // TODO: validation? + // } + g.excludedPrincipals = principals + return nil + } +} + func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) { normalizedConstraint := strings.TrimSpace(constraint) if strings.Contains(normalizedConstraint, "..") { diff --git a/policy/x509/options_test.go b/policy/options_test.go similarity index 99% rename from policy/x509/options_test.go rename to policy/options_test.go index 304e208f..5e84d20e 100644 --- a/policy/x509/options_test.go +++ b/policy/options_test.go @@ -1,4 +1,4 @@ -package x509policy +package policy import ( "net" diff --git a/policy/ssh.go b/policy/ssh.go new file mode 100644 index 00000000..0b4290d2 --- /dev/null +++ b/policy/ssh.go @@ -0,0 +1,9 @@ +package policy + +import ( + "golang.org/x/crypto/ssh" +) + +type SSHNamePolicyEngine interface { + ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) +} diff --git a/policy/ssh/options.go b/policy/ssh/options.go deleted file mode 100644 index 30b68a1d..00000000 --- a/policy/ssh/options.go +++ /dev/null @@ -1,99 +0,0 @@ -package sshpolicy - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" -) - -type NamePolicyOption func(g *NamePolicyEngine) error - -func WithPermittedDNSDomains(domains []string) NamePolicyOption { - return func(g *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) - } - } - g.permittedDNSDomains = domains - return nil - } -} - -func WithExcludedDNSDomains(domains []string) NamePolicyOption { - return func(g *NamePolicyEngine) error { - for _, domain := range domains { - if err := validateDNSDomainConstraint(domain); err != nil { - return errors.Errorf("cannot parse excluded domain constraint %q", domain) - } - } - g.excludedDNSDomains = domains - return nil - } -} - -func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { - return func(g *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { - return err - } - } - g.permittedEmailAddresses = emailAddresses - return nil - } -} - -func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { - return func(g *NamePolicyEngine) error { - for _, email := range emailAddresses { - if err := validateEmailConstraint(email); err != nil { - return err - } - } - g.excludedEmailAddresses = emailAddresses - return nil - } -} - -func WithPermittedPrincipals(principals []string) NamePolicyOption { - return func(g *NamePolicyEngine) error { - // for _, principal := range principals { - // // TODO: validation? - // } - g.permittedPrincipals = principals - return nil - } -} - -func WithExcludedPrincipals(principals []string) NamePolicyOption { - return func(g *NamePolicyEngine) error { - // for _, principal := range principals { - // // TODO: validation? - // } - g.excludedPrincipals = principals - return nil - } -} - -func validateDNSDomainConstraint(domain string) error { - if _, ok := domainToReverseLabels(domain); !ok { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) - } - return nil -} - -func validateEmailConstraint(constraint string) error { - if strings.Contains(constraint, "@") { - _, ok := parseRFC2821Mailbox(constraint) - if !ok { - return fmt.Errorf("cannot parse email constraint %q", constraint) - } - } - _, ok := domainToReverseLabels(constraint) - if !ok { - return fmt.Errorf("cannot parse email domain constraint %q", constraint) - } - return nil -} diff --git a/policy/ssh/ssh.go b/policy/ssh/ssh.go deleted file mode 100644 index dcf5394f..00000000 --- a/policy/ssh/ssh.go +++ /dev/null @@ -1,472 +0,0 @@ -package sshpolicy - -import ( - "bytes" - "crypto/x509" - "fmt" - "reflect" - "strings" - - "github.com/pkg/errors" - "golang.org/x/crypto/ssh" -) - -type CertificateInvalidError struct { - Reason x509.InvalidReason - Detail string -} - -func (e CertificateInvalidError) Error() string { - switch e.Reason { - // TODO: include logical errors for this package; exlude ones that don't make sense for its current use case? - // TODO: currently only CANotAuthorizedForThisName is used by this package; we're not checking the other things in CSRs in this package. - case x509.NotAuthorizedToSign: - return "not authorized to sign other certificates" // TODO: this one doesn't make sense for this pkg - case x509.Expired: - return "csr has expired or is not yet valid: " + e.Detail - case x509.CANotAuthorizedForThisName: - return "not authorized to sign for this name: " + e.Detail - case x509.CANotAuthorizedForExtKeyUsage: - return "not authorized for an extended key usage: " + e.Detail - case x509.TooManyIntermediates: - return "too many intermediates for path length constraint" - case x509.IncompatibleUsage: - return "csr specifies an incompatible key usage" - case x509.NameMismatch: - return "issuer name does not match subject from issuing certificate" - case x509.NameConstraintsWithoutSANs: - return "issuer has name constraints but csr doesn't have a SAN extension" - case x509.UnconstrainedName: - return "issuer has name constraints but csr contains unknown or unconstrained name: " + e.Detail - } - return "unknown error" -} - -type NamePolicyEngine struct { - options []NamePolicyOption - permittedDNSDomains []string - excludedDNSDomains []string - permittedEmailAddresses []string - excludedEmailAddresses []string - permittedPrincipals []string // TODO: rename to usernames, as principals can be host, user@ (like mail) and usernames? - excludedPrincipals []string -} - -func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { - - e := &NamePolicyEngine{} // TODO: embed an x509 engine instead of building it again? - e.options = append(e.options, opts...) - for _, option := range e.options { - if err := option(e); err != nil { - return nil, err - } - } - - return e, nil -} - -func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) { - dnsNames, emails, userNames := splitPrincipals(cert.ValidPrincipals) - if err := e.validateNames(dnsNames, emails, userNames); err != nil { - return false, err - } - return true, nil -} - -func (e *NamePolicyEngine) validateNames(dnsNames, emails, userNames []string) error { - //"dns": ["*.smallstep.com"], - //"email": ["@smallstep.com", "@google.com"], - //"principal": ["max", "mariano", "mike"] - /* No regexes for now. But if we ever implement them, they'd probably look like this */ - /*"principal": ["foo.smallstep.com", "/^*\.smallstep\.com$/"]*/ - - // Principals can be single user names (mariano, max, mike, ...), hostnames/domains (*.smallstep.com, host.smallstep.com, ...) and "emails" (max@smallstep.com, @smallstep.com, ...) - // All ValidPrincipals can thus be any one of those, and they can be mixed (mike@smallstep.com, mike, ...); we need to split this? - // Should we assume a generic engine, or can we do it host vs. user based? If host vs. user based, then it becomes easier w.r.t. dns; hosts will only be DNS, right? - // If we assume generic, we _may_ have a harder time distinguishing host vs. user certs. We propose to use host + user specific provisioners, though... - // Perhaps we can do some heuristics on the principal names vs. hostnames (i.e. when only a single label and no dot, then it's a user principal) - - for _, dns := range dnsNames { - if _, ok := domainToReverseLabels(dns); !ok { - return errors.Errorf("cannot parse dns %q", dns) - } - if err := checkNameConstraints("dns", dns, dns, - func(parsedName, constraint interface{}) (bool, error) { - return matchDomainConstraint(parsedName.(string), constraint.(string)) - }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { - return err - } - } - - for _, email := range emails { - mailbox, ok := parseRFC2821Mailbox(email) - if !ok { - return fmt.Errorf("cannot parse rfc822Name %q", mailbox) - } - if err := checkNameConstraints("email", email, mailbox, - func(parsedName, constraint interface{}) (bool, error) { - return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) - }, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil { - return err - } - } - - for _, userName := range userNames { - // TODO: some validation? I.e. allowed characters? - if err := checkNameConstraints("username", userName, userName, - func(parsedName, constraint interface{}) (bool, error) { - return matchUserNameConstraint(parsedName.(string), constraint.(string)) - }, e.permittedPrincipals, e.excludedPrincipals); err != nil { - return err - } - } - - return nil -} - -// splitPrincipals splits SSH certificate principals into DNS names, emails and user names. -func splitPrincipals(principals []string) (dnsNames, emails, userNames []string) { - dnsNames = []string{} - emails = []string{} - userNames = []string{} - for _, principal := range principals { - if strings.Contains(principal, "@") { - emails = append(emails, principal) - } else if len(strings.Split(principal, ".")) > 1 { - dnsNames = append(dnsNames, principal) - } else { - userNames = append(userNames, principal) - } - } - return -} - -// checkNameConstraints checks that c permits a child certificate to claim the -// given name, of type nameType. The argument parsedName contains the parsed -// form of name, suitable for passing to the match function. The total number -// of comparisons is tracked in the given count and should not exceed the given -// limit. -// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func checkNameConstraints( - nameType string, - name string, - parsedName interface{}, - match func(parsedName, constraint interface{}) (match bool, err error), - permitted, excluded interface{}) error { - - excludedValue := reflect.ValueOf(excluded) - - // *count += excludedValue.Len() - // if *count > maxConstraintComparisons { - // return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""} - // } - - // TODO: fix the errors; return our own, because we don't have cert ... - - for i := 0; i < excludedValue.Len(); i++ { - constraint := excludedValue.Index(i).Interface() - match, err := match(parsedName, constraint) - if err != nil { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: err.Error(), - } - } - - if match { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), - } - } - } - - permittedValue := reflect.ValueOf(permitted) - - // *count += permittedValue.Len() - // if *count > maxConstraintComparisons { - // return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""} - // } - - ok := true - for i := 0; i < permittedValue.Len(); i++ { - constraint := permittedValue.Index(i).Interface() - var err error - if ok, err = match(parsedName, constraint); err != nil { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: err.Error(), - } - } - - if ok { - break - } - } - - if !ok { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), - } - } - - return nil -} - -// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func 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. - if constraint == "" { - return true, nil - } - - domainLabels, ok := domainToReverseLabels(domain) - if !ok { - return false, fmt.Errorf("cannot parse domain %q", domain) - } - - // RFC 5280 says that a leading period in a domain name means that at - // least one label must be prepended, but only for URI and email - // constraints, not DNS constraints. The code also supports that - // behavior for DNS constraints. - - mustHaveSubdomains := false - if constraint[0] == '.' { - mustHaveSubdomains = true - constraint = constraint[1:] - } - - constraintLabels, ok := domainToReverseLabels(constraint) - if !ok { - return false, fmt.Errorf("cannot parse domain %q", constraint) - } - - if len(domainLabels) < len(constraintLabels) || - (mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) { - return false, nil - } - - for i, constraintLabel := range constraintLabels { - if !strings.EqualFold(constraintLabel, domainLabels[i]) { - return false, nil - } - } - - return true, nil -} - -// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { - // If the constraint contains an @, then it specifies an exact mailbox name. - if strings.Contains(constraint, "@") { - constraintMailbox, ok := parseRFC2821Mailbox(constraint) - if !ok { - return false, fmt.Errorf("cannot parse constraint %q", constraint) - } - return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil - } - - // Otherwise the constraint is like a DNS constraint of the domain part - // of the mailbox. - return matchDomainConstraint(mailbox.domain, constraint) -} - -// matchUserNameConstraint performs a string literal match against a constraint -func matchUserNameConstraint(userName, constraint string) (bool, error) { - return userName == constraint, nil -} - -// TODO: decrease code duplication: single policy engine again, with principals added, but not used in x509? -// Not sure how I'd like to model that in Go, though: use (embedded) structs? interfaces? An x509 name policy engine -// interface could expose the methods that are useful to x509; the SSH name policy engine interfaces could do the -// same for SSH ones. One interface for both (with no methods?); then two, so that not all name policy options -// can be executed on both types? The shared ones could then maybe use the one with no methods? But we need protect -// it from being applied to just any type, of course. Not sure if Go allows us to do something like that, though. -// Maybe some kind of dummy function helps there? - -// 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 { - reverseLabels = append(reverseLabels, domain) - domain = "" - } else { - reverseLabels = append(reverseLabels, domain[i+1:]) - domain = domain[:i] - } - } - - if len(reverseLabels) > 0 && reverseLabels[0] == "" { - // An empty label at the end indicates an absolute value. - return nil, false - } - - for _, label := range reverseLabels { - if label == "" { - // Empty labels are otherwise invalid. - return nil, false - } - - for _, c := range label { - if c < 33 || c > 126 { - // Invalid character. - return nil, false - } - } - } - - return reverseLabels, true -} - -// 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 -} - -// parseRFC2821Mailbox parses an email address into local and domain parts, -// 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 - } - - localPartBytes := make([]byte, 0, len(in)/2) - - if in[0] == '"' { - // Quoted-string = DQUOTE *qcontent DQUOTE - // non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127 - // qcontent = qtext / quoted-pair - // qtext = non-whitespace-control / - // %d33 / %d35-91 / %d93-126 - // quoted-pair = ("\" text) / obs-qp - // text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text - // - // (Names beginning with “obs-” are the obsolete syntax from RFC 2822, - // Section 4. Since it has been 16 years, we no longer accept that.) - in = in[1:] - QuotedString: - for { - if in == "" { - return mailbox, false - } - c := in[0] - in = in[1:] - - switch { - case c == '"': - break QuotedString - - case c == '\\': - // quoted-pair - if in == "" { - return mailbox, false - } - if in[0] == 11 || - in[0] == 12 || - (1 <= in[0] && in[0] <= 9) || - (14 <= in[0] && in[0] <= 127) { - localPartBytes = append(localPartBytes, in[0]) - in = in[1:] - } else { - return mailbox, false - } - - case c == 11 || - c == 12 || - // Space (char 32) is not allowed based on the - // BNF, but RFC 3696 gives an example that - // assumes that it is. Several “verified” - // errata continue to argue about this point. - // We choose to accept it. - c == 32 || - c == 33 || - c == 127 || - (1 <= c && c <= 8) || - (14 <= c && c <= 31) || - (35 <= c && c <= 91) || - (93 <= c && c <= 126): - // qtext - localPartBytes = append(localPartBytes, c) - - default: - return mailbox, false - } - } - } else { - // Atom ("." Atom)* - NextChar: - for len(in) > 0 { - // atext from RFC 2822, Section 3.2.4 - c := in[0] - - switch { - case c == '\\': - // Examples given in RFC 3696 suggest that - // escaped characters can appear outside of a - // quoted string. Several “verified” errata - // continue to argue the point. We choose to - // accept it. - in = in[1:] - if in == "" { - return mailbox, false - } - fallthrough - - case ('0' <= c && c <= '9') || - ('a' <= c && c <= 'z') || - ('A' <= c && c <= 'Z') || - c == '!' || c == '#' || c == '$' || c == '%' || - c == '&' || c == '\'' || c == '*' || c == '+' || - c == '-' || c == '/' || c == '=' || c == '?' || - c == '^' || c == '_' || c == '`' || c == '{' || - c == '|' || c == '}' || c == '~' || c == '.': - localPartBytes = append(localPartBytes, in[0]) - in = in[1:] - - default: - break NextChar - } - } - - if len(localPartBytes) == 0 { - return mailbox, false - } - - // From RFC 3696, Section 3: - // “period (".") may also appear, but may not be used to start - // or end the local part, nor may two or more consecutive - // periods appear.” - twoDots := []byte{'.', '.'} - if localPartBytes[0] == '.' || - localPartBytes[len(localPartBytes)-1] == '.' || - bytes.Contains(localPartBytes, twoDots) { - return mailbox, false - } - } - - if in == "" || in[0] != '@' { - return mailbox, false - } - in = in[1:] - - // The RFC species a format for domains, but that's known to be - // violated in practice so we accept that anything after an '@' is the - // domain part. - if _, ok := domainToReverseLabels(in); !ok { - return mailbox, false - } - - mailbox.local = string(localPartBytes) - mailbox.domain = in - return mailbox, true -} diff --git a/policy/ssh/ssh_test.go b/policy/ssh/ssh_test.go deleted file mode 100644 index e56ce592..00000000 --- a/policy/ssh/ssh_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package sshpolicy - -import ( - "testing" - - "golang.org/x/crypto/ssh" -) - -func TestNamePolicyEngine_ArePrincipalsAllowed(t *testing.T) { - type fields struct { - options []NamePolicyOption - permittedDNSDomains []string - excludedDNSDomains []string - permittedEmailAddresses []string - excludedEmailAddresses []string - permittedPrincipals []string - excludedPrincipals []string - } - tests := []struct { - name string - fields fields - cert *ssh.Certificate - want bool - wantErr bool - }{ - { - name: "fail/dns-permitted", - fields: fields{ - permittedDNSDomains: []string{".local"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"host.notlocal"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/dns-permitted", - fields: fields{ - excludedDNSDomains: []string{".local"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"host.local"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/mail-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"user@example.notlocal"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/mail-excluded", - fields: fields{ - excludedEmailAddresses: []string{"example.local"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"user@example.local"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/principal-permitted", - fields: fields{ - permittedPrincipals: []string{"user1"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"user2"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/principal-excluded", - fields: fields{ - excludedPrincipals: []string{"user"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"user"}, - }, - want: false, - wantErr: true, - }, - { - name: "fail/combined-complex-all-badhost.local", - fields: fields{ - permittedDNSDomains: []string{".local"}, - permittedEmailAddresses: []string{"example.local"}, - permittedPrincipals: []string{"user"}, - excludedDNSDomains: []string{"badhost.local"}, - excludedEmailAddresses: []string{"badmail@example.local"}, - excludedPrincipals: []string{"baduser"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{ - "user", - "user@example.local", - "badhost.local", - }, - }, - want: false, - wantErr: true, - }, - { - name: "ok/no-constraints", - fields: fields{}, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"host.example.com"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/dns-permitted", - fields: fields{ - permittedDNSDomains: []string{".local"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/dns-excluded", - fields: fields{ - excludedDNSDomains: []string{".notlocal"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/mail-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"user@example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/mail-excluded", - fields: fields{ - excludedEmailAddresses: []string{"example.notlocal"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"user@example.local"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/principal-permitted", - fields: fields{ - permittedPrincipals: []string{"user"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"user"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/principal-excluded", - fields: fields{ - excludedPrincipals: []string{"someone"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{"user"}, - }, - want: true, - wantErr: false, - }, - { - name: "ok/combined-simple-user-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, - permittedPrincipals: []string{"user"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{ - "user", - "user@example.local", - }, - }, - want: true, - wantErr: false, - }, - { - name: "ok/combined-simple-all-permitted", - fields: fields{ - permittedDNSDomains: []string{".local"}, - permittedEmailAddresses: []string{"example.local"}, - permittedPrincipals: []string{"user"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{ - "user", - "user@example.local", - "host.local", - }, - }, - want: true, - wantErr: false, - }, - { - name: "ok/combined-complex-all", - fields: fields{ - permittedDNSDomains: []string{".local"}, - permittedEmailAddresses: []string{"example.local"}, - permittedPrincipals: []string{"user"}, - excludedDNSDomains: []string{"badhost.local"}, - excludedEmailAddresses: []string{"badmail@example.local"}, - excludedPrincipals: []string{"baduser"}, - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{ - "user", - "user@example.local", - "host.local", - }, - }, - want: true, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := &NamePolicyEngine{ - options: tt.fields.options, - permittedDNSDomains: tt.fields.permittedDNSDomains, - excludedDNSDomains: tt.fields.excludedDNSDomains, - permittedEmailAddresses: tt.fields.permittedEmailAddresses, - excludedEmailAddresses: tt.fields.excludedEmailAddresses, - permittedPrincipals: tt.fields.permittedPrincipals, - excludedPrincipals: tt.fields.excludedPrincipals, - } - got, err := e.ArePrincipalsAllowed(tt.cert) - if (err != nil) != tt.wantErr { - t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/policy/x509.go b/policy/x509.go new file mode 100644 index 00000000..0bc35d89 --- /dev/null +++ b/policy/x509.go @@ -0,0 +1,14 @@ +package policy + +import ( + "crypto/x509" + "net" +) + +type X509NamePolicyEngine interface { + AreCertificateNamesAllowed(cert *x509.Certificate) (bool, error) + AreCSRNamesAllowed(csr *x509.CertificateRequest) (bool, error) + AreSANsAllowed(sans []string) (bool, error) + IsDNSAllowed(dns string) (bool, error) + IsIPAllowed(ip net.IP) (bool, error) +} From 6440870a8065e69679d9a40838ce9e369964816b Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 18 Jan 2022 14:39:21 +0100 Subject: [PATCH 06/78] Clean up, improve test cases and coverage --- acme/api/order.go | 0 authority/provisioner/acme.go | 0 authority/provisioner/jwk.go | 0 authority/provisioner/options.go | 4 +- authority/provisioner/policy.go | 4 +- authority/provisioner/sign_options.go | 0 policy/engine.go | 138 +++----- policy/engine_test.go | 484 +++++++++++++++++++++++++- policy/options.go | 90 +++-- policy/options_test.go | 126 +++++++ 10 files changed, 723 insertions(+), 123 deletions(-) mode change 100755 => 100644 acme/api/order.go mode change 100755 => 100644 authority/provisioner/acme.go mode change 100755 => 100644 authority/provisioner/jwk.go mode change 100755 => 100644 authority/provisioner/options.go mode change 100755 => 100644 authority/provisioner/sign_options.go diff --git a/acme/api/order.go b/acme/api/order.go old mode 100755 new mode 100644 diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go old mode 100755 new mode 100644 diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go old mode 100755 new mode 100644 diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go old mode 100755 new mode 100644 index 7c516f6d..55750d79 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -90,7 +90,7 @@ func (o *X509Options) GetDeniedNameOptions() *DeniedX509NameOptions { // AllowedX509NameOptions models the allowed names type AllowedX509NameOptions struct { DNSDomains []string `json:"dns,omitempty"` - IPRanges []string `json:"ip,omitempty"` // TODO(hs): support IPs as well as ranges + IPRanges []string `json:"ip,omitempty"` EmailAddresses []string `json:"email,omitempty"` URIDomains []string `json:"uri,omitempty"` } @@ -98,7 +98,7 @@ type AllowedX509NameOptions struct { // DeniedX509NameOptions models the denied names type DeniedX509NameOptions struct { DNSDomains []string `json:"dns,omitempty"` - IPRanges []string `json:"ip,omitempty"` // TODO(hs): support IPs as well as ranges + IPRanges []string `json:"ip,omitempty"` EmailAddresses []string `json:"email,omitempty"` URIDomains []string `json:"uri,omitempty"` } diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go index 5fe31935..2780d3c4 100644 --- a/authority/provisioner/policy.go +++ b/authority/provisioner/policy.go @@ -19,7 +19,7 @@ func newX509PolicyEngine(x509Opts *X509Options) (policy.X509NamePolicyEngine, er if allowed != nil && allowed.HasNames() { options = append(options, policy.WithPermittedDNSDomains(allowed.DNSDomains), - policy.WithPermittedCIDRs(allowed.IPRanges), // TODO(hs): support IPs in addition to ranges + policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), policy.WithPermittedEmailAddresses(allowed.EmailAddresses), policy.WithPermittedURIDomains(allowed.URIDomains), ) @@ -29,7 +29,7 @@ func newX509PolicyEngine(x509Opts *X509Options) (policy.X509NamePolicyEngine, er if denied != nil && denied.HasNames() { options = append(options, policy.WithExcludedDNSDomains(denied.DNSDomains), - policy.WithExcludedCIDRs(denied.IPRanges), // TODO(hs): support IPs in addition to ranges + policy.WithExcludedIPsOrCIDRs(denied.IPRanges), policy.WithExcludedEmailAddresses(denied.EmailAddresses), policy.WithExcludedURIDomains(denied.URIDomains), ) diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go old mode 100755 new mode 100644 diff --git a/policy/engine.go b/policy/engine.go index 10da6bc9..f850ecf3 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -15,33 +15,25 @@ import ( "golang.org/x/crypto/ssh" ) -type CertificateInvalidError struct { - Reason x509.InvalidReason +type NamePolicyReason int + +const ( + // NotAuthorizedForThisName results when an instance of + // NamePolicyEngine determines that there's a constraint which + // doesn't permit a DNS or another type of SAN to be signed + // (or otherwise used). + NotAuthorizedForThisName NamePolicyReason = iota +) + +type NamePolicyError struct { + Reason NamePolicyReason Detail string } -func (e CertificateInvalidError) Error() string { +func (e NamePolicyError) Error() string { switch e.Reason { - // TODO: include logical errors for this package; exlude ones that don't make sense for its current use case? - // TODO: currently only CANotAuthorizedForThisName is used by this package; we're not checking the other things in CSRs in this package. - case x509.NotAuthorizedToSign: - return "not authorized to sign other certificates" // TODO: this one doesn't make sense for this pkg - case x509.Expired: - return "csr has expired or is not yet valid: " + e.Detail - case x509.CANotAuthorizedForThisName: + case NotAuthorizedForThisName: return "not authorized to sign for this name: " + e.Detail - case x509.CANotAuthorizedForExtKeyUsage: - return "not authorized for an extended key usage: " + e.Detail - case x509.TooManyIntermediates: - return "too many intermediates for path length constraint" - case x509.IncompatibleUsage: - return "csr specifies an incompatible key usage" - case x509.NameMismatch: - return "issuer name does not match subject from issuing certificate" - case x509.NameConstraintsWithoutSANs: - return "issuer has name constraints but csr doesn't have a SAN extension" - case x509.UnconstrainedName: - return "issuer has name constraints but csr contains unknown or unconstrained name: " + e.Detail } return "unknown error" } @@ -126,7 +118,7 @@ func removeDuplicates(strSlice []string) []string { keys := make(map[string]bool) result := []string{} for _, item := range strSlice { - if _, value := keys[item]; !value { + if _, value := keys[item]; !value && item != "" { // skip empty constraints keys[item] = true result = append(result, item) } @@ -206,8 +198,8 @@ func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) { // ArePrincipalsAllowed verifies that all principals in an SSH certificate are allowed. func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) { - dnsNames, emails, usernames := splitPrincipals(cert.ValidPrincipals) - if err := e.validateNames(dnsNames, []net.IP{}, emails, []*url.URL{}, usernames); err != nil { + dnsNames, ips, emails, usernames := splitPrincipals(cert.ValidPrincipals) + if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, usernames); err != nil { return false, err } return true, nil @@ -233,14 +225,17 @@ func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.I } } -// splitPrincipals splits SSH certificate principals into DNS names, emails and user names. -func splitPrincipals(principals []string) (dnsNames, emails, usernames []string) { +// splitPrincipals splits SSH certificate principals into DNS names, emails and usernames. +func splitPrincipals(principals []string) (dnsNames []string, ips []net.IP, emails, usernames []string) { dnsNames = []string{} + ips = []net.IP{} emails = []string{} usernames = []string{} for _, principal := range principals { if strings.Contains(principal, "@") { emails = append(emails, principal) + } else if ip := net.ParseIP(principal); ip != nil { + ips = append(ips, ip) } else if len(strings.Split(principal, ".")) > 1 { dnsNames = append(dnsNames, principal) } else { @@ -260,7 +255,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA return nil } - // TODO: return our own type(s) of error? // 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 @@ -277,9 +271,9 @@ 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 // (other) excluded constraints, we'll allow a DNS (implicit allow; currently). if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: fmt.Sprintf("dns %q is not permitted by any constraint", dns), // TODO(hs): change this error (message) + return NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), } } if _, ok := domainToReverseLabels(dns); !ok { @@ -295,9 +289,9 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA for _, ip := range ips { if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: fmt.Sprintf("ip %q is not permitted by any constraint", ip.String()), + return NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()), } } if err := checkNameConstraints("ip", ip.String(), ip, @@ -310,9 +304,9 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA for _, email := range emailAddresses { if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: fmt.Sprintf("email %q is not permitted by any constraint", email), + return NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email), } } mailbox, ok := parseRFC2821Mailbox(email) @@ -329,9 +323,9 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA for _, uri := range uris { if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: fmt.Sprintf("uri %q is not permitted by any constraint", uri.String()), + return NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()), } } if err := checkNameConstraints("uri", uri.String(), uri, @@ -342,23 +336,11 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } - //"dns": ["*.smallstep.com"], - //"email": ["@smallstep.com", "@google.com"], - //"principal": ["max", "mariano", "mike"] - /* No regexes for now. But if we ever implement them, they'd probably look like this */ - /*"principal": ["foo.smallstep.com", "/^*\.smallstep\.com$/"]*/ - - // Principals can be single user names (mariano, max, mike, ...), hostnames/domains (*.smallstep.com, host.smallstep.com, ...) and "emails" (max@smallstep.com, @smallstep.com, ...) - // All ValidPrincipals can thus be any one of those, and they can be mixed (mike@smallstep.com, mike, ...); we need to split this? - // Should we assume a generic engine, or can we do it host vs. user based? If host vs. user based, then it becomes easier w.r.t. dns; hosts will only be DNS, right? - // If we assume generic, we _may_ have a harder time distinguishing host vs. user certs. We propose to use host + user specific provisioners, though... - // Perhaps we can do some heuristics on the principal names vs. hostnames (i.e. when only a single label and no dot, then it's a user principal) - for _, username := range usernames { if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, - Detail: fmt.Sprintf("username principal %q is not permitted by any constraint", username), + return NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("username principal %q is not explicity permitted by any constraint", username), } } // TODO: some validation? I.e. allowed characters? @@ -370,7 +352,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } - // TODO: when the error is not nil and returned up in the above, we can add + // TODO(hs): when the error is not nil and returned up in the above, we can add // additional context to it (i.e. the cert or csr that was inspected). // TODO(hs): validate other types of SANs? The Go std library skips those. @@ -382,8 +364,7 @@ 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. The total number of comparisons is tracked in the given -// count and should not exceed the given limit. +// 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, @@ -394,26 +375,19 @@ func checkNameConstraints( excludedValue := reflect.ValueOf(excluded) - // *count += excludedValue.Len() - // if *count > maxConstraintComparisons { - // return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""} - // } - - // TODO: fix the errors; return our own, because we don't have cert ... - for i := 0; i < excludedValue.Len(); i++ { constraint := excludedValue.Index(i).Interface() match, err := match(parsedName, constraint) if err != nil { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, + return NamePolicyError{ + Reason: NotAuthorizedForThisName, Detail: err.Error(), } } if match { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, + return NamePolicyError{ + Reason: NotAuthorizedForThisName, Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), } } @@ -421,18 +395,13 @@ func checkNameConstraints( permittedValue := reflect.ValueOf(permitted) - // *count += permittedValue.Len() - // if *count > maxConstraintComparisons { - // return x509.CertificateInvalidError{c, x509.TooManyConstraints, ""} - // } - ok := true for i := 0; i < permittedValue.Len(); i++ { constraint := permittedValue.Index(i).Interface() var err error if ok, err = match(parsedName, constraint); err != nil { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, + return NamePolicyError{ + Reason: NotAuthorizedForThisName, Detail: err.Error(), } } @@ -443,8 +412,8 @@ func checkNameConstraints( } if !ok { - return CertificateInvalidError{ - Reason: x509.CANotAuthorizedForThisName, + return NamePolicyError{ + Reason: NotAuthorizedForThisName, Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), } } @@ -651,7 +620,6 @@ func (e *NamePolicyEngine) matchDomainConstraint(domain, constraint string) (boo } // Block domains that start with just a period - // TODO(hs): check if we should allow domains starting with "." at all; not sure if this is allowed in x509 names and certs. if domain[0] == '.' { return false, nil } @@ -744,19 +712,11 @@ func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) { // } // } - // if isIPv4(ip) != isIPv4(constraint.IP) { // TODO(hs): this check seems to do what the above intended to do? - // return false, nil - // } - contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior; also check IPv4-in-IPv6 (again) return contained, nil } -func isIPv4(ip net.IP) bool { - return ip.To4() != nil -} - // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { // TODO(hs): handle literal wildcard case for emails? Does that even make sense? @@ -817,5 +777,9 @@ func (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) ( // matchUsernameConstraint performs a string literal match against a constraint. func matchUsernameConstraint(username, constraint string) (bool, error) { + // allow any plain principal username + if constraint == "*" { + return true, nil + } return strings.EqualFold(username, constraint), nil } diff --git a/policy/engine_test.go b/policy/engine_test.go index 2b3484ab..bea231ea 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -8,11 +8,11 @@ import ( "testing" "github.com/smallstep/assert" + "golang.org/x/crypto/ssh" ) // 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): check errors (reasons) are as expected func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { tests := []struct { @@ -135,6 +135,22 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { want: false, wantErr: false, }, + { + name: "false/idna-internationalized-domain-name", + engine: &NamePolicyEngine{}, + domain: "JP納豆.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + constraint: ".例.jp", + want: false, + wantErr: true, + }, + { + name: "false/idna-internationalized-domain-name-constraint", + engine: &NamePolicyEngine{}, + domain: "xn--jp-cd2fp15c.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + constraint: ".例.jp", + want: false, + wantErr: true, + }, { name: "ok/empty-constraint", engine: &NamePolicyEngine{}, @@ -169,6 +185,22 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/different-case", + engine: &NamePolicyEngine{}, + domain: "WWW.EXAMPLE.com", + constraint: "www.example.com", + want: true, + wantErr: false, + }, + { + name: "ok/idna-internationalized-domain-name-punycode", + engine: &NamePolicyEngine{}, + domain: "xn--jp-cd2fp15c.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + constraint: ".xn--fsq.jp", + want: true, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -413,6 +445,17 @@ func TestNamePolicyEngine_matchEmailConstraint(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/different-case", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "EXAMPLE.com", + }, + constraint: "example.com", // "wildcard" for 'example.com' + want: true, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -558,6 +601,17 @@ func TestNamePolicyEngine_matchURIConstraint(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/different-case", + engine: &NamePolicyEngine{}, + uri: &url.URL{ + Scheme: "https", + Host: "www.EXAMPLE.local", + }, + constraint: ".example.local", // using x509 period as the "wildcard"; expects a single subdomain + want: true, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -573,7 +627,23 @@ func TestNamePolicyEngine_matchURIConstraint(t *testing.T) { } } -func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { +func extractSANs(cert *x509.Certificate, includeSubject bool) []string { + sans := []string{} + sans = append(sans, cert.DNSNames...) + for _, ip := range cert.IPAddresses { + sans = append(sans, ip.String()) + } + sans = append(sans, cert.EmailAddresses...) + for _, uri := range cert.URIs { + sans = append(sans, uri.String()) + } + if includeSubject && cert.Subject.CommonName != "" { + sans = append(sans, cert.Subject.CommonName) + } + return sans +} + +func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { tests := []struct { name string options []NamePolicyOption @@ -2048,14 +2118,422 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { t.Run(tt.name, func(t *testing.T) { engine, err := New(tt.options...) assert.FatalError(t, err) - got, err := engine.AreCertificateNamesAllowed(tt.cert) // TODO: perform tests on CSR, sans, etc. too + got, err := engine.AreCertificateNamesAllowed(tt.cert) if (err != nil) != tt.wantErr { t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) return } + if err != nil { + assert.NotEquals(t, "", err.Error()) // TODO(hs): implement a more specific error comparison? + } if got != tt.want { t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() = %v, want %v", got, tt.want) } + + // Perform the same tests for a CSR, which are similar to Certificates + csr := &x509.CertificateRequest{ + Subject: tt.cert.Subject, + DNSNames: tt.cert.DNSNames, + EmailAddresses: tt.cert.EmailAddresses, + IPAddresses: tt.cert.IPAddresses, + URIs: tt.cert.URIs, + } + got, err = engine.AreCSRNamesAllowed(csr) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + assert.NotEquals(t, "", err.Error()) + } + if got != tt.want { + t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() = %v, want %v", got, tt.want) + } + + // Perform the same tests for a slice of SANs + includeSubject := engine.verifySubjectCommonName // copy behavior of the engine when Subject has to be included as a SAN + sans := extractSANs(tt.cert, includeSubject) + got, err = engine.AreSANsAllowed(sans) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.AreSANsAllowed() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + assert.NotEquals(t, "", err.Error()) + } + if got != tt.want { + t.Errorf("NamePolicyEngine.AreSANsAllowed() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { + tests := []struct { + name string + options []NamePolicyOption + cert *ssh.Certificate + want bool + wantErr bool + }{ + { + name: "fail/with-permitted-dns-domain", + options: []NamePolicyOption{ + WithPermittedDNSDomain("*.local"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "host.example.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/with-excluded-dns-domain", + options: []NamePolicyOption{ + WithExcludedDNSDomain("*.local"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "host.local", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/with-permitted-ip", + options: []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1/24"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "192.168.0.22", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/with-excluded-ip", + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "127.0.0.0", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/with-permitted-email", + options: []NamePolicyOption{ + WithPermittedEmailAddress("@example.com"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "mail@local", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/with-excluded-email", + options: []NamePolicyOption{ + WithExcludedEmailAddress("@example.com"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "mail@example.com", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/with-permitted-principals", + options: []NamePolicyOption{ + WithPermittedPrincipals([]string{"user"}), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "root", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/with-excluded-principals", + options: []NamePolicyOption{ + WithExcludedPrincipals([]string{"user"}), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/with-permitted-principal-as-mail", + options: []NamePolicyOption{ + WithPermittedPrincipals([]string{"ops"}), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "ops@work", // this is (currently) parsed as an email-like principal; not allowed with just "ops" as the permitted principal + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/principal-with-permitted-dns-domain", // when only DNS is permitted, username principals are not allowed. + options: []NamePolicyOption{ + WithPermittedDNSDomain("*.local"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/principal-with-permitted-ip-range", // when only IPs are permitted, username principals are not allowed. + options: []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1/24"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/principal-with-permitted-email", // when only emails are permitted, username principals are not allowed. + options: []NamePolicyOption{ + WithPermittedEmailAddress("@example.com"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/combined-user", + options: []NamePolicyOption{ + WithPermittedEmailAddress("@smallstep.com"), + WithExcludedEmailAddress("root@smallstep.com"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "someone@smallstep.com", + "someone", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/combined-user-with-excluded-user-principal", + options: []NamePolicyOption{ + WithPermittedEmailAddress("@smallstep.com"), + WithExcludedPrincipals([]string{"root"}), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "someone@smallstep.com", + "root", + }, + }, + want: false, + wantErr: true, + }, + { + name: "ok/with-permitted-dns-domain", + options: []NamePolicyOption{ + WithPermittedDNSDomain("*.local"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "host.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/with-excluded-dns-domain", + options: []NamePolicyOption{ + WithExcludedDNSDomain("*.example.com"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "host.local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/with-permitted-ip", + options: []NamePolicyOption{ + WithPermittedCIDR("127.0.0.1/24"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "127.0.0.33", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/with-excluded-ip", + options: []NamePolicyOption{ + WithExcludedCIDR("127.0.0.1/24"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "192.168.0.35", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/with-permitted-email", + options: []NamePolicyOption{ + WithPermittedEmailAddress("@example.com"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "mail@example.com", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/with-excluded-email", + options: []NamePolicyOption{ + WithExcludedEmailAddress("@example.com"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "mail@local", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/with-permitted-principals", + options: []NamePolicyOption{ + WithPermittedPrincipals([]string{"*"}), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "user", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/with-excluded-principals", + options: []NamePolicyOption{ + WithExcludedPrincipals([]string{"user"}), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "root", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-user", + options: []NamePolicyOption{ + WithPermittedEmailAddress("@smallstep.com"), + WithPermittedPrincipals([]string{"*"}), // without specifying the wildcard, "someone" would not be allowed. + WithExcludedEmailAddress("root@smallstep.com"), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "someone@smallstep.com", + "someone", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-user-with-excluded-user-principal", + options: []NamePolicyOption{ + WithPermittedEmailAddress("@smallstep.com"), + WithExcludedEmailAddress("root@smallstep.com"), + WithExcludedPrincipals([]string{"root"}), // unlike the previous test, this implicitly allows any other username principal + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "someone@smallstep.com", + "someone", + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/combined-simple-all", + options: []NamePolicyOption{ + WithPermittedDNSDomain("*.local"), + WithPermittedCIDR("127.0.0.1/24"), + WithPermittedEmailAddress("@example.local"), + WithPermittedPrincipals([]string{"user"}), + WithExcludedDNSDomain("badhost.local"), + WithExcludedCIDR("127.0.0.128/25"), + WithExcludedEmailAddress("badmail@example.local"), + WithExcludedPrincipals([]string{"root"}), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "example.local", + "127.0.0.1", + "user@example.local", + "user", + }, + }, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine, err := New(tt.options...) + assert.FatalError(t, err) + got, err := engine.ArePrincipalsAllowed(tt.cert) + if (err != nil) != tt.wantErr { + t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() = %v, want %v", got, tt.want) + } }) } } diff --git a/policy/options.go b/policy/options.go index f628a083..b1fbba70 100755 --- a/policy/options.go +++ b/policy/options.go @@ -204,6 +204,42 @@ func AddExcludedCIDRs(cidrs []string) NamePolicyOption { } } +func WithPermittedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + networks := make([]*net.IPNet, len(ipsOrCIDRs)) + for i, ipOrCIDR := range ipsOrCIDRs { + _, nw, err := net.ParseCIDR(ipOrCIDR) + if err == nil { + networks[i] = nw + } else if ip := net.ParseIP(ipOrCIDR); ip != nil { + networks[i] = networkFor(ip) + } else { + return errors.Errorf("cannot parse permitted constraint %q as IP nor CIDR", ipOrCIDR) + } + } + e.permittedIPRanges = networks + return nil + } +} + +func WithExcludedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { + return func(e *NamePolicyEngine) error { + networks := make([]*net.IPNet, len(ipsOrCIDRs)) + for i, ipOrCIDR := range ipsOrCIDRs { + _, nw, err := net.ParseCIDR(ipOrCIDR) + if err == nil { + networks[i] = nw + } else if ip := net.ParseIP(ipOrCIDR); ip != nil { + networks[i] = networkFor(ip) + } else { + return errors.Errorf("cannot parse excluded constraint %q as IP nor CIDR", ipOrCIDR) + } + } + e.excludedIPRanges = networks + return nil + } +} + func WithPermittedCIDR(cidr string) NamePolicyOption { return func(e *NamePolicyEngine) error { _, nw, err := net.ParseCIDR(cidr) @@ -228,16 +264,7 @@ func AddPermittedCIDR(cidr string) NamePolicyOption { func WithPermittedIP(ip net.IP) NamePolicyOption { return func(e *NamePolicyEngine) error { - var mask net.IPMask - if !isIPv4(ip) { - mask = net.CIDRMask(128, 128) - } else { - mask = net.CIDRMask(32, 32) - } - nw := &net.IPNet{ - IP: ip, - Mask: mask, - } + nw := networkFor(ip) e.permittedIPRanges = []*net.IPNet{nw} return nil } @@ -245,16 +272,7 @@ func WithPermittedIP(ip net.IP) NamePolicyOption { func AddPermittedIP(ip net.IP) NamePolicyOption { return func(e *NamePolicyEngine) error { - var mask net.IPMask - if !isIPv4(ip) { - mask = net.CIDRMask(128, 128) - } else { - mask = net.CIDRMask(32, 32) - } - nw := &net.IPNet{ - IP: ip, - Mask: mask, - } + nw := networkFor(ip) e.permittedIPRanges = append(e.permittedIPRanges, nw) return nil } @@ -540,9 +558,7 @@ func AddExcludedURIDomain(uriDomain string) NamePolicyOption { func WithPermittedPrincipals(principals []string) NamePolicyOption { return func(g *NamePolicyEngine) error { - // for _, principal := range principals { - // // TODO: validation? - // } + // TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do. g.permittedPrincipals = principals return nil } @@ -550,16 +566,32 @@ func WithPermittedPrincipals(principals []string) NamePolicyOption { func WithExcludedPrincipals(principals []string) NamePolicyOption { return func(g *NamePolicyEngine) error { - // for _, principal := range principals { - // // TODO: validation? - // } + // TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do. g.excludedPrincipals = principals return nil } } +func networkFor(ip net.IP) *net.IPNet { + var mask net.IPMask + if !isIPv4(ip) { + mask = net.CIDRMask(128, 128) + } else { + mask = net.CIDRMask(32, 32) + } + nw := &net.IPNet{ + IP: ip, + Mask: mask, + } + return nw +} + +func isIPv4(ip net.IP) bool { + return ip.To4() != nil +} + func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) { - normalizedConstraint := strings.TrimSpace(constraint) + normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) if strings.Contains(normalizedConstraint, "..") { return "", errors.Errorf("domain constraint %q cannot have empty labels", constraint) } @@ -576,7 +608,7 @@ func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) } func normalizeAndValidateEmailConstraint(constraint string) (string, error) { - normalizedConstraint := strings.TrimSpace(constraint) + normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) if strings.Contains(normalizedConstraint, "*") { return "", fmt.Errorf("email constraint %q cannot contain asterisk", constraint) } @@ -601,7 +633,7 @@ func normalizeAndValidateEmailConstraint(constraint string) (string, error) { } func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) { - normalizedConstraint := strings.TrimSpace(constraint) + normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) if strings.Contains(normalizedConstraint, "..") { return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint) } diff --git a/policy/options_test.go b/policy/options_test.go index 5e84d20e..7f417887 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -33,6 +33,18 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { want: "", wantErr: true, }, + { + name: "false/idna-internationalized-domain-name", + constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + want: "", + wantErr: true, + }, + { + name: "false/idna-internationalized-domain-name-constraint", + constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + want: "", + wantErr: true, + }, { name: "ok/wildcard", constraint: "*.local", @@ -45,6 +57,12 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { want: "example.local", wantErr: false, }, + { + name: "ok/idna-internationalized-domain-name-punycode", + constraint: ".xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + want: ".xn--fsq.jp", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -297,6 +315,42 @@ func TestNew(t *testing.T) { wantErr: true, } }, + "fail/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedIPsOrCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-permitted-ipsOrCIDRs-ip": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedIPsOrCIDRs([]string{"127.0.0:1"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedIPsOrCIDRs([]string{"127.0.0.1//24"}), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-ipsOrCIDRs-ip": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedIPsOrCIDRs([]string{"127.0.0:1"}), + }, + want: nil, + wantErr: true, + } + }, "fail/with-permitted-cidr": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -828,6 +882,48 @@ func TestNew(t *testing.T) { wantErr: false, } }, + "ok/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.31/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithPermittedIPsOrCIDRs([]string{"127.0.0.1/24", "192.168.0.31"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test { + _, nw1, err := net.ParseCIDR("127.0.0.1/24") + assert.FatalError(t, err) + _, nw2, err := net.ParseCIDR("192.168.0.31/32") + assert.FatalError(t, err) + options := []NamePolicyOption{ + WithExcludedIPsOrCIDRs([]string{"127.0.0.1/24", "192.168.0.31"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedIPRanges: []*net.IPNet{ + nw1, nw2, + }, + numberOfIPRangeConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, "ok/with-permitted-cidr": func(t *testing.T) test { _, nw1, err := net.ParseCIDR("127.0.0.1/24") assert.FatalError(t, err) @@ -1322,6 +1418,36 @@ func TestNew(t *testing.T) { wantErr: false, } }, + "ok/with-permitted-principals": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedPrincipals([]string{"root", "ops"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedPrincipals: []string{"root", "ops"}, + numberOfPrincipalConstraints: 2, + totalNumberOfPermittedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, + "ok/with-excluded-principals": func(t *testing.T) test { + options := []NamePolicyOption{ + WithExcludedPrincipals([]string{"root", "ops"}), + } + return test{ + options: options, + want: &NamePolicyEngine{ + excludedPrincipals: []string{"root", "ops"}, + numberOfPrincipalConstraints: 2, + totalNumberOfExcludedConstraints: 2, + totalNumberOfConstraints: 2, + }, + wantErr: false, + } + }, } for name, prep := range tests { tc := prep(t) From ff08b5055ed300a62c7b034aab483ec8b24f6a55 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 18 Jan 2022 14:42:56 +0100 Subject: [PATCH 07/78] Fix linting issues --- policy/engine.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/policy/engine.go b/policy/engine.go index f850ecf3..4c102fd6 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -31,8 +31,7 @@ type NamePolicyError struct { } func (e NamePolicyError) Error() string { - switch e.Reason { - case NotAuthorizedForThisName: + if e.Reason == NotAuthorizedForThisName { return "not authorized to sign for this name: " + e.Detail } return "unknown error" @@ -340,7 +339,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return NamePolicyError{ Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("username principal %q is not explicity permitted by any constraint", username), + Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", username), } } // TODO: some validation? I.e. allowed characters? From 066bf320866de37f509ee647466b5c9a6666c8ff Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 25 Jan 2022 14:59:55 +0100 Subject: [PATCH 08/78] Fix part of PR comments --- authority/provisioner/options.go | 31 +-- authority/provisioner/policy.go | 6 +- authority/provisioner/sign_options.go | 6 +- authority/provisioner/sign_ssh_options.go | 10 +- authority/provisioner/ssh_options.go | 32 +-- policy/engine.go | 54 ++-- policy/engine_test.go | 290 +++++++++++++++++++--- policy/options.go | 2 +- 8 files changed, 322 insertions(+), 109 deletions(-) diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 55750d79..257a2107 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -58,10 +58,10 @@ type X509Options struct { TemplateData json.RawMessage `json:"templateData,omitempty"` // AllowedNames contains the SANs the provisioner is authorized to sign - AllowedNames *AllowedX509NameOptions `json:"allow,omitempty"` + AllowedNames *X509NameOptions `json:"allow,omitempty"` // DeniedNames contains the SANs the provisioner is not authorized to sign - DeniedNames *DeniedX509NameOptions `json:"deny,omitempty"` + DeniedNames *X509NameOptions `json:"deny,omitempty"` } // HasTemplate returns true if a template is defined in the provisioner options. @@ -71,7 +71,7 @@ func (o *X509Options) HasTemplate() bool { // GetAllowedNameOptions returns the AllowedNameOptions, which models the // SANs that a provisioner is authorized to sign x509 certificates for. -func (o *X509Options) GetAllowedNameOptions() *AllowedX509NameOptions { +func (o *X509Options) GetAllowedNameOptions() *X509NameOptions { if o == nil { return nil } @@ -80,23 +80,15 @@ func (o *X509Options) GetAllowedNameOptions() *AllowedX509NameOptions { // GetDeniedNameOptions returns the DeniedNameOptions, which models the // SANs that a provisioner is NOT authorized to sign x509 certificates for. -func (o *X509Options) GetDeniedNameOptions() *DeniedX509NameOptions { +func (o *X509Options) GetDeniedNameOptions() *X509NameOptions { if o == nil { return nil } return o.DeniedNames } -// AllowedX509NameOptions models the allowed names -type AllowedX509NameOptions struct { - DNSDomains []string `json:"dns,omitempty"` - IPRanges []string `json:"ip,omitempty"` - EmailAddresses []string `json:"email,omitempty"` - URIDomains []string `json:"uri,omitempty"` -} - -// DeniedX509NameOptions models the denied names -type DeniedX509NameOptions struct { +// X509NameOptions models the X509 name policy configuration. +type X509NameOptions struct { DNSDomains []string `json:"dns,omitempty"` IPRanges []string `json:"ip,omitempty"` EmailAddresses []string `json:"email,omitempty"` @@ -105,16 +97,7 @@ type DeniedX509NameOptions struct { // HasNames checks if the AllowedNameOptions has one or more // names configured. -func (o *AllowedX509NameOptions) HasNames() bool { - return len(o.DNSDomains) > 0 || - len(o.IPRanges) > 0 || - len(o.EmailAddresses) > 0 || - len(o.URIDomains) > 0 -} - -// HasNames checks if the DeniedNameOptions has one or more -// names configured. -func (o *DeniedX509NameOptions) HasNames() bool { +func (o *X509NameOptions) HasNames() bool { return len(o.DNSDomains) > 0 || len(o.IPRanges) > 0 || len(o.EmailAddresses) > 0 || diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go index 2780d3c4..8a69e1e5 100644 --- a/authority/provisioner/policy.go +++ b/authority/provisioner/policy.go @@ -50,7 +50,8 @@ func newSSHPolicyEngine(sshOpts *SSHOptions) (policy.SSHNamePolicyEngine, error) allowed := sshOpts.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { options = append(options, - policy.WithPermittedDNSDomains(allowed.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. + policy.WithPermittedDNSDomains(allowed.DNSDomains), + policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), policy.WithPermittedEmailAddresses(allowed.EmailAddresses), policy.WithPermittedPrincipals(allowed.Principals), ) @@ -59,7 +60,8 @@ func newSSHPolicyEngine(sshOpts *SSHOptions) (policy.SSHNamePolicyEngine, error) denied := sshOpts.GetDeniedNameOptions() if denied != nil && denied.HasNames() { options = append(options, - policy.WithExcludedDNSDomains(denied.DNSDomains), // TODO(hs): be a bit more lenient w.r.t. the format of domains? I.e. allow "*.localhost" instead of the ".localhost", which is what Name Constraints do. + policy.WithExcludedDNSDomains(denied.DNSDomains), + policy.WithExcludedIPsOrCIDRs(denied.IPRanges), policy.WithExcludedEmailAddresses(denied.EmailAddresses), policy.WithExcludedPrincipals(denied.Principals), ) diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index a0e27f6d..7ca6cec4 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -424,11 +424,9 @@ func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) e if v.policyEngine == nil { return nil } + _, err := v.policyEngine.AreCertificateNamesAllowed(cert) - if err != nil { - return err - } - return nil + return err } var ( diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index e52d3aa7..e1853fa1 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -454,6 +454,7 @@ type sshNamePolicyValidator struct { // newSSHNamePolicyValidator return a new SSH allow/deny validator. func newSSHNamePolicyValidator(engine policy.SSHNamePolicyEngine) *sshNamePolicyValidator { return &sshNamePolicyValidator{ + // TODO: should we use two engines, one for host certs; another for user certs? policyEngine: engine, } } @@ -464,14 +465,9 @@ func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) if v.policyEngine == nil { return nil } - // TODO(hs): should this perform checks only for hosts vs. user certs depending on context? - // The current best practice is to have separate provisioners for hosts and users, and thus - // separate policy engines for the principals that are allowed. + _, err := v.policyEngine.ArePrincipalsAllowed(cert) - if err != nil { - return err - } - return nil + return err } // sshCertTypeUInt32 diff --git a/authority/provisioner/ssh_options.go b/authority/provisioner/ssh_options.go index ada26d7d..91ce7126 100644 --- a/authority/provisioner/ssh_options.go +++ b/authority/provisioner/ssh_options.go @@ -35,22 +35,16 @@ type SSHOptions struct { TemplateData json.RawMessage `json:"templateData,omitempty"` // AllowedNames contains the names the provisioner is authorized to sign - AllowedNames *AllowedSSHNameOptions `json:"allow,omitempty"` + AllowedNames *SSHNameOptions `json:"allow,omitempty"` // DeniedNames contains the names the provisioner is not authorized to sign - DeniedNames *DeniedSSHNameOptions `json:"deny,omitempty"` + DeniedNames *SSHNameOptions `json:"deny,omitempty"` } -// AllowedSSHNameOptions models the allowed names -type AllowedSSHNameOptions struct { - DNSDomains []string `json:"dns,omitempty"` - EmailAddresses []string `json:"email,omitempty"` - Principals []string `json:"principal,omitempty"` -} - -// DeniedSSHNameOptions models the denied names -type DeniedSSHNameOptions struct { +// SSHNameOptions models the SSH name policy configuration. +type SSHNameOptions struct { DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` EmailAddresses []string `json:"email,omitempty"` Principals []string `json:"principal,omitempty"` } @@ -62,7 +56,7 @@ func (o *SSHOptions) HasTemplate() bool { // GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the // names that a provisioner is authorized to sign SSH certificates for. -func (o *SSHOptions) GetAllowedNameOptions() *AllowedSSHNameOptions { +func (o *SSHOptions) GetAllowedNameOptions() *SSHNameOptions { if o == nil { return nil } @@ -71,24 +65,16 @@ func (o *SSHOptions) GetAllowedNameOptions() *AllowedSSHNameOptions { // GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the // names that a provisioner is NOT authorized to sign SSH certificates for. -func (o *SSHOptions) GetDeniedNameOptions() *DeniedSSHNameOptions { +func (o *SSHOptions) GetDeniedNameOptions() *SSHNameOptions { if o == nil { return nil } return o.DeniedNames } -// HasNames checks if the AllowedSSHNameOptions has one or more +// HasNames checks if the SSHNameOptions has one or more // names configured. -func (o *AllowedSSHNameOptions) HasNames() bool { - return len(o.DNSDomains) > 0 || - len(o.EmailAddresses) > 0 || - len(o.Principals) > 0 -} - -// HasNames checks if the DeniedSSHNameOptions has one or more -// names configured. -func (o *DeniedSSHNameOptions) HasNames() bool { +func (o *SSHNameOptions) HasNames() bool { return len(o.DNSDomains) > 0 || len(o.EmailAddresses) > 0 || len(o.Principals) > 0 diff --git a/policy/engine.go b/policy/engine.go index 4c102fd6..345d6282 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -197,7 +197,10 @@ func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) { // ArePrincipalsAllowed verifies that all principals in an SSH certificate are allowed. func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) { - dnsNames, ips, emails, usernames := splitPrincipals(cert.ValidPrincipals) + dnsNames, ips, emails, usernames, err := splitSSHPrincipals(cert) + if err != nil { + return false, err + } if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, usernames); err != nil { return false, err } @@ -213,34 +216,45 @@ func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.I if commonName == "" { return } - if ip := net.ParseIP(commonName); ip != nil { - *ips = append(*ips, ip) - } else if u, err := url.Parse(commonName); err == nil && u.Scheme != "" { - *uris = append(*uris, u) - } else if strings.Contains(commonName, "@") { - *emails = append(*emails, commonName) - } else { - *dnsNames = append(*dnsNames, commonName) - } + subjectDNSNames, subjectIPs, subjectEmails, subjectURIs := x509util.SplitSANs([]string{commonName}) + *dnsNames = append(*dnsNames, subjectDNSNames...) + *ips = append(*ips, subjectIPs...) + *emails = append(*emails, subjectEmails...) + *uris = append(*uris, subjectURIs...) } // splitPrincipals splits SSH certificate principals into DNS names, emails and usernames. -func splitPrincipals(principals []string) (dnsNames []string, ips []net.IP, emails, usernames []string) { +func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, usernames []string, err error) { dnsNames = []string{} ips = []net.IP{} emails = []string{} usernames = []string{} - for _, principal := range principals { - if strings.Contains(principal, "@") { - emails = append(emails, principal) - } else if ip := net.ParseIP(principal); ip != nil { - ips = append(ips, ip) - } else if len(strings.Split(principal, ".")) > 1 { - dnsNames = append(dnsNames, principal) - } else { - usernames = append(usernames, principal) + var uris []*url.URL + switch cert.CertType { + case ssh.HostCert: + dnsNames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) + switch { + case len(emails) > 0: + err = fmt.Errorf("Email(-like) principals %v not expected in SSH Host certificate ", emails) + case len(uris) > 0: + err = fmt.Errorf("URL principals %v not expected in SSH Host certificate ", uris) } + case ssh.UserCert: + // re-using SplitSANs results in anything that can't be parsed as an IP, URI or email + // to be considered a username. This allows usernames like h.slatman to be present + // in the SSH certificate. We're exluding IPs and URIs, because they can be confusing + // when used in a SSH user certificate. + usernames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) + switch { + case len(ips) > 0: + err = fmt.Errorf("IP principals %v not expected in SSH User certificate ", ips) + case len(uris) > 0: + err = fmt.Errorf("URL principals %v not expected in SSH User certificate ", uris) + } + default: + err = fmt.Errorf("unexpected SSH certificate type %d", cert.CertType) } + return } diff --git a/policy/engine_test.go b/policy/engine_test.go index bea231ea..9bc535ea 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -7,6 +7,7 @@ import ( "net/url" "testing" + "github.com/google/go-cmp/cmp" "github.com/smallstep/assert" "golang.org/x/crypto/ssh" ) @@ -2177,11 +2178,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr bool }{ { - name: "fail/with-permitted-dns-domain", + name: "fail/host-with-permitted-dns-domain", options: []NamePolicyOption{ WithPermittedDNSDomain("*.local"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "host.example.com", }, @@ -2190,11 +2192,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/with-excluded-dns-domain", + name: "fail/host-with-excluded-dns-domain", options: []NamePolicyOption{ WithExcludedDNSDomain("*.local"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "host.local", }, @@ -2203,11 +2206,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/with-permitted-ip", + name: "fail/host-with-permitted-ip", options: []NamePolicyOption{ WithPermittedCIDR("127.0.0.1/24"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "192.168.0.22", }, @@ -2216,11 +2220,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/with-excluded-ip", + name: "fail/host-with-excluded-ip", options: []NamePolicyOption{ WithExcludedCIDR("127.0.0.1/24"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "127.0.0.0", }, @@ -2229,11 +2234,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/with-permitted-email", + name: "fail/user-with-permitted-email", options: []NamePolicyOption{ WithPermittedEmailAddress("@example.com"), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "mail@local", }, @@ -2242,11 +2248,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/with-excluded-email", + name: "fail/user-with-excluded-email", options: []NamePolicyOption{ WithExcludedEmailAddress("@example.com"), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "mail@example.com", }, @@ -2255,11 +2262,39 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/with-permitted-principals", + name: "fail/host-with-permitted-principals", + options: []NamePolicyOption{ + WithPermittedPrincipals([]string{"localhost"}), + }, + cert: &ssh.Certificate{ + CertType: ssh.HostCert, + ValidPrincipals: []string{ + "host", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/host-with-excluded-principals", + options: []NamePolicyOption{ + WithExcludedPrincipals([]string{"localhost"}), + }, + cert: &ssh.Certificate{ + ValidPrincipals: []string{ + "localhost", + }, + }, + want: false, + wantErr: true, + }, + { + name: "fail/user-with-permitted-principals", options: []NamePolicyOption{ WithPermittedPrincipals([]string{"user"}), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "root", }, @@ -2268,11 +2303,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/with-excluded-principals", + name: "fail/user-with-excluded-principals", options: []NamePolicyOption{ WithExcludedPrincipals([]string{"user"}), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "user", }, @@ -2281,11 +2317,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/with-permitted-principal-as-mail", + name: "fail/user-with-permitted-principal-as-mail", options: []NamePolicyOption{ WithPermittedPrincipals([]string{"ops"}), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "ops@work", // this is (currently) parsed as an email-like principal; not allowed with just "ops" as the permitted principal }, @@ -2294,11 +2331,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/principal-with-permitted-dns-domain", // when only DNS is permitted, username principals are not allowed. + name: "fail/host-principal-with-permitted-dns-domain", // when only DNS is permitted, username principals are not allowed. options: []NamePolicyOption{ WithPermittedDNSDomain("*.local"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "user", }, @@ -2307,11 +2345,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/principal-with-permitted-ip-range", // when only IPs are permitted, username principals are not allowed. + name: "fail/host-principal-with-permitted-ip-range", // when only IPs are permitted, username principals are not allowed. options: []NamePolicyOption{ WithPermittedCIDR("127.0.0.1/24"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "user", }, @@ -2320,11 +2359,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/principal-with-permitted-email", // when only emails are permitted, username principals are not allowed. + name: "fail/user-principal-with-permitted-email", // when only emails are permitted, username principals are not allowed. options: []NamePolicyOption{ WithPermittedEmailAddress("@example.com"), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "user", }, @@ -2339,6 +2379,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { WithExcludedEmailAddress("root@smallstep.com"), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "someone@smallstep.com", "someone", @@ -2354,6 +2395,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { WithExcludedPrincipals([]string{"root"}), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "someone@smallstep.com", "root", @@ -2363,11 +2405,40 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "ok/with-permitted-dns-domain", + name: "ok/host-with-permitted-user-principals", + options: []NamePolicyOption{ + WithPermittedEmailAddress("@work"), + }, + cert: &ssh.Certificate{ + CertType: ssh.HostCert, + ValidPrincipals: []string{ + "example.work", + }, + }, + want: false, + wantErr: true, + }, + { + name: "ok/user-with-permitted-user-principals", options: []NamePolicyOption{ WithPermittedDNSDomain("*.local"), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, + ValidPrincipals: []string{ + "herman@work", + }, + }, + want: false, + wantErr: true, + }, + { + name: "ok/host-with-permitted-dns-domain", + options: []NamePolicyOption{ + WithPermittedDNSDomain("*.local"), + }, + cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "host.local", }, @@ -2376,11 +2447,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/with-excluded-dns-domain", + name: "ok/host-with-excluded-dns-domain", options: []NamePolicyOption{ WithExcludedDNSDomain("*.example.com"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "host.local", }, @@ -2389,11 +2461,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/with-permitted-ip", + name: "ok/host-with-permitted-ip", options: []NamePolicyOption{ WithPermittedCIDR("127.0.0.1/24"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "127.0.0.33", }, @@ -2402,11 +2475,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/with-excluded-ip", + name: "ok/host-with-excluded-ip", options: []NamePolicyOption{ WithExcludedCIDR("127.0.0.1/24"), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "192.168.0.35", }, @@ -2415,11 +2489,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/with-permitted-email", + name: "ok/user-with-permitted-email", options: []NamePolicyOption{ WithPermittedEmailAddress("@example.com"), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "mail@example.com", }, @@ -2428,11 +2503,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/with-excluded-email", + name: "ok/user-with-excluded-email", options: []NamePolicyOption{ WithExcludedEmailAddress("@example.com"), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "mail@local", }, @@ -2441,11 +2517,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/with-permitted-principals", + name: "ok/user-with-permitted-principals", options: []NamePolicyOption{ WithPermittedPrincipals([]string{"*"}), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "user", }, @@ -2454,11 +2531,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/with-excluded-principals", + name: "ok/user-with-excluded-principals", options: []NamePolicyOption{ WithExcludedPrincipals([]string{"user"}), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "root", }, @@ -2474,6 +2552,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { WithExcludedEmailAddress("root@smallstep.com"), }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "someone@smallstep.com", "someone", @@ -2490,6 +2569,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { WithExcludedPrincipals([]string{"root"}), // unlike the previous test, this implicitly allows any other username principal }, cert: &ssh.Certificate{ + CertType: ssh.UserCert, ValidPrincipals: []string{ "someone@smallstep.com", "someone", @@ -2499,23 +2579,18 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/combined-simple-all", + name: "ok/combined-host", options: []NamePolicyOption{ WithPermittedDNSDomain("*.local"), WithPermittedCIDR("127.0.0.1/24"), - WithPermittedEmailAddress("@example.local"), - WithPermittedPrincipals([]string{"user"}), WithExcludedDNSDomain("badhost.local"), WithExcludedCIDR("127.0.0.128/25"), - WithExcludedEmailAddress("badmail@example.local"), - WithExcludedPrincipals([]string{"root"}), }, cert: &ssh.Certificate{ + CertType: ssh.HostCert, ValidPrincipals: []string{ "example.local", - "127.0.0.1", - "user@example.local", - "user", + "127.0.0.31", }, }, want: true, @@ -2537,3 +2612,162 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { }) } } + +type result struct { + wantDNSNames []string + wantIps []net.IP + wantEmails []string + wantUsernames []string +} + +func emptyResult() result { + return result{ + wantDNSNames: []string{}, + wantIps: []net.IP{}, + wantEmails: []string{}, + wantUsernames: []string{}, + } +} + +func Test_splitSSHPrincipals(t *testing.T) { + type test struct { + cert *ssh.Certificate + r result + wantErr bool + } + var tests = map[string]func(t *testing.T) test{ + "fail/unexpected-cert-type": func(t *testing.T) test { + r := emptyResult() + return test{ + cert: &ssh.Certificate{ + CertType: uint32(0), + }, + r: r, + wantErr: true, + } + }, + "fail/user-ip": func(t *testing.T) test { + r := emptyResult() + r.wantIps = []net.IP{net.ParseIP("127.0.0.1")} // this will still be in the result + return test{ + cert: &ssh.Certificate{ + CertType: ssh.UserCert, + ValidPrincipals: []string{"127.0.0.1"}, + }, + r: r, + wantErr: true, + } + }, + "fail/user-uri": func(t *testing.T) test { + r := emptyResult() + return test{ + cert: &ssh.Certificate{ + CertType: ssh.UserCert, + ValidPrincipals: []string{"https://host.local/"}, + }, + r: r, + wantErr: true, + } + }, + "fail/host-email": func(t *testing.T) test { + r := emptyResult() + r.wantEmails = []string{"ops@work"} // this will still be in the result + return test{ + cert: &ssh.Certificate{ + CertType: ssh.HostCert, + ValidPrincipals: []string{"ops@work"}, + }, + r: r, + wantErr: true, + } + }, + "fail/host-uri": func(t *testing.T) test { + r := emptyResult() + return test{ + cert: &ssh.Certificate{ + CertType: ssh.HostCert, + ValidPrincipals: []string{"https://host.local/"}, + }, + r: r, + wantErr: true, + } + }, + "ok/host-dns": func(t *testing.T) test { + r := emptyResult() + r.wantDNSNames = []string{"host.example.com"} + return test{ + cert: &ssh.Certificate{ + CertType: ssh.HostCert, + ValidPrincipals: []string{"host.example.com"}, + }, + r: r, + } + }, + "ok/host-ip": func(t *testing.T) test { + r := emptyResult() + r.wantIps = []net.IP{net.ParseIP("127.0.0.1")} + return test{ + cert: &ssh.Certificate{ + CertType: ssh.HostCert, + ValidPrincipals: []string{"127.0.0.1"}, + }, + r: r, + } + }, + "ok/user-localhost": func(t *testing.T) test { + r := emptyResult() + r.wantUsernames = []string{"localhost"} // when type is User cert, this is considered a username; not a DNS + return test{ + cert: &ssh.Certificate{ + CertType: ssh.UserCert, + ValidPrincipals: []string{"localhost"}, + }, + r: r, + } + }, + "ok/user-username-with-period": func(t *testing.T) test { + r := emptyResult() + r.wantUsernames = []string{"x.joe"} + return test{ + cert: &ssh.Certificate{ + CertType: ssh.UserCert, + ValidPrincipals: []string{"x.joe"}, + }, + r: r, + } + }, + "ok/user-maillike": func(t *testing.T) test { + r := emptyResult() + r.wantEmails = []string{"ops@work"} + return test{ + cert: &ssh.Certificate{ + CertType: ssh.UserCert, + ValidPrincipals: []string{"ops@work"}, + }, + r: r, + } + }, + } + for name, prep := range tests { + tt := prep(t) + t.Run(name, func(t *testing.T) { + gotDNSNames, gotIps, gotEmails, gotUsernames, err := splitSSHPrincipals(tt.cert) + if (err != nil) != tt.wantErr { + t.Errorf("splitSSHPrincipals() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !cmp.Equal(tt.r.wantDNSNames, gotDNSNames) { + t.Errorf("splitSSHPrincipals() DNS names diff =\n%s", cmp.Diff(tt.r.wantDNSNames, gotDNSNames)) + } + if !cmp.Equal(tt.r.wantIps, gotIps) { + t.Errorf("splitSSHPrincipals() IPs diff =\n%s", cmp.Diff(tt.r.wantIps, gotIps)) + } + if !cmp.Equal(tt.r.wantEmails, gotEmails) { + t.Errorf("splitSSHPrincipals() Emails diff =\n%s", cmp.Diff(tt.r.wantEmails, gotEmails)) + } + if !cmp.Equal(tt.r.wantUsernames, gotUsernames) { + t.Errorf("splitSSHPrincipals() Usernames diff =\n%s", cmp.Diff(tt.r.wantUsernames, gotUsernames)) + } + }) + } +} diff --git a/policy/options.go b/policy/options.go index b1fbba70..60bf2f72 100755 --- a/policy/options.go +++ b/policy/options.go @@ -602,7 +602,7 @@ func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) return "", errors.Errorf("domain constraint %q can only have wildcard as starting character", constraint) } if _, ok := domainToReverseLabels(normalizedConstraint); !ok { - return "", errors.Errorf("cannot parse permitted domain constraint %q", constraint) + return "", errors.Errorf("cannot parse domain constraint %q", constraint) } return normalizedConstraint, nil } From 512b8d673083f0b61d1642e09575a8629629b2e3 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 25 Jan 2022 16:45:25 +0100 Subject: [PATCH 09/78] Refactor instantiation of policy engines Instead of using the `base` struct, the x509 and SSH policy engines are now added to each provisioner directly. --- authority/provisioner/acme.go | 27 ++++++++++---------- authority/provisioner/aws.go | 12 +++++---- authority/provisioner/azure.go | 12 +++++---- authority/provisioner/gcp.go | 12 +++++---- authority/provisioner/jwk.go | 12 +++++---- authority/provisioner/k8sSA.go | 15 +++++++----- authority/provisioner/nebula.go | 29 +++++++++++++--------- authority/provisioner/oidc.go | 12 +++++---- authority/provisioner/provisioner.go | 6 +---- authority/provisioner/scep.go | 16 ++++++------ authority/provisioner/sign_ssh_options.go | 2 -- authority/provisioner/sshpop.go | 1 - authority/provisioner/utils_test.go | 9 ------- authority/provisioner/x5c.go | 30 ++++++++++++----------- 14 files changed, 100 insertions(+), 95 deletions(-) diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 83d35e49..bab7e7ae 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -8,19 +8,21 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" ) // ACME is the acme provisioner type, an entity that can authorize the ACME // provisioning flow. type ACME struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - ForceCN bool `json:"forceCN,omitempty"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - claimer *Claimer + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + ForceCN bool `json:"forceCN,omitempty"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + claimer *Claimer + x509Policy policy.X509NamePolicyEngine } // GetID returns the provisioner unique identifier. @@ -70,7 +72,6 @@ func (p *ACME) DefaultTLSCertDuration() time.Duration { // Init initializes and validates the fields of an ACME type. func (p *ACME) Init(config Config) (err error) { - p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -86,7 +87,7 @@ func (p *ACME) Init(config Config) (err error) { // Initialize the x509 allow/deny policy engine // TODO(hs): ensure no race conditions happen when reloading settings and requesting certs? // TODO(hs): implement memoization strategy, so that reloading is not required when no changes were made to allow/deny? - if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } @@ -113,16 +114,16 @@ type ACMEIdentifier struct { // certificate for the Identifiers provided in an Order. func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier string) error { - if p.x509PolicyEngine == nil { + if p.x509Policy == nil { return nil } // assuming only valid identifiers (IP or DNS) are provided var err error if ip := net.ParseIP(identifier); ip != nil { - _, err = p.x509PolicyEngine.IsIPAllowed(ip) + _, err = p.x509Policy.IsIPAllowed(ip) } else { - _, err = p.x509PolicyEngine.IsDNSAllowed(identifier) + _, err = p.x509Policy.IsDNSAllowed(identifier) } return err @@ -140,7 +141,7 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509PolicyEngine), + newX509NamePolicyValidator(p.x509Policy), } return opts, nil diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 9f542873..f63b9ced 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -18,6 +18,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -267,6 +268,8 @@ type AWS struct { claimer *Claimer config *awsConfig audiences Audiences + x509Policy policy.X509NamePolicyEngine + sshPolicy policy.SSHNamePolicyEngine } // GetID returns the provisioner unique identifier. @@ -392,7 +395,6 @@ func (p *AWS) GetIdentityToken(subject, caURL string) (string, error) { // Init validates and initializes the AWS provisioner. func (p *AWS) Init(config Config) (err error) { - p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -427,12 +429,12 @@ func (p *AWS) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine - if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -489,7 +491,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, commonNameValidator(payload.Claims.Subject), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509PolicyEngine), + newX509NamePolicyValidator(p.x509Policy), ), nil } @@ -772,6 +774,6 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicyEngine), + newSSHNamePolicyValidator(p.sshPolicy), ), nil } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index b8bbe143..5ccdc06b 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -14,6 +14,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -98,6 +99,8 @@ type Azure struct { config *azureConfig oidcConfig openIDConfiguration keyStore *keyStore + x509Policy policy.X509NamePolicyEngine + sshPolicy policy.SSHNamePolicyEngine } // GetID returns the provisioner unique identifier. @@ -191,7 +194,6 @@ func (p *Azure) GetIdentityToken(subject, caURL string) (string, error) { // Init validates and initializes the Azure provisioner. func (p *Azure) Init(config Config) (err error) { - p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -223,12 +225,12 @@ func (p *Azure) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine - if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -339,7 +341,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // validators defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509PolicyEngine), + newX509NamePolicyValidator(p.x509Policy), ), nil } @@ -409,7 +411,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicyEngine), + newSSHNamePolicyValidator(p.sshPolicy), ), nil } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 4c7f2046..590c32e2 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -15,6 +15,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -92,6 +93,8 @@ type GCP struct { config *gcpConfig keyStore *keyStore audiences Audiences + x509Policy policy.X509NamePolicyEngine + sshPolicy policy.SSHNamePolicyEngine } // GetID returns the provisioner unique identifier. The name should uniquely @@ -195,7 +198,6 @@ func (p *GCP) GetIdentityToken(subject, caURL string) (string, error) { // Init validates and initializes the GCP provisioner. func (p *GCP) Init(config Config) error { - p.base = &base{} // prevent nil pointers var err error switch { case p.Type == "": @@ -218,12 +220,12 @@ func (p *GCP) Init(config Config) error { } // Initialize the x509 allow/deny policy engine - if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine - if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -284,7 +286,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // validators defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509PolicyEngine), + newX509NamePolicyValidator(p.x509Policy), ), nil } @@ -451,6 +453,6 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicyEngine), + newSSHNamePolicyValidator(p.sshPolicy), ), nil } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 3ee8113f..081fbb90 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -37,6 +38,8 @@ type JWK struct { Options *Options `json:"options,omitempty"` claimer *Claimer audiences Audiences + x509Policy policy.X509NamePolicyEngine + sshPolicy policy.SSHNamePolicyEngine } // GetID returns the provisioner unique identifier. The name and credential id @@ -89,7 +92,6 @@ func (p *JWK) GetEncryptedKey() (string, string, bool) { // Init initializes and validates the fields of a JWK type. func (p *JWK) Init(config Config) (err error) { - p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -105,12 +107,12 @@ func (p *JWK) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine - if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -197,7 +199,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, defaultSANsValidator(claims.SANs), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509PolicyEngine), + newX509NamePolicyValidator(p.x509Policy), }, nil } @@ -292,7 +294,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicyEngine), + newSSHNamePolicyValidator(p.sshPolicy), ), nil } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 707e141e..d52f0d12 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/sshutil" @@ -51,7 +52,9 @@ type K8sSA struct { claimer *Claimer audiences Audiences //kauthn kauthn.AuthenticationV1Interface - pubKeys []interface{} + pubKeys []interface{} + x509Policy policy.X509NamePolicyEngine + sshPolicy policy.SSHNamePolicyEngine } // GetID returns the provisioner unique identifier. The name and credential id @@ -92,7 +95,7 @@ func (p *K8sSA) GetEncryptedKey() (string, string, bool) { // Init initializes and validates the fields of a K8sSA type. func (p *K8sSA) Init(config Config) (err error) { - p.base = &base{} // prevent nil pointers + switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -145,12 +148,12 @@ func (p *K8sSA) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine - if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -255,7 +258,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // validators defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509PolicyEngine), + newX509NamePolicyValidator(p.x509Policy), }, nil } @@ -302,7 +305,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicyEngine), + newSSHNamePolicyValidator(p.sshPolicy), ), nil } diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index dfff8617..545939ac 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" nebula "github.com/slackhq/nebula/cert" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x25519" @@ -35,20 +36,21 @@ const ( // go.step.sm/crypto/x25519. type Nebula struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Roots []byte `json:"roots"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - claimer *Claimer - caPool *nebula.NebulaCAPool - audiences Audiences + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Roots []byte `json:"roots"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + claimer *Claimer + caPool *nebula.NebulaCAPool + audiences Audiences + x509Policy policy.X509NamePolicyEngine + sshPolicy policy.SSHNamePolicyEngine } // Init verifies and initializes the Nebula provisioner. func (p *Nebula) Init(config Config) error { - p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -71,12 +73,12 @@ func (p *Nebula) Init(config Config) error { p.audiences = config.Audiences.WithFragment(p.GetIDForToken()) // Initialize the x509 allow/deny policy engine - if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine - if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -177,6 +179,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption, }, defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + newX509NamePolicyValidator(p.x509Policy), }, nil } @@ -272,6 +275,8 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, + // Ensure that all principal names are allowed + newSSHNamePolicyValidator(p.sshPolicy), ), nil } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 707f8228..e4fe8090 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -94,6 +95,8 @@ type OIDC struct { keyStore *keyStore claimer *Claimer getIdentityFunc GetIdentityFunc + x509Policy policy.X509NamePolicyEngine + sshPolicy policy.SSHNamePolicyEngine } func sanitizeEmail(email string) string { @@ -154,7 +157,6 @@ func (o *OIDC) GetEncryptedKey() (kid, key string, ok bool) { // Init validates and initializes the OIDC provider. func (o *OIDC) Init(config Config) (err error) { - o.base = &base{} // prevent nil pointers switch { case o.Type == "": return errors.New("type cannot be empty") @@ -210,12 +212,12 @@ func (o *OIDC) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if o.x509PolicyEngine, err = newX509PolicyEngine(o.Options.GetX509Options()); err != nil { + if o.x509Policy, err = newX509PolicyEngine(o.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine - if o.sshPolicyEngine, err = newSSHPolicyEngine(o.Options.GetSSHOptions()); err != nil { + if o.sshPolicy, err = newSSHPolicyEngine(o.Options.GetSSHOptions()); err != nil { return err } @@ -375,7 +377,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators defaultPublicKeyValidator{}, newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(o.x509PolicyEngine), + newX509NamePolicyValidator(o.x509Policy), }, nil } @@ -466,7 +468,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(o.sshPolicyEngine), + newSSHNamePolicyValidator(o.sshPolicy), ), nil } diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index a7d6e01d..55ebe092 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -12,7 +12,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "golang.org/x/crypto/ssh" ) @@ -305,10 +304,7 @@ func SanitizeSSHUserPrincipal(email string) string { }, strings.ToLower(email)) } -type base struct { - x509PolicyEngine policy.X509NamePolicyEngine - sshPolicyEngine policy.SSHNamePolicyEngine -} +type base struct{} // AuthorizeSign returns an unimplemented error. Provisioners should overwrite // this method if they will support authorizing tokens for signing x509 Certificates. diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index 7c78d14b..418f7030 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -5,6 +5,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/policy" ) // SCEP is the SCEP provisioner type, an entity that can authorize the @@ -19,11 +20,11 @@ type SCEP struct { ChallengePassword string `json:"challenge,omitempty"` Capabilities []string `json:"capabilities,omitempty"` // MinimumPublicKeyLength is the minimum length for public keys in CSRs - MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"` - Options *Options `json:"options,omitempty"` - Claims *Claims `json:"claims,omitempty"` - claimer *Claimer - + MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"` + Options *Options `json:"options,omitempty"` + Claims *Claims `json:"claims,omitempty"` + claimer *Claimer + x509Policy policy.X509NamePolicyEngine secretChallengePassword string } @@ -74,7 +75,6 @@ func (s *SCEP) DefaultTLSCertDuration() time.Duration { // Init initializes and validates the fields of a SCEP type. func (s *SCEP) Init(config Config) (err error) { - s.base = &base{} // prevent nil pointers switch { case s.Type == "": return errors.New("provisioner type cannot be empty") @@ -103,7 +103,7 @@ func (s *SCEP) Init(config Config) (err error) { // TODO: add other, SCEP specific, options? // Initialize the x509 allow/deny policy engine - if s.x509PolicyEngine, err = newX509PolicyEngine(s.Options.GetX509Options()); err != nil { + if s.x509Policy, err = newX509PolicyEngine(s.Options.GetX509Options()); err != nil { return err } @@ -122,7 +122,7 @@ func (s *SCEP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength), newValidityValidator(s.claimer.MinTLSCertDuration(), s.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(s.x509PolicyEngine), + newX509NamePolicyValidator(s.x509Policy), }, nil } diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index e1853fa1..374bd65c 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -454,7 +454,6 @@ type sshNamePolicyValidator struct { // newSSHNamePolicyValidator return a new SSH allow/deny validator. func newSSHNamePolicyValidator(engine policy.SSHNamePolicyEngine) *sshNamePolicyValidator { return &sshNamePolicyValidator{ - // TODO: should we use two engines, one for host certs; another for user certs? policyEngine: engine, } } @@ -465,7 +464,6 @@ func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) if v.policyEngine == nil { return nil } - _, err := v.policyEngine.ArePrincipalsAllowed(cert) return err } diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index b41f512e..16fa0599 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -84,7 +84,6 @@ func (p *SSHPOP) GetEncryptedKey() (string, string, bool) { // Init initializes and validates the fields of a SSHPOP type. func (p *SSHPOP) Init(config Config) error { - p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index ea0890ae..fe2678fc 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -177,7 +177,6 @@ func generateJWK() (*JWK, error) { return nil, err } return &JWK{ - base: &base{}, Name: name, Type: "JWK", Key: &public, @@ -216,7 +215,6 @@ func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) { } return &K8sSA{ - base: &base{}, Name: K8sSAName, Type: "K8sSA", Claims: &globalProvisionerClaims, @@ -254,7 +252,6 @@ func generateSSHPOP() (*SSHPOP, error) { } return &SSHPOP{ - base: &base{}, Name: name, Type: "SSHPOP", Claims: &globalProvisionerClaims, @@ -309,7 +306,6 @@ M46l92gdOozT rootPool.AddCert(cert) } return &X5C{ - base: &base{}, Name: name, Type: "X5C", Roots: root, @@ -342,7 +338,6 @@ func generateOIDC() (*OIDC, error) { return nil, err } return &OIDC{ - base: &base{}, Name: name, Type: "OIDC", ClientID: clientID, @@ -378,7 +373,6 @@ func generateGCP() (*GCP, error) { return nil, err } return &GCP{ - base: &base{}, Type: "GCP", Name: name, ServiceAccounts: []string{serviceAccount}, @@ -415,7 +409,6 @@ func generateAWS() (*AWS, error) { return nil, errors.Wrap(err, "error parsing AWS certificate") } return &AWS{ - base: &base{}, Type: "AWS", Name: name, Accounts: []string{accountID}, @@ -525,7 +518,6 @@ func generateAWSV1Only() (*AWS, error) { return nil, errors.Wrap(err, "error parsing AWS certificate") } return &AWS{ - base: &base{}, Type: "AWS", Name: name, Accounts: []string{accountID}, @@ -617,7 +609,6 @@ func generateAzure() (*Azure, error) { return nil, err } return &Azure{ - base: &base{}, Type: "Azure", Name: name, TenantID: tenantID, diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index a87e4392..434fc576 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -26,15 +27,17 @@ type x5cPayload struct { // signature requests. type X5C struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Roots []byte `json:"roots"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - claimer *Claimer - audiences Audiences - rootPool *x509.CertPool + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Roots []byte `json:"roots"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + claimer *Claimer + audiences Audiences + rootPool *x509.CertPool + x509Policy policy.X509NamePolicyEngine + sshPolicy policy.SSHNamePolicyEngine } // GetID returns the provisioner unique identifier. The name and credential id @@ -87,7 +90,6 @@ func (p *X5C) GetEncryptedKey() (string, string, bool) { // Init initializes and validates the fields of a X5C type. func (p *X5C) Init(config Config) error { - p.base = &base{} // prevent nil pointers switch { case p.Type == "": return errors.New("provisioner type cannot be empty") @@ -127,12 +129,12 @@ func (p *X5C) Init(config Config) error { } // Initialize the x509 allow/deny policy engine - if p.x509PolicyEngine, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine - if p.sshPolicyEngine, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -240,7 +242,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultSANsValidator(claims.SANs), defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509PolicyEngine), + newX509NamePolicyValidator(p.x509Policy), }, nil } @@ -324,6 +326,6 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicyEngine), + newSSHNamePolicyValidator(p.sshPolicy), ), nil } From 9617edf0c2b160f1fbd3e2835e2a40855ee5644c Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 27 Jan 2022 17:18:33 +0100 Subject: [PATCH 10/78] Improve internationalized domain name handling This PR improves internationalized domain name handling according to rules of IDNA and based on the description in RFC 5280, section 7: https://datatracker.ietf.org/doc/html/rfc5280#section-7. Support for internationalized URI(s), so-called IRIs, still needs to be done. --- authority/provisioner/nebula.go | 1 - policy/engine.go | 79 ++++++++++++++---- policy/engine_test.go | 137 ++++++++++++++++++++------------ policy/options.go | 60 ++++++++++++-- policy/options_test.go | 106 ++++++++++++++++++++---- 5 files changed, 295 insertions(+), 88 deletions(-) diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index 545939ac..6c31dbc5 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -35,7 +35,6 @@ const ( // https://signal.org/docs/specifications/xeddsa/#xeddsa and implemented by // go.step.sm/crypto/x25519. type Nebula struct { - *base ID string `json:"-"` Type string `json:"type"` Name string `json:"name"` diff --git a/policy/engine.go b/policy/engine.go index 345d6282..42d4f303 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -10,9 +10,9 @@ import ( "reflect" "strings" - "github.com/pkg/errors" "go.step.sm/crypto/x509util" "golang.org/x/crypto/ssh" + "golang.org/x/net/idna" ) type NamePolicyReason int @@ -23,6 +23,15 @@ const ( // doesn't permit a DNS or another type of SAN to be signed // (or otherwise used). NotAuthorizedForThisName NamePolicyReason = iota + // CannotParseDomain is returned when an error occurs + // when parsing the domain part of SAN or subject. + CannotParseDomain + // CannotParseRFC822Name is returned when an error + // occurs when parsing an email address. + CannotParseRFC822Name + // CannotMatch is the type of error returned when + // an error happens when matching SAN types. + CannotMatchNameToConstraint ) type NamePolicyError struct { @@ -31,16 +40,26 @@ type NamePolicyError struct { } func (e NamePolicyError) Error() string { - if e.Reason == NotAuthorizedForThisName { + switch e.Reason { + case NotAuthorizedForThisName: return "not authorized to sign for this name: " + e.Detail + case CannotParseDomain: + return "cannot parse domain: " + e.Detail + case CannotParseRFC822Name: + return "cannot parse rfc822Name: " + e.Detail + case CannotMatchNameToConstraint: + return "error matching name to constraint: " + e.Detail + default: + return "unknown error: " + e.Detail } - return "unknown error" } // NamePolicyEngine can be used to check that a CSR or Certificate meets all allowed and // denied names before a CA creates and/or signs the Certificate. // TODO(hs): the X509 RFC also defines name checks on directory name; support that? // TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine? +// TODO(hs): implement matching URI schemes, paths, etc; not just the domain part of URI domains + type NamePolicyEngine struct { // verifySubjectCommonName is set when Subject Common Name must be verified @@ -275,8 +294,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA // this number as a total of all checks and keeps a (pointer to a) counter of the number of checks // executed so far. - // TODO: implement matching URI schemes, paths, etc; not just the domain - // TODO: gather all errors, or return early? Currently we return early on the first wrong name; check might fail for multiple names. // Perhaps make that an option? for _, dns := range dnsNames { @@ -289,10 +306,28 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), } } - if _, ok := domainToReverseLabels(dns); !ok { - return errors.Errorf("cannot parse dns %q", dns) + didCutWildcard := false + if strings.HasPrefix(dns, "*.") { + dns = dns[1:] + didCutWildcard = true } - if err := checkNameConstraints("dns", dns, dns, + parsedDNS, err := idna.Lookup.ToASCII(dns) + if err != nil { + return NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns), + } + } + if didCutWildcard { + parsedDNS = "*" + parsedDNS + } + if _, ok := domainToReverseLabels(parsedDNS); !ok { + return NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("cannot parse dns %q", dns), + } + } + if err := checkNameConstraints("dns", dns, parsedDNS, func(parsedName, constraint interface{}) (bool, error) { return e.matchDomainConstraint(parsedName.(string), constraint.(string)) }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { @@ -324,8 +359,22 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } mailbox, ok := parseRFC2821Mailbox(email) if !ok { - return fmt.Errorf("cannot parse rfc822Name %q", mailbox) + return NamePolicyError{ + Reason: CannotParseRFC822Name, + Detail: fmt.Sprintf("invalid rfc822Name %q", mailbox), + } } + // According to RFC 5280, section 7.5, emails are considered to match if the local part is + // an exact match and the host (domain) part matches the ASCII representation (case-insensitive): + // https://datatracker.ietf.org/doc/html/rfc5280#section-7.5 + domainASCII, err := idna.ToASCII(mailbox.domain) + if err != nil { + return NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("cannot parse email domain %q", email), + } + } + mailbox.domain = domainASCII if err := checkNameConstraints("email", email, mailbox, func(parsedName, constraint interface{}) (bool, error) { return e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) @@ -334,6 +383,8 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } + // TODO(hs): fix internationalization for URIs (IRIs) + for _, uri := range uris { if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return NamePolicyError{ @@ -365,12 +416,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } - // TODO(hs): when the error is not nil and returned up in the above, we can add - // additional context to it (i.e. the cert or csr that was inspected). - - // TODO(hs): validate other types of SANs? The Go std library skips those. - // These could be custom checkers. - // if all checks out, all SANs are allowed return nil } @@ -393,7 +438,7 @@ func checkNameConstraints( match, err := match(parsedName, constraint) if err != nil { return NamePolicyError{ - Reason: NotAuthorizedForThisName, + Reason: CannotMatchNameToConstraint, Detail: err.Error(), } } @@ -414,7 +459,7 @@ func checkNameConstraints( var err error if ok, err = match(parsedName, constraint); err != nil { return NamePolicyError{ - Reason: NotAuthorizedForThisName, + Reason: CannotMatchNameToConstraint, Detail: err.Error(), } } diff --git a/policy/engine_test.go b/policy/engine_test.go index 9bc535ea..e42c589d 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -17,16 +17,15 @@ import ( func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { tests := []struct { - name string - engine *NamePolicyEngine - domain string - constraint string - want bool - wantErr bool + name string + allowLiteralWildcardNames bool + domain string + constraint string + want bool + wantErr bool }{ { name: "fail/wildcard", - engine: &NamePolicyEngine{}, domain: "host.local", constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain want: false, @@ -34,7 +33,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/wildcard-literal", - engine: &NamePolicyEngine{}, domain: "*.example.com", constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain want: false, @@ -42,7 +40,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/specific-domain", - engine: &NamePolicyEngine{}, domain: "www.example.com", constraint: "host.example.com", want: false, @@ -50,7 +47,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/single-whitespace-domain", - engine: &NamePolicyEngine{}, domain: " ", constraint: "host.example.com", want: false, @@ -58,7 +54,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/period-domain", - engine: &NamePolicyEngine{}, domain: ".host.example.com", constraint: ".example.com", want: false, @@ -66,7 +61,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/wrong-asterisk-prefix", - engine: &NamePolicyEngine{}, domain: "*Xexample.com", constraint: ".example.com", want: false, @@ -74,7 +68,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/asterisk-in-domain", - engine: &NamePolicyEngine{}, domain: "e*ample.com", constraint: ".com", want: false, @@ -82,7 +75,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/asterisk-label", - engine: &NamePolicyEngine{}, domain: "example.*.local", constraint: ".local", want: false, @@ -90,7 +82,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/multiple-periods", - engine: &NamePolicyEngine{}, domain: "example.local", constraint: "..local", want: false, @@ -98,23 +89,20 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/error-parsing-domain", - engine: &NamePolicyEngine{}, - domain: string([]byte{0}), + domain: string(byte(0)), constraint: ".local", want: false, wantErr: true, }, { name: "fail/error-parsing-constraint", - engine: &NamePolicyEngine{}, domain: "example.local", - constraint: string([]byte{0}), + constraint: string(byte(0)), want: false, wantErr: true, }, { name: "fail/no-subdomain", - engine: &NamePolicyEngine{}, domain: "local", constraint: ".local", want: false, @@ -122,7 +110,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/too-many-subdomains", - engine: &NamePolicyEngine{}, domain: "www.example.local", constraint: ".local", want: false, @@ -130,7 +117,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "fail/wrong-domain", - engine: &NamePolicyEngine{}, domain: "example.notlocal", constraint: ".local", want: false, @@ -138,7 +124,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "false/idna-internationalized-domain-name", - engine: &NamePolicyEngine{}, domain: "JP納豆.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ constraint: ".例.jp", want: false, @@ -146,7 +131,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "false/idna-internationalized-domain-name-constraint", - engine: &NamePolicyEngine{}, domain: "xn--jp-cd2fp15c.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ constraint: ".例.jp", want: false, @@ -154,7 +138,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "ok/empty-constraint", - engine: &NamePolicyEngine{}, domain: "www.example.com", constraint: "", want: true, @@ -162,25 +145,21 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "ok/wildcard", - engine: &NamePolicyEngine{}, domain: "www.example.com", constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain want: true, wantErr: false, }, { - name: "ok/wildcard-literal", - engine: &NamePolicyEngine{ - allowLiteralWildcardNames: true, - }, - domain: "*.example.com", // specifically allowed using an option on the NamePolicyEngine - constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain - want: true, - wantErr: false, + name: "ok/wildcard-literal", + allowLiteralWildcardNames: true, + domain: "*.example.com", // specifically allowed using an option on the NamePolicyEngine + constraint: ".example.com", // internally we're using the x509 period prefix as the indicator for exactly one subdomain + want: true, + wantErr: false, }, { name: "ok/specific-domain", - engine: &NamePolicyEngine{}, domain: "www.example.com", constraint: "www.example.com", want: true, @@ -188,7 +167,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "ok/different-case", - engine: &NamePolicyEngine{}, domain: "WWW.EXAMPLE.com", constraint: "www.example.com", want: true, @@ -196,7 +174,6 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { }, { name: "ok/idna-internationalized-domain-name-punycode", - engine: &NamePolicyEngine{}, domain: "xn--jp-cd2fp15c.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ constraint: ".xn--fsq.jp", want: true, @@ -205,7 +182,10 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.engine.matchDomainConstraint(tt.domain, tt.constraint) + engine := NamePolicyEngine{ + allowLiteralWildcardNames: tt.allowLiteralWildcardNames, + } + got, err := engine.matchDomainConstraint(tt.domain, tt.constraint) if (err != nil) != tt.wantErr { t.Errorf("NamePolicyEngine.matchDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) return @@ -749,6 +729,19 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/dns-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.豆.jp"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + string(byte(0)) + ".例.jp", + }, + }, + want: false, + wantErr: true, + }, { name: "fail/ipv4-permitted", options: []NamePolicyOption{ @@ -837,6 +830,39 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/mail-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@例.jp"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"bücher@例.jp"}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-idna-internationalized-domain-rfc822", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@例.jp"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"bücher@例.jp" + string(byte(0))}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-permitted-idna-internationalized-domain-ascii", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@例.jp"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@xn---bla.jp"}, + }, + want: false, + wantErr: true, + }, { name: "fail/permitted-uri-domain-wildcard", options: []NamePolicyOption{ @@ -1453,17 +1479,6 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: true, wantErr: false, }, - { - name: "ok/empty-dns-constraint", - options: []NamePolicyOption{ - AddPermittedDNSDomain(""), - }, - cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, - }, - want: true, - wantErr: false, - }, { name: "ok/dns-permitted-wildcard-literal", options: []NamePolicyOption{ @@ -1497,6 +1512,19 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/dns-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.例.jp"), + }, + cert: &x509.Certificate{ + DNSNames: []string{ + "JP納豆.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + }, + }, + want: true, + wantErr: false, + }, { name: "ok/ipv4-permitted", options: []NamePolicyOption{ @@ -1558,6 +1586,17 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/mail-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@例.jp"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{}, + }, + want: true, + wantErr: false, + }, { name: "ok/uri-permitted-domain-wildcard", options: []NamePolicyOption{ diff --git a/policy/options.go b/policy/options.go index 60bf2f72..d37b206f 100755 --- a/policy/options.go +++ b/policy/options.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/pkg/errors" + "golang.org/x/net/idna" ) type NamePolicyOption func(e *NamePolicyEngine) error @@ -592,14 +593,24 @@ func isIPv4(ip net.IP) bool { func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) + if normalizedConstraint == "" { + return "", errors.Errorf("contraint %q can not be empty or white space string", constraint) + } if strings.Contains(normalizedConstraint, "..") { return "", errors.Errorf("domain constraint %q cannot have empty labels", constraint) } + if normalizedConstraint[0] == '*' && normalizedConstraint[1] != '.' { + return "", errors.Errorf("wildcard character in domain constraint %q can only be used to match (full) labels", constraint) + } + if strings.LastIndex(normalizedConstraint, "*") > 0 { + return "", errors.Errorf("domain constraint %q can only have wildcard as starting character", constraint) + } if strings.HasPrefix(normalizedConstraint, "*.") { normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period } - if strings.Contains(normalizedConstraint, "*") { - return "", errors.Errorf("domain constraint %q can only have wildcard as starting character", constraint) + normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint) + if err != nil { + return "", errors.Wrapf(err, "domain constraint %q can not be converted to ASCII", constraint) } if _, ok := domainToReverseLabels(normalizedConstraint); !ok { return "", errors.Errorf("cannot parse domain constraint %q", constraint) @@ -609,8 +620,11 @@ func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) func normalizeAndValidateEmailConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) + if normalizedConstraint == "" { + return "", errors.Errorf("email contraint %q can not be empty or white space string", constraint) + } if strings.Contains(normalizedConstraint, "*") { - return "", fmt.Errorf("email constraint %q cannot contain asterisk", constraint) + return "", fmt.Errorf("email constraint %q cannot contain asterisk wildcard", constraint) } if strings.Count(normalizedConstraint, "@") > 1 { return "", fmt.Errorf("email constraint %q contains too many @ characters", constraint) @@ -622,8 +636,23 @@ func normalizeAndValidateEmailConstraint(constraint string) (string, error) { return "", fmt.Errorf("email constraint %q cannot start with period", constraint) } if strings.Contains(normalizedConstraint, "@") { - if _, ok := parseRFC2821Mailbox(normalizedConstraint); !ok { - return "", fmt.Errorf("cannot parse email constraint %q", constraint) + mailbox, ok := parseRFC2821Mailbox(normalizedConstraint) + if !ok { + return "", fmt.Errorf("cannot parse email constraint %q as RFC 2821 mailbox", constraint) + } + // According to RFC 5280, section 7.5, emails are considered to match if the local part is + // an exact match and the host (domain) part matches the ASCII representation (case-insensitive): + // https://datatracker.ietf.org/doc/html/rfc5280#section-7.5 + domainASCII, err := idna.Lookup.ToASCII(mailbox.domain) + if err != nil { + return "", errors.Wrapf(err, "email constraint %q domain part %q cannot be converted to ASCII", constraint, mailbox.domain) + } + normalizedConstraint = mailbox.local + "@" + domainASCII + } else { + var err error + normalizedConstraint, err = idna.Lookup.ToASCII(normalizedConstraint) + if err != nil { + return "", errors.Wrapf(err, "email constraint %q cannot be converted to ASCII", constraint) } } if _, ok := domainToReverseLabels(normalizedConstraint); !ok { @@ -634,6 +663,9 @@ func normalizeAndValidateEmailConstraint(constraint string) (string, error) { func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) + if normalizedConstraint == "" { + return "", errors.Errorf("URI domain contraint %q cannot be empty or white space string", constraint) + } if strings.Contains(normalizedConstraint, "..") { return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint) } @@ -643,7 +675,23 @@ func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) if strings.Contains(normalizedConstraint, "*") { return "", errors.Errorf("URI domain constraint %q can only have wildcard as starting character", constraint) } - // TODO(hs): block constraints that look like IPs too? Because hosts can't be matched to those. + // we're being strict with square brackets in domains; we don't allow them, no matter what + if strings.Contains(normalizedConstraint, "[") || strings.Contains(normalizedConstraint, "]") { + return "", errors.Errorf("URI domain constraint %q contains invalid square brackets", constraint) + } + if _, _, err := net.SplitHostPort(normalizedConstraint); err == nil { + // a successful split (likely) with host and port; we don't currently allow ports in the config + return "", errors.Errorf("URI domain constraint %q cannot contain port", constraint) + } + // check if the host part of the URI domain constraint is an IP + if net.ParseIP(normalizedConstraint) != nil { + return "", errors.Errorf("URI domain constraint %q cannot be an IP", constraint) + } + // TODO(hs): verify that this is OK for URI (IRI) domains too + normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint) + if err != nil { + return "", errors.Wrapf(err, "URI domain constraint %q cannot be converted to ASCII", constraint) + } _, ok := domainToReverseLabels(normalizedConstraint) if !ok { return "", fmt.Errorf("cannot parse URI domain constraint %q", constraint) diff --git a/policy/options_test.go b/policy/options_test.go index 7f417887..af4aeb3a 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -16,8 +16,20 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { wantErr bool }{ { - name: "fail/too-many-asterisks", - constraint: "**.local", + name: "fail/empty-constraint", + constraint: "", + want: "", + wantErr: true, + }, + { + name: "fail/wildcard-partial-label", + constraint: "*xxxx.local", + want: "", + wantErr: true, + }, + { + name: "fail/wildcard-in-the-middle", + constraint: "x.*.local", want: "", wantErr: true, }, @@ -34,14 +46,8 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { wantErr: true, }, { - name: "false/idna-internationalized-domain-name", - constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ - want: "", - wantErr: true, - }, - { - name: "false/idna-internationalized-domain-name-constraint", - constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + name: "fail/idna-internationalized-domain-name-lookup", + constraint: `\00.local`, // invalid IDNA ASCII character want: "", wantErr: true, }, @@ -63,13 +69,18 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { want: ".xn--fsq.jp", wantErr: false, }, + { + name: "ok/idna-internationalized-domain-name-lookup-transformed", + constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + want: ".xn--fsq.jp", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := normalizeAndValidateDNSDomainConstraint(tt.constraint) if (err != nil) != tt.wantErr { t.Errorf("normalizeAndValidateDNSDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) - return } if got != tt.want { t.Errorf("normalizeAndValidateDNSDomainConstraint() = %v, want %v", got, tt.want) @@ -85,6 +96,12 @@ func Test_normalizeAndValidateEmailConstraint(t *testing.T) { want string wantErr bool }{ + { + name: "fail/empty-constraint", + constraint: "", + want: "", + wantErr: true, + }, { name: "fail/asterisk", constraint: "*.local", @@ -111,13 +128,25 @@ func Test_normalizeAndValidateEmailConstraint(t *testing.T) { }, { name: "fail/parse-mailbox", - constraint: "mail@example.com" + string([]byte{0}), + constraint: "mail@example.com" + string(byte(0)), + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain", + constraint: `mail@xn--bla.local`, + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain-name-lookup", + constraint: `\00local`, want: "", wantErr: true, }, { name: "fail/parse-domain", - constraint: "example.com" + string([]byte{0}), + constraint: "x..example.com", want: "", wantErr: true, }, @@ -133,13 +162,19 @@ func Test_normalizeAndValidateEmailConstraint(t *testing.T) { want: "mail@local", wantErr: false, }, + // TODO(hs): fix the below; doesn't get past parseRFC2821Mailbox; I think it should be allowed. + // { + // name: "ok/idna-internationalized-local", + // constraint: `bücher@local`, + // want: "bücher@local", + // wantErr: false, + // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := normalizeAndValidateEmailConstraint(tt.constraint) if (err != nil) != tt.wantErr { t.Errorf("normalizeAndValidateEmailConstraint() error = %v, wantErr %v", err, tt.wantErr) - return } if got != tt.want { t.Errorf("normalizeAndValidateEmailConstraint() = %v, want %v", got, tt.want) @@ -155,6 +190,12 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { want string wantErr bool }{ + { + name: "fail/empty-constraint", + constraint: "", + want: "", + wantErr: true, + }, { name: "fail/too-many-asterisks", constraint: "**.local", @@ -173,6 +214,42 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { want: "", wantErr: true, }, + { + name: "fail/domain-with-port", + constraint: "host.local:8443", + want: "", + wantErr: true, + }, + { + name: "fail/ipv4", + constraint: "127.0.0.1", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-brackets", + constraint: "[::1]", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-no-brackets", + constraint: "::1", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-no-brackets", + constraint: "[::1", + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain-name-lookup", + constraint: `\00local`, + want: "", + wantErr: true, + }, { name: "ok/wildcard", constraint: "*.local", @@ -191,7 +268,6 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { got, err := normalizeAndValidateURIDomainConstraint(tt.constraint) if (err != nil) != tt.wantErr { t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) - return } if got != tt.want { t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want) From a7eb27d30951364b3f12c56d8375fb0c62236feb Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 31 Jan 2022 15:34:02 +0100 Subject: [PATCH 11/78] Fix URI domains IDNA support --- policy/engine_test.go | 70 ++++++++++++++++++++++++++++++++++++++++-- policy/options.go | 4 ++- policy/options_test.go | 33 ++++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/policy/engine_test.go b/policy/engine_test.go index e42c589d..1f8be691 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -864,7 +864,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/permitted-uri-domain-wildcard", + name: "fail/uri-permitted-domain-wildcard", options: []NamePolicyOption{ AddPermittedURIDomain("*.local"), }, @@ -880,7 +880,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/permitted-uri", + name: "fail/uri-permitted", options: []NamePolicyOption{ AddPermittedURIDomain("test.local"), }, @@ -896,7 +896,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/permitted-uri-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld + name: "fail/uri-permitted-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld options: []NamePolicyOption{ AddPermittedURIDomain("*.local"), }, @@ -911,6 +911,22 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/uri-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedURIDomain("*.bücher.example.com"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "abc.bücher.example.com", + }, + }, + }, + want: false, + wantErr: true, + }, // SINGLE SAN TYPE EXCLUDED FAILURE TESTS { name: "fail/dns-excluded", @@ -997,6 +1013,22 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: true, }, + { + name: "fail/uri-excluded-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld + options: []NamePolicyOption{ + AddExcludedURIDomain("*.local"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "*.local", + }, + }, + }, + want: false, + wantErr: true, + }, // SUBJECT FAILURE TESTS { name: "fail/subject-dns-permitted", @@ -1645,6 +1677,38 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/uri-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedURIDomain("*.bücher.example.com"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "abc.xn--bcher-kva.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "ok/uri-permitted-idna-internationalized-domain", + options: []NamePolicyOption{ + AddPermittedURIDomain("bücher.example.com"), + }, + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "xn--bcher-kva.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, // SINGLE SAN TYPE EXCLUDED SUCCESS TESTS { name: "ok/dns-excluded", diff --git a/policy/options.go b/policy/options.go index d37b206f..fe8f470e 100755 --- a/policy/options.go +++ b/policy/options.go @@ -666,6 +666,9 @@ func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) if normalizedConstraint == "" { return "", errors.Errorf("URI domain contraint %q cannot be empty or white space string", constraint) } + if strings.Contains(normalizedConstraint, "://") { + return "", errors.Errorf("URI domain constraint %q contains scheme (not supported yet)", constraint) + } if strings.Contains(normalizedConstraint, "..") { return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint) } @@ -687,7 +690,6 @@ func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) if net.ParseIP(normalizedConstraint) != nil { return "", errors.Errorf("URI domain constraint %q cannot be an IP", constraint) } - // TODO(hs): verify that this is OK for URI (IRI) domains too normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint) if err != nil { return "", errors.Wrapf(err, "URI domain constraint %q cannot be converted to ASCII", constraint) diff --git a/policy/options_test.go b/policy/options_test.go index af4aeb3a..0fc54aa2 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -196,6 +196,12 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { want: "", wantErr: true, }, + { + name: "fail/scheme-https", + constraint: `https://*.local`, + want: "", + wantErr: true, + }, { name: "fail/too-many-asterisks", constraint: "**.local", @@ -262,6 +268,18 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { want: "example.local", wantErr: false, }, + { + name: "ok/idna-internationalized-domain-name-lookup", + constraint: `*.bücher.example.com`, + want: ".xn--bcher-kva.example.com", + wantErr: false, + }, + { + name: "ok/idna-internationalized-domain-name-lookup-deviation", + constraint: `*.faß.de`, + want: ".fass.de", // IDNA2003 vs. 2008 deviation: https://unicode.org/reports/tr46/#Deviations + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1447,6 +1465,21 @@ func TestNew(t *testing.T) { wantErr: false, } }, + "ok/with-permitted-uri-idna": func(t *testing.T) test { + options := []NamePolicyOption{ + WithPermittedURIDomain("*.bücher.example.com"), + } + return test{ + options: options, + want: &NamePolicyEngine{ + permittedURIDomains: []string{".xn--bcher-kva.example.com"}, + numberOfURIDomainConstraints: 1, + totalNumberOfPermittedConstraints: 1, + totalNumberOfConstraints: 1, + }, + wantErr: false, + } + }, "ok/add-permitted-uri": func(t *testing.T) test { options := []NamePolicyOption{ WithPermittedURIDomain("host.local"), From 88c7b63c9d913fea82123399d3e4b507cc0fb59f Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 1 Feb 2022 14:58:13 +0100 Subject: [PATCH 12/78] Split SSH user and cert policy configuration and execution --- authority/provisioner/aws.go | 11 ++- authority/provisioner/azure.go | 11 ++- authority/provisioner/gcp.go | 11 ++- authority/provisioner/jwk.go | 35 +++++---- authority/provisioner/k8sSA.go | 19 +++-- authority/provisioner/k8sSA_test.go | 3 +- authority/provisioner/nebula.go | 35 +++++---- authority/provisioner/oidc.go | 17 +++-- authority/provisioner/policy.go | 93 ++++++++++++++++++++++- authority/provisioner/sign_options.go | 6 +- authority/provisioner/sign_ssh_options.go | 45 +++++++++-- authority/provisioner/ssh_options.go | 18 ++++- authority/provisioner/x5c.go | 35 +++++---- authority/provisioner/x5c_test.go | 3 +- policy/engine.go | 34 ++++----- policy/engine_test.go | 48 ++++++------ 16 files changed, 285 insertions(+), 139 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index f63b9ced..2ff8ade9 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -18,7 +18,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -268,8 +267,8 @@ type AWS struct { claimer *Claimer config *awsConfig audiences Audiences - x509Policy policy.X509NamePolicyEngine - sshPolicy policy.SSHNamePolicyEngine + x509Policy x509PolicyEngine + sshHostPolicy *hostPolicyEngine } // GetID returns the provisioner unique identifier. @@ -433,8 +432,8 @@ func (p *AWS) Init(config Config) (err error) { return err } - // Initialize the SSH allow/deny policy engine - if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + // Initialize the SSH allow/deny policy engine for host certificates + if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -774,6 +773,6 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, nil), ), nil } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 5ccdc06b..40b7d3f5 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -14,7 +14,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -99,8 +98,8 @@ type Azure struct { config *azureConfig oidcConfig openIDConfiguration keyStore *keyStore - x509Policy policy.X509NamePolicyEngine - sshPolicy policy.SSHNamePolicyEngine + x509Policy x509PolicyEngine + sshHostPolicy *hostPolicyEngine } // GetID returns the provisioner unique identifier. @@ -229,8 +228,8 @@ func (p *Azure) Init(config Config) (err error) { return err } - // Initialize the SSH allow/deny policy engine - if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + // Initialize the SSH allow/deny policy engine for host certificates + if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -411,7 +410,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, nil), ), nil } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 590c32e2..e56c0729 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -15,7 +15,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -93,8 +92,8 @@ type GCP struct { config *gcpConfig keyStore *keyStore audiences Audiences - x509Policy policy.X509NamePolicyEngine - sshPolicy policy.SSHNamePolicyEngine + x509Policy x509PolicyEngine + sshHostPolicy *hostPolicyEngine } // GetID returns the provisioner unique identifier. The name should uniquely @@ -224,8 +223,8 @@ func (p *GCP) Init(config Config) error { return err } - // Initialize the SSH allow/deny policy engine - if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + // Initialize the SSH allow/deny policy engine for host certificates + if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -453,6 +452,6 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, nil), ), nil } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 081fbb90..a129a536 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -8,7 +8,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -29,17 +28,18 @@ type stepPayload struct { // signature requests. type JWK struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Key *jose.JSONWebKey `json:"key"` - EncryptedKey string `json:"encryptedKey,omitempty"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - claimer *Claimer - audiences Audiences - x509Policy policy.X509NamePolicyEngine - sshPolicy policy.SSHNamePolicyEngine + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Key *jose.JSONWebKey `json:"key"` + EncryptedKey string `json:"encryptedKey,omitempty"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + claimer *Claimer + audiences Audiences + x509Policy x509PolicyEngine + sshHostPolicy *hostPolicyEngine + sshUserPolicy *userPolicyEngine } // GetID returns the provisioner unique identifier. The name and credential id @@ -111,8 +111,13 @@ func (p *JWK) Init(config Config) (err error) { return err } - // Initialize the SSH allow/deny policy engine - if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + // Initialize the SSH allow/deny policy engine for user certificates + if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine for host certificates + if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -294,7 +299,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), ), nil } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index d52f0d12..be55f114 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -11,7 +11,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/sshutil" @@ -52,9 +51,10 @@ type K8sSA struct { claimer *Claimer audiences Audiences //kauthn kauthn.AuthenticationV1Interface - pubKeys []interface{} - x509Policy policy.X509NamePolicyEngine - sshPolicy policy.SSHNamePolicyEngine + pubKeys []interface{} + x509Policy x509PolicyEngine + sshHostPolicy *hostPolicyEngine + sshUserPolicy *userPolicyEngine } // GetID returns the provisioner unique identifier. The name and credential id @@ -152,8 +152,13 @@ func (p *K8sSA) Init(config Config) (err error) { return err } - // Initialize the SSH allow/deny policy engine - if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + // Initialize the SSH allow/deny policy engine for user certificates + if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine for host certificates + if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -305,7 +310,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), ), nil } diff --git a/authority/provisioner/k8sSA_test.go b/authority/provisioner/k8sSA_test.go index 3ccce461..c63a32bc 100644 --- a/authority/provisioner/k8sSA_test.go +++ b/authority/provisioner/k8sSA_test.go @@ -371,7 +371,8 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) { case *sshDefaultDuration: assert.Equals(t, v.Claimer, tc.p.claimer) case *sshNamePolicyValidator: - assert.Equals(t, nil, v.policyEngine) + assert.Equals(t, nil, v.userPolicyEngine) + assert.Equals(t, nil, v.hostPolicyEngine) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index 6c31dbc5..39980cc8 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -11,7 +11,6 @@ import ( "github.com/pkg/errors" nebula "github.com/slackhq/nebula/cert" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x25519" @@ -35,17 +34,18 @@ const ( // https://signal.org/docs/specifications/xeddsa/#xeddsa and implemented by // go.step.sm/crypto/x25519. type Nebula struct { - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Roots []byte `json:"roots"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - claimer *Claimer - caPool *nebula.NebulaCAPool - audiences Audiences - x509Policy policy.X509NamePolicyEngine - sshPolicy policy.SSHNamePolicyEngine + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Roots []byte `json:"roots"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + claimer *Claimer + caPool *nebula.NebulaCAPool + audiences Audiences + x509Policy x509PolicyEngine + sshHostPolicy *hostPolicyEngine + sshUserPolicy *userPolicyEngine } // Init verifies and initializes the Nebula provisioner. @@ -76,8 +76,13 @@ func (p *Nebula) Init(config Config) error { return err } - // Initialize the SSH allow/deny policy engine - if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + // Initialize the SSH allow/deny policy engine for user certificates + if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine for host certificates + if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -275,7 +280,7 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), ), nil } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index e4fe8090..60bb5cf1 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -13,7 +13,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -95,8 +94,9 @@ type OIDC struct { keyStore *keyStore claimer *Claimer getIdentityFunc GetIdentityFunc - x509Policy policy.X509NamePolicyEngine - sshPolicy policy.SSHNamePolicyEngine + x509Policy x509PolicyEngine + sshHostPolicy *hostPolicyEngine + sshUserPolicy *userPolicyEngine } func sanitizeEmail(email string) string { @@ -216,8 +216,13 @@ func (o *OIDC) Init(config Config) (err error) { return err } - // Initialize the SSH allow/deny policy engine - if o.sshPolicy, err = newSSHPolicyEngine(o.Options.GetSSHOptions()); err != nil { + // Initialize the SSH allow/deny policy engine for user certificates + if o.sshUserPolicy, err = newSSHUserPolicyEngine(o.Options.GetSSHOptions()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine for host certificates + if o.sshHostPolicy, err = newSSHHostPolicyEngine(o.Options.GetSSHOptions()); err != nil { return err } @@ -468,7 +473,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(o.sshPolicy), + newSSHNamePolicyValidator(o.sshHostPolicy, o.sshUserPolicy), ), nil } diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go index 8a69e1e5..b9740e39 100644 --- a/authority/provisioner/policy.go +++ b/authority/provisioner/policy.go @@ -1,11 +1,38 @@ package provisioner import ( + "fmt" + "github.com/smallstep/certificates/policy" + "golang.org/x/crypto/ssh" ) +type sshPolicyEngineType string + +const ( + userPolicyEngineType sshPolicyEngineType = "user" + hostPolicyEngineType sshPolicyEngineType = "host" +) + +var certTypeToPolicyEngineType = map[uint32]sshPolicyEngineType{ + uint32(ssh.UserCert): userPolicyEngineType, + uint32(ssh.HostCert): hostPolicyEngineType, +} + +type x509PolicyEngine interface { + policy.X509NamePolicyEngine +} + +type userPolicyEngine struct { + policy.SSHNamePolicyEngine +} + +type hostPolicyEngine struct { + policy.SSHNamePolicyEngine +} + // newX509PolicyEngine creates a new x509 name policy engine -func newX509PolicyEngine(x509Opts *X509Options) (policy.X509NamePolicyEngine, error) { +func newX509PolicyEngine(x509Opts *X509Options) (x509PolicyEngine, error) { if x509Opts == nil { return nil, nil @@ -38,16 +65,66 @@ func newX509PolicyEngine(x509Opts *X509Options) (policy.X509NamePolicyEngine, er return policy.New(options...) } +// newSSHUserPolicyEngine creates a new SSH user certificate policy engine +func newSSHUserPolicyEngine(sshOpts *SSHOptions) (*userPolicyEngine, error) { + policyEngine, err := newSSHPolicyEngine(sshOpts, userPolicyEngineType) + if err != nil { + return nil, err + } + // ensure we're not wrapping a nil engine + if policyEngine == nil { + return nil, nil + } + return &userPolicyEngine{ + SSHNamePolicyEngine: policyEngine, + }, nil +} + +// newSSHHostPolicyEngine create a new SSH host certificate policy engine +func newSSHHostPolicyEngine(sshOpts *SSHOptions) (*hostPolicyEngine, error) { + policyEngine, err := newSSHPolicyEngine(sshOpts, hostPolicyEngineType) + if err != nil { + return nil, err + } + // ensure we're not wrapping a nil engine + if policyEngine == nil { + return nil, nil + } + return &hostPolicyEngine{ + SSHNamePolicyEngine: policyEngine, + }, nil +} + // newSSHPolicyEngine creates a new SSH name policy engine -func newSSHPolicyEngine(sshOpts *SSHOptions) (policy.SSHNamePolicyEngine, error) { +func newSSHPolicyEngine(sshOpts *SSHOptions, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) { if sshOpts == nil { return nil, nil } + var ( + allowed *SSHNameOptions + denied *SSHNameOptions + ) + + // TODO: embed the type in the policy engine itself for reference? + switch typ { + case userPolicyEngineType: + if sshOpts.User != nil { + allowed = sshOpts.User.GetAllowedNameOptions() + denied = sshOpts.User.GetDeniedNameOptions() + } + case hostPolicyEngineType: + if sshOpts.Host != nil { + allowed = sshOpts.Host.AllowedNames + denied = sshOpts.Host.DeniedNames + } + default: + return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ) + } + options := []policy.NamePolicyOption{} - allowed := sshOpts.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { options = append(options, policy.WithPermittedDNSDomains(allowed.DNSDomains), @@ -57,7 +134,6 @@ func newSSHPolicyEngine(sshOpts *SSHOptions) (policy.SSHNamePolicyEngine, error) ) } - denied := sshOpts.GetDeniedNameOptions() if denied != nil && denied.HasNames() { options = append(options, policy.WithExcludedDNSDomains(denied.DNSDomains), @@ -67,5 +143,14 @@ func newSSHPolicyEngine(sshOpts *SSHOptions) (policy.SSHNamePolicyEngine, error) ) } + // Return nil, because there's no policy to execute. This is + // important, because the logic that determines user vs. host certs + // are allowed depends on this fact. The two policy engines are + // not aware of eachother, so this check is performed in the + // SSH name validator, instead. + if len(options) == 0 { + return nil, nil + } + return policy.New(options...) } diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index 7ca6cec4..3327310b 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -16,7 +16,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" ) @@ -408,11 +407,11 @@ func (v *validityValidator) Valid(cert *x509.Certificate, o SignOptions) error { // x509NamePolicyValidator validates that the certificate (to be signed) // contains only allowed SANs. type x509NamePolicyValidator struct { - policyEngine policy.X509NamePolicyEngine + policyEngine x509PolicyEngine } // newX509NamePolicyValidator return a new SANs allow/deny validator. -func newX509NamePolicyValidator(engine policy.X509NamePolicyEngine) *x509NamePolicyValidator { +func newX509NamePolicyValidator(engine x509PolicyEngine) *x509NamePolicyValidator { return &x509NamePolicyValidator{ policyEngine: engine, } @@ -424,7 +423,6 @@ func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) e if v.policyEngine == nil { return nil } - _, err := v.policyEngine.AreCertificateNamesAllowed(cert) return err } diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index 374bd65c..8f9cf466 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -4,13 +4,13 @@ import ( "crypto/rsa" "encoding/binary" "encoding/json" + "fmt" "math/big" "strings" "time" "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/keyutil" "golang.org/x/crypto/ssh" ) @@ -448,24 +448,55 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SignSSHOpti // sshNamePolicyValidator validates that the certificate (to be signed) // contains only allowed principals. type sshNamePolicyValidator struct { - policyEngine policy.SSHNamePolicyEngine + hostPolicyEngine *hostPolicyEngine + userPolicyEngine *userPolicyEngine } // newSSHNamePolicyValidator return a new SSH allow/deny validator. -func newSSHNamePolicyValidator(engine policy.SSHNamePolicyEngine) *sshNamePolicyValidator { +func newSSHNamePolicyValidator(host *hostPolicyEngine, user *userPolicyEngine) *sshNamePolicyValidator { return &sshNamePolicyValidator{ - policyEngine: engine, + hostPolicyEngine: host, + userPolicyEngine: user, } } // Valid validates validates that the certificate (to be signed) // contains only allowed principals. func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error { - if v.policyEngine == nil { + if v.hostPolicyEngine == nil && v.userPolicyEngine == nil { + // no policy configured at all; allow anything return nil } - _, err := v.policyEngine.ArePrincipalsAllowed(cert) - return err + + // Check the policy type to execute based on type of the certificate. + // We don't allow user certs if only a host policy engine is configured and + // the same for host certs: if only a user policy engine is configured, host + // certs are denied. When both policy engines are configured, the type of + // cert determines which policy engine is used. + policyType, ok := certTypeToPolicyEngineType[cert.CertType] + if !ok { + return fmt.Errorf("unexpected SSH cert type %d", cert.CertType) + } + switch policyType { + case hostPolicyEngineType: + // when no host policy engine is configured, but a user policy engine is + // configured, we don't allow the host certificate. + if v.hostPolicyEngine == nil && v.userPolicyEngine != nil { + return errors.New("SSH host certificate not authorized") // TODO: include principals in message? + } + _, err := v.hostPolicyEngine.ArePrincipalsAllowed(cert) + return err + case userPolicyEngineType: + // when no user policy engine is configured, but a host policy engine is + // configured, we don't allow the user certificate. + if v.userPolicyEngine == nil && v.hostPolicyEngine != nil { + return errors.New("SSH user certificate not authorized") // TODO: include principals in message? + } + _, err := v.userPolicyEngine.ArePrincipalsAllowed(cert) + return err + default: + return fmt.Errorf("unexpected policy engine type %q", policyType) // satisfy return; shouldn't happen + } } // sshCertTypeUInt32 diff --git a/authority/provisioner/ssh_options.go b/authority/provisioner/ssh_options.go index 91ce7126..dacafc80 100644 --- a/authority/provisioner/ssh_options.go +++ b/authority/provisioner/ssh_options.go @@ -34,6 +34,15 @@ type SSHOptions struct { // templates. TemplateData json.RawMessage `json:"templateData,omitempty"` + // User contains SSH user certificate options. + User *SSHUserCertificateOptions `json:"user,omitempty"` + + // Host contains SSH host certificate options. + Host *SSHHostCertificateOptions `json:"host,omitempty"` +} + +// SSHUserCertificateOptions is a collection of SSH user certificate options. +type SSHUserCertificateOptions struct { // AllowedNames contains the names the provisioner is authorized to sign AllowedNames *SSHNameOptions `json:"allow,omitempty"` @@ -41,6 +50,11 @@ type SSHOptions struct { DeniedNames *SSHNameOptions `json:"deny,omitempty"` } +// SSHHostCertificateOptions is a collection of SSH host certificate options. +// It's an alias of SSHUserCertificateOptions, as the options are the same +// for both types of certificates. +type SSHHostCertificateOptions SSHUserCertificateOptions + // SSHNameOptions models the SSH name policy configuration. type SSHNameOptions struct { DNSDomains []string `json:"dns,omitempty"` @@ -56,7 +70,7 @@ func (o *SSHOptions) HasTemplate() bool { // GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the // names that a provisioner is authorized to sign SSH certificates for. -func (o *SSHOptions) GetAllowedNameOptions() *SSHNameOptions { +func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions { if o == nil { return nil } @@ -65,7 +79,7 @@ func (o *SSHOptions) GetAllowedNameOptions() *SSHNameOptions { // GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the // names that a provisioner is NOT authorized to sign SSH certificates for. -func (o *SSHOptions) GetDeniedNameOptions() *SSHNameOptions { +func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions { if o == nil { return nil } diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 434fc576..850fc752 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" @@ -27,17 +26,18 @@ type x5cPayload struct { // signature requests. type X5C struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Roots []byte `json:"roots"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - claimer *Claimer - audiences Audiences - rootPool *x509.CertPool - x509Policy policy.X509NamePolicyEngine - sshPolicy policy.SSHNamePolicyEngine + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Roots []byte `json:"roots"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + claimer *Claimer + audiences Audiences + rootPool *x509.CertPool + x509Policy x509PolicyEngine + sshHostPolicy *hostPolicyEngine + sshUserPolicy *userPolicyEngine } // GetID returns the provisioner unique identifier. The name and credential id @@ -133,8 +133,13 @@ func (p *X5C) Init(config Config) error { return err } - // Initialize the SSH allow/deny policy engine - if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil { + // Initialize the SSH allow/deny policy engine for user certificates + if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + + // Initialize the SSH allow/deny policy engine for host certificates + if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } @@ -326,6 +331,6 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), ), nil } diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 5d2a3566..b91ca2ea 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -780,7 +780,8 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { case *sshCertValidityValidator: assert.Equals(t, v.Claimer, tc.p.claimer) case *sshNamePolicyValidator: - assert.Equals(t, nil, v.policyEngine) + assert.Equals(t, nil, v.userPolicyEngine) + assert.Equals(t, nil, v.hostPolicyEngine) case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc: default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) diff --git a/policy/engine.go b/policy/engine.go index 42d4f303..e9038dd0 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -216,11 +216,11 @@ func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) { // ArePrincipalsAllowed verifies that all principals in an SSH certificate are allowed. func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) { - dnsNames, ips, emails, usernames, err := splitSSHPrincipals(cert) + dnsNames, ips, emails, principals, err := splitSSHPrincipals(cert) if err != nil { return false, err } - if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, usernames); err != nil { + if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, principals); err != nil { return false, err } return true, nil @@ -243,32 +243,26 @@ func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.I } // splitPrincipals splits SSH certificate principals into DNS names, emails and usernames. -func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, usernames []string, err error) { +func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, principals []string, err error) { dnsNames = []string{} ips = []net.IP{} emails = []string{} - usernames = []string{} + principals = []string{} var uris []*url.URL switch cert.CertType { case ssh.HostCert: dnsNames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) - switch { - case len(emails) > 0: - err = fmt.Errorf("Email(-like) principals %v not expected in SSH Host certificate ", emails) - case len(uris) > 0: - err = fmt.Errorf("URL principals %v not expected in SSH Host certificate ", uris) + if len(uris) > 0 { + err = fmt.Errorf("URL principals %v not expected in SSH host certificate ", uris) } case ssh.UserCert: // re-using SplitSANs results in anything that can't be parsed as an IP, URI or email - // to be considered a username. This allows usernames like h.slatman to be present + // to be considered a username principal. This allows usernames like h.slatman to be present // in the SSH certificate. We're exluding IPs and URIs, because they can be confusing // when used in a SSH user certificate. - usernames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) - switch { - case len(ips) > 0: - err = fmt.Errorf("IP principals %v not expected in SSH User certificate ", ips) - case len(uris) > 0: - err = fmt.Errorf("URL principals %v not expected in SSH User certificate ", uris) + principals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) + if len(uris) > 0 { + err = fmt.Errorf("URL principals %v not expected in SSH user certificate ", uris) } default: err = fmt.Errorf("unexpected SSH certificate type %d", cert.CertType) @@ -280,7 +274,7 @@ func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, // 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, usernames []string) error { +func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailAddresses []string, uris []*url.URL, principals []string) error { // nothing to compare against; return early if e.totalNumberOfConstraints == 0 { @@ -400,15 +394,15 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } - for _, username := range usernames { + for _, principal := range principals { if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return NamePolicyError{ Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", username), + Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal), } } // TODO: some validation? I.e. allowed characters? - if err := checkNameConstraints("username", username, username, + if err := checkNameConstraints("principal", principal, principal, func(parsedName, constraint interface{}) (bool, error) { return matchUsernameConstraint(parsedName.(string), constraint.(string)) }, e.permittedPrincipals, e.excludedPrincipals); err != nil { diff --git a/policy/engine_test.go b/policy/engine_test.go index 1f8be691..0259e8de 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -2749,18 +2749,6 @@ func Test_splitSSHPrincipals(t *testing.T) { wantErr: true, } }, - "fail/user-ip": func(t *testing.T) test { - r := emptyResult() - r.wantIps = []net.IP{net.ParseIP("127.0.0.1")} // this will still be in the result - return test{ - cert: &ssh.Certificate{ - CertType: ssh.UserCert, - ValidPrincipals: []string{"127.0.0.1"}, - }, - r: r, - wantErr: true, - } - }, "fail/user-uri": func(t *testing.T) test { r := emptyResult() return test{ @@ -2772,18 +2760,6 @@ func Test_splitSSHPrincipals(t *testing.T) { wantErr: true, } }, - "fail/host-email": func(t *testing.T) test { - r := emptyResult() - r.wantEmails = []string{"ops@work"} // this will still be in the result - return test{ - cert: &ssh.Certificate{ - CertType: ssh.HostCert, - ValidPrincipals: []string{"ops@work"}, - }, - r: r, - wantErr: true, - } - }, "fail/host-uri": func(t *testing.T) test { r := emptyResult() return test{ @@ -2817,6 +2793,18 @@ func Test_splitSSHPrincipals(t *testing.T) { r: r, } }, + "ok/host-email": func(t *testing.T) test { + r := emptyResult() + r.wantEmails = []string{"ops@work"} + return test{ + cert: &ssh.Certificate{ + CertType: ssh.HostCert, + ValidPrincipals: []string{"ops@work"}, + }, + r: r, + wantErr: false, + } + }, "ok/user-localhost": func(t *testing.T) test { r := emptyResult() r.wantUsernames = []string{"localhost"} // when type is User cert, this is considered a username; not a DNS @@ -2839,6 +2827,18 @@ func Test_splitSSHPrincipals(t *testing.T) { r: r, } }, + "ok/user-ip": func(t *testing.T) test { + r := emptyResult() + r.wantIps = []net.IP{net.ParseIP("127.0.0.1")} + return test{ + cert: &ssh.Certificate{ + CertType: ssh.UserCert, + ValidPrincipals: []string{"127.0.0.1"}, + }, + r: r, + wantErr: false, + } + }, "ok/user-maillike": func(t *testing.T) test { r := emptyResult() r.wantEmails = []string{"ops@work"} From 7c541888ad281e0c6f669ce3963c0f305ac84a62 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 8 Mar 2022 13:26:07 +0100 Subject: [PATCH 13/78] Refactor configuration of allow/deny on authority level --- authority/authority.go | 21 +++ authority/config/config.go | 2 + authority/policy/options.go | 170 ++++++++++++++++++++++ authority/policy/policy.go | 134 +++++++++++++++++ authority/provisioner/acme.go | 6 +- authority/provisioner/aws.go | 9 +- authority/provisioner/azure.go | 9 +- authority/provisioner/gcp.go | 9 +- authority/provisioner/jwk.go | 13 +- authority/provisioner/k8sSA.go | 13 +- authority/provisioner/nebula.go | 13 +- authority/provisioner/oidc.go | 13 +- authority/provisioner/options.go | 32 ++-- authority/provisioner/policy.go | 156 -------------------- authority/provisioner/scep.go | 6 +- authority/provisioner/sign_options.go | 8 +- authority/provisioner/sign_ssh_options.go | 30 ++-- authority/provisioner/ssh_options.go | 92 ++++++------ authority/provisioner/x5c.go | 13 +- authority/ssh.go | 40 +++++ authority/tls.go | 19 +++ 21 files changed, 515 insertions(+), 293 deletions(-) create mode 100644 authority/policy/options.go create mode 100644 authority/policy/policy.go delete mode 100644 authority/provisioner/policy.go diff --git a/authority/authority.go b/authority/authority.go index f396c588..4eacfad7 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -16,6 +16,7 @@ import ( adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql" "github.com/smallstep/certificates/authority/administrator" "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/cas" casapi "github.com/smallstep/certificates/cas/apiv1" @@ -75,6 +76,11 @@ type Authority struct { sshGetHostsFunc func(ctx context.Context, cert *x509.Certificate) ([]config.Host, error) getIdentityFunc provisioner.GetIdentityFunc + // Policy engines + x509Policy policy.X509Policy + sshUserPolicy policy.UserPolicy + sshHostPolicy policy.HostPolicy + adminMutex sync.RWMutex } @@ -539,6 +545,21 @@ func (a *Authority) init() error { a.templates.Data["Step"] = tmplVars } + // Initialize the x509 allow/deny policy engine + if a.x509Policy, err = policy.NewX509PolicyEngine(a.config.AuthorityConfig.Policy.GetX509Options()); err != nil { + return err + } + + // // Initialize the SSH allow/deny policy engine for host certificates + if a.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(a.config.AuthorityConfig.Policy.GetSSHOptions()); err != nil { + return err + } + + // // Initialize the SSH allow/deny policy engine for user certificates + if a.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(a.config.AuthorityConfig.Policy.GetSSHOptions()); err != nil { + return err + } + // JWT numeric dates are seconds. a.startTime = time.Now().Truncate(time.Second) // Set flag indicating that initialization has been completed, and should diff --git a/authority/config/config.go b/authority/config/config.go index 589b5bbf..0f6120f9 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -8,6 +8,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" cas "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" @@ -90,6 +91,7 @@ type AuthConfig struct { Admins []*linkedca.Admin `json:"-"` Template *ASN1DN `json:"template,omitempty"` Claims *provisioner.Claims `json:"claims,omitempty"` + Policy *policy.Options `json:"policy,omitempty"` DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"` Backdate *provisioner.Duration `json:"backdate,omitempty"` EnableAdmin bool `json:"enableAdmin,omitempty"` diff --git a/authority/policy/options.go b/authority/policy/options.go new file mode 100644 index 00000000..f57f3bcf --- /dev/null +++ b/authority/policy/options.go @@ -0,0 +1,170 @@ +package policy + +type Options struct { + X509 *X509PolicyOptions `json:"x509,omitempty"` + SSH *SSHPolicyOptions `json:"ssh,omitempty"` +} + +func (o *Options) GetX509Options() *X509PolicyOptions { + if o == nil { + return nil + } + return o.X509 +} + +func (o *Options) GetSSHOptions() *SSHPolicyOptions { + if o == nil { + return nil + } + return o.SSH +} + +type X509PolicyOptionsInterface interface { + GetAllowedNameOptions() *X509NameOptions + GetDeniedNameOptions() *X509NameOptions +} + +type X509PolicyOptions struct { + // AllowedNames ... + AllowedNames *X509NameOptions `json:"allow,omitempty"` + + // DeniedNames ... + DeniedNames *X509NameOptions `json:"deny,omitempty"` +} + +// X509NameOptions models the X509 name policy configuration. +type X509NameOptions struct { + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` + EmailAddresses []string `json:"email,omitempty"` + URIDomains []string `json:"uri,omitempty"` +} + +// HasNames checks if the AllowedNameOptions has one or more +// names configured. +func (o *X509NameOptions) HasNames() bool { + return len(o.DNSDomains) > 0 || + len(o.IPRanges) > 0 || + len(o.EmailAddresses) > 0 || + len(o.URIDomains) > 0 +} + +type SSHPolicyOptionsInterface interface { + GetAllowedUserNameOptions() *SSHNameOptions + GetDeniedUserNameOptions() *SSHNameOptions + GetAllowedHostNameOptions() *SSHNameOptions + GetDeniedHostNameOptions() *SSHNameOptions +} + +type SSHPolicyOptions struct { + // User contains SSH user certificate options. + User *SSHUserCertificateOptions `json:"user,omitempty"` + + // Host contains SSH host certificate options. + Host *SSHHostCertificateOptions `json:"host,omitempty"` +} + +// GetAllowedNameOptions returns AllowedNames, which models the +// SANs that ... +func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions { + if o == nil { + return nil + } + return o.AllowedNames +} + +// GetDeniedNameOptions returns the DeniedNames, which models the +// SANs that ... +func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { + if o == nil { + return nil + } + return o.DeniedNames +} + +func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.AllowedNames +} + +func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.DeniedNames +} + +func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.Host == nil { + return nil + } + return o.Host.AllowedNames +} + +func (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.Host == nil { + return nil + } + return o.Host.DeniedNames +} + +// SSHUserCertificateOptions is a collection of SSH user certificate options. +type SSHUserCertificateOptions struct { + // AllowedNames contains the names the provisioner is authorized to sign + AllowedNames *SSHNameOptions `json:"allow,omitempty"` + // DeniedNames contains the names the provisioner is not authorized to sign + DeniedNames *SSHNameOptions `json:"deny,omitempty"` +} + +// SSHHostCertificateOptions is a collection of SSH host certificate options. +// It's an alias of SSHUserCertificateOptions, as the options are the same +// for both types of certificates. +type SSHHostCertificateOptions SSHUserCertificateOptions + +// SSHNameOptions models the SSH name policy configuration. +type SSHNameOptions struct { + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` + EmailAddresses []string `json:"email,omitempty"` + Principals []string `json:"principal,omitempty"` +} + +// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the +// names that a provisioner is authorized to sign SSH certificates for. +func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + return o.AllowedNames +} + +// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the +// names that a provisioner is NOT authorized to sign SSH certificates for. +func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + return o.DeniedNames +} + +// HasNames checks if the SSHNameOptions has one or more +// names configured. +func (o *SSHNameOptions) HasNames() bool { + return len(o.DNSDomains) > 0 || + len(o.EmailAddresses) > 0 || + len(o.Principals) > 0 +} diff --git a/authority/policy/policy.go b/authority/policy/policy.go new file mode 100644 index 00000000..403ac0b7 --- /dev/null +++ b/authority/policy/policy.go @@ -0,0 +1,134 @@ +package policy + +import ( + "fmt" + + "github.com/smallstep/certificates/policy" +) + +// X509Policy is an alias for policy.X509NamePolicyEngine +type X509Policy policy.X509NamePolicyEngine + +// UserPolicy is an alias for policy.SSHNamePolicyEngine +type UserPolicy policy.SSHNamePolicyEngine + +// HostPolicy is an alias for policy.SSHNamePolicyEngine +type HostPolicy policy.SSHNamePolicyEngine + +// NewX509PolicyEngine creates a new x509 name policy engine +func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) { + + // return early if no policy engine options to configure + if policyOptions == nil { + return nil, nil + } + + options := []policy.NamePolicyOption{} + + allowed := policyOptions.GetAllowedNameOptions() + if allowed != nil && allowed.HasNames() { + options = append(options, + policy.WithPermittedDNSDomains(allowed.DNSDomains), + policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), + policy.WithPermittedEmailAddresses(allowed.EmailAddresses), + policy.WithPermittedURIDomains(allowed.URIDomains), + ) + } + + denied := policyOptions.GetDeniedNameOptions() + if denied != nil && denied.HasNames() { + options = append(options, + policy.WithExcludedDNSDomains(denied.DNSDomains), + policy.WithExcludedIPsOrCIDRs(denied.IPRanges), + policy.WithExcludedEmailAddresses(denied.EmailAddresses), + policy.WithExcludedURIDomains(denied.URIDomains), + ) + } + + // ensure no policy engine is returned when no name options were provided + if len(options) == 0 { + return nil, nil + } + + // enable x509 Subject Common Name validation by default + options = append(options, policy.WithSubjectCommonNameVerification()) + + return policy.New(options...) +} + +type sshPolicyEngineType string + +const ( + UserPolicyEngineType sshPolicyEngineType = "user" + HostPolicyEngineType sshPolicyEngineType = "host" +) + +// newSSHUserPolicyEngine creates a new SSH user certificate policy engine +func NewSSHUserPolicyEngine(policyOptions SSHPolicyOptionsInterface) (UserPolicy, error) { + policyEngine, err := newSSHPolicyEngine(policyOptions, UserPolicyEngineType) + if err != nil { + return nil, err + } + return policyEngine, nil +} + +// newSSHHostPolicyEngine create a new SSH host certificate policy engine +func NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy, error) { + policyEngine, err := newSSHPolicyEngine(policyOptions, HostPolicyEngineType) + if err != nil { + return nil, err + } + return policyEngine, nil +} + +// newSSHPolicyEngine creates a new SSH name policy engine +func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) { + + // return early if no policy engine options to configure + if policyOptions == nil { + return nil, nil + } + + var ( + allowed *SSHNameOptions + denied *SSHNameOptions + ) + + switch typ { + case UserPolicyEngineType: + allowed = policyOptions.GetAllowedUserNameOptions() + denied = policyOptions.GetDeniedUserNameOptions() + case HostPolicyEngineType: + allowed = policyOptions.GetAllowedHostNameOptions() + denied = policyOptions.GetDeniedHostNameOptions() + default: + return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ) + } + + options := []policy.NamePolicyOption{} + + if allowed != nil && allowed.HasNames() { + options = append(options, + policy.WithPermittedDNSDomains(allowed.DNSDomains), + policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), + policy.WithPermittedEmailAddresses(allowed.EmailAddresses), + policy.WithPermittedPrincipals(allowed.Principals), + ) + } + + if denied != nil && denied.HasNames() { + options = append(options, + policy.WithExcludedDNSDomains(denied.DNSDomains), + policy.WithExcludedIPsOrCIDRs(denied.IPRanges), + policy.WithExcludedEmailAddresses(denied.EmailAddresses), + policy.WithExcludedPrincipals(denied.Principals), + ) + } + + // ensure no policy engine is returned when no name options were provided + if len(options) == 0 { + return nil, nil + } + + return policy.New(options...) +} diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 05d16e7f..2d5f74ff 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -7,8 +7,8 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" ) // ACME is the acme provisioner type, an entity that can authorize the ACME @@ -27,7 +27,7 @@ type ACME struct { Claims *Claims `json:"claims,omitempty"` Options *Options `json:"options,omitempty"` claimer *Claimer - x509Policy policy.X509NamePolicyEngine + x509Policy policy.X509Policy } // GetID returns the provisioner unique identifier. @@ -92,7 +92,7 @@ func (p *ACME) Init(config Config) (err error) { // Initialize the x509 allow/deny policy engine // TODO(hs): ensure no race conditions happen when reloading settings and requesting certs? // TODO(hs): implement memoization strategy, so that reloading is not required when no changes were made to allow/deny? - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 2ff8ade9..81029b1d 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -17,6 +17,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -267,8 +268,8 @@ type AWS struct { claimer *Claimer config *awsConfig audiences Audiences - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. @@ -428,12 +429,12 @@ func (p *AWS) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index f010364c..9c596b11 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -13,6 +13,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -100,8 +101,8 @@ type Azure struct { config *azureConfig oidcConfig openIDConfiguration keyStore *keyStore - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. @@ -226,12 +227,12 @@ func (p *Azure) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index e56c0729..5f08f2f6 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -14,6 +14,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -92,8 +93,8 @@ type GCP struct { config *gcpConfig keyStore *keyStore audiences Audiences - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. The name should uniquely @@ -219,12 +220,12 @@ func (p *GCP) Init(config Config) error { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index a129a536..b1716233 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -7,6 +7,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -37,9 +38,9 @@ type JWK struct { Options *Options `json:"options,omitempty"` claimer *Claimer audiences Audiences - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // GetID returns the provisioner unique identifier. The name and credential id @@ -107,17 +108,17 @@ func (p *JWK) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index be55f114..7737c1cc 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -10,6 +10,7 @@ import ( "net/http" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" @@ -52,9 +53,9 @@ type K8sSA struct { audiences Audiences //kauthn kauthn.AuthenticationV1Interface pubKeys []interface{} - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // GetID returns the provisioner unique identifier. The name and credential id @@ -148,17 +149,17 @@ func (p *K8sSA) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index f8027de9..a9bfab9f 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" nebula "github.com/slackhq/nebula/cert" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -43,9 +44,9 @@ type Nebula struct { claimer *Claimer caPool *nebula.NebulaCAPool audiences Audiences - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // Init verifies and initializes the Nebula provisioner. @@ -72,17 +73,17 @@ func (p *Nebula) Init(config Config) error { p.audiences = config.Audiences.WithFragment(p.GetIDForToken()) // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 60bb5cf1..e3c8740a 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -12,6 +12,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -94,9 +95,9 @@ type OIDC struct { keyStore *keyStore claimer *Claimer getIdentityFunc GetIdentityFunc - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } func sanitizeEmail(email string) string { @@ -212,17 +213,17 @@ func (o *OIDC) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if o.x509Policy, err = newX509PolicyEngine(o.Options.GetX509Options()); err != nil { + if o.x509Policy, err = policy.NewX509PolicyEngine(o.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if o.sshUserPolicy, err = newSSHUserPolicyEngine(o.Options.GetSSHOptions()); err != nil { + if o.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(o.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if o.sshHostPolicy, err = newSSHHostPolicyEngine(o.Options.GetSSHOptions()); err != nil { + if o.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(o.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 257a2107..7725c8b0 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -5,8 +5,11 @@ import ( "strings" "github.com/pkg/errors" + "go.step.sm/crypto/jose" "go.step.sm/crypto/x509util" + + "github.com/smallstep/certificates/authority/policy" ) // CertificateOptions is an interface that returns a list of options passed when @@ -58,10 +61,10 @@ type X509Options struct { TemplateData json.RawMessage `json:"templateData,omitempty"` // AllowedNames contains the SANs the provisioner is authorized to sign - AllowedNames *X509NameOptions `json:"allow,omitempty"` + AllowedNames *policy.X509NameOptions // DeniedNames contains the SANs the provisioner is not authorized to sign - DeniedNames *X509NameOptions `json:"deny,omitempty"` + DeniedNames *policy.X509NameOptions } // HasTemplate returns true if a template is defined in the provisioner options. @@ -69,41 +72,24 @@ func (o *X509Options) HasTemplate() bool { return o != nil && (o.Template != "" || o.TemplateFile != "") } -// GetAllowedNameOptions returns the AllowedNameOptions, which models the +// GetAllowedNameOptions returns the AllowedNames, which models the // SANs that a provisioner is authorized to sign x509 certificates for. -func (o *X509Options) GetAllowedNameOptions() *X509NameOptions { +func (o *X509Options) GetAllowedNameOptions() *policy.X509NameOptions { if o == nil { return nil } return o.AllowedNames } -// GetDeniedNameOptions returns the DeniedNameOptions, which models the +// GetDeniedNameOptions returns the DeniedNames, which models the // SANs that a provisioner is NOT authorized to sign x509 certificates for. -func (o *X509Options) GetDeniedNameOptions() *X509NameOptions { +func (o *X509Options) GetDeniedNameOptions() *policy.X509NameOptions { if o == nil { return nil } return o.DeniedNames } -// X509NameOptions models the X509 name policy configuration. -type X509NameOptions struct { - DNSDomains []string `json:"dns,omitempty"` - IPRanges []string `json:"ip,omitempty"` - EmailAddresses []string `json:"email,omitempty"` - URIDomains []string `json:"uri,omitempty"` -} - -// HasNames checks if the AllowedNameOptions has one or more -// names configured. -func (o *X509NameOptions) HasNames() bool { - return len(o.DNSDomains) > 0 || - len(o.IPRanges) > 0 || - len(o.EmailAddresses) > 0 || - len(o.URIDomains) > 0 -} - // TemplateOptions generates a CertificateOptions with the template and data // defined in the ProvisionerOptions, the provisioner generated data, and the // user data provided in the request. If no template has been provided, diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go deleted file mode 100644 index b9740e39..00000000 --- a/authority/provisioner/policy.go +++ /dev/null @@ -1,156 +0,0 @@ -package provisioner - -import ( - "fmt" - - "github.com/smallstep/certificates/policy" - "golang.org/x/crypto/ssh" -) - -type sshPolicyEngineType string - -const ( - userPolicyEngineType sshPolicyEngineType = "user" - hostPolicyEngineType sshPolicyEngineType = "host" -) - -var certTypeToPolicyEngineType = map[uint32]sshPolicyEngineType{ - uint32(ssh.UserCert): userPolicyEngineType, - uint32(ssh.HostCert): hostPolicyEngineType, -} - -type x509PolicyEngine interface { - policy.X509NamePolicyEngine -} - -type userPolicyEngine struct { - policy.SSHNamePolicyEngine -} - -type hostPolicyEngine struct { - policy.SSHNamePolicyEngine -} - -// newX509PolicyEngine creates a new x509 name policy engine -func newX509PolicyEngine(x509Opts *X509Options) (x509PolicyEngine, error) { - - if x509Opts == nil { - return nil, nil - } - - options := []policy.NamePolicyOption{ - policy.WithSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default - } - - allowed := x509Opts.GetAllowedNameOptions() - if allowed != nil && allowed.HasNames() { - options = append(options, - policy.WithPermittedDNSDomains(allowed.DNSDomains), - policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), - policy.WithPermittedEmailAddresses(allowed.EmailAddresses), - policy.WithPermittedURIDomains(allowed.URIDomains), - ) - } - - denied := x509Opts.GetDeniedNameOptions() - if denied != nil && denied.HasNames() { - options = append(options, - policy.WithExcludedDNSDomains(denied.DNSDomains), - policy.WithExcludedIPsOrCIDRs(denied.IPRanges), - policy.WithExcludedEmailAddresses(denied.EmailAddresses), - policy.WithExcludedURIDomains(denied.URIDomains), - ) - } - - return policy.New(options...) -} - -// newSSHUserPolicyEngine creates a new SSH user certificate policy engine -func newSSHUserPolicyEngine(sshOpts *SSHOptions) (*userPolicyEngine, error) { - policyEngine, err := newSSHPolicyEngine(sshOpts, userPolicyEngineType) - if err != nil { - return nil, err - } - // ensure we're not wrapping a nil engine - if policyEngine == nil { - return nil, nil - } - return &userPolicyEngine{ - SSHNamePolicyEngine: policyEngine, - }, nil -} - -// newSSHHostPolicyEngine create a new SSH host certificate policy engine -func newSSHHostPolicyEngine(sshOpts *SSHOptions) (*hostPolicyEngine, error) { - policyEngine, err := newSSHPolicyEngine(sshOpts, hostPolicyEngineType) - if err != nil { - return nil, err - } - // ensure we're not wrapping a nil engine - if policyEngine == nil { - return nil, nil - } - return &hostPolicyEngine{ - SSHNamePolicyEngine: policyEngine, - }, nil -} - -// newSSHPolicyEngine creates a new SSH name policy engine -func newSSHPolicyEngine(sshOpts *SSHOptions, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) { - - if sshOpts == nil { - return nil, nil - } - - var ( - allowed *SSHNameOptions - denied *SSHNameOptions - ) - - // TODO: embed the type in the policy engine itself for reference? - switch typ { - case userPolicyEngineType: - if sshOpts.User != nil { - allowed = sshOpts.User.GetAllowedNameOptions() - denied = sshOpts.User.GetDeniedNameOptions() - } - case hostPolicyEngineType: - if sshOpts.Host != nil { - allowed = sshOpts.Host.AllowedNames - denied = sshOpts.Host.DeniedNames - } - default: - return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ) - } - - options := []policy.NamePolicyOption{} - - if allowed != nil && allowed.HasNames() { - options = append(options, - policy.WithPermittedDNSDomains(allowed.DNSDomains), - policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), - policy.WithPermittedEmailAddresses(allowed.EmailAddresses), - policy.WithPermittedPrincipals(allowed.Principals), - ) - } - - if denied != nil && denied.HasNames() { - options = append(options, - policy.WithExcludedDNSDomains(denied.DNSDomains), - policy.WithExcludedIPsOrCIDRs(denied.IPRanges), - policy.WithExcludedEmailAddresses(denied.EmailAddresses), - policy.WithExcludedPrincipals(denied.Principals), - ) - } - - // Return nil, because there's no policy to execute. This is - // important, because the logic that determines user vs. host certs - // are allowed depends on this fact. The two policy engines are - // not aware of eachother, so this check is performed in the - // SSH name validator, instead. - if len(options) == 0 { - return nil, nil - } - - return policy.New(options...) -} diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index a9a06cae..9d02aebb 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -5,7 +5,7 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/policy" + "github.com/smallstep/certificates/authority/policy" ) // SCEP is the SCEP provisioner type, an entity that can authorize the @@ -31,7 +31,7 @@ type SCEP struct { Options *Options `json:"options,omitempty"` Claims *Claims `json:"claims,omitempty"` claimer *Claimer - x509Policy policy.X509NamePolicyEngine + x509Policy policy.X509Policy secretChallengePassword string encryptionAlgorithm int } @@ -116,7 +116,7 @@ func (s *SCEP) Init(config Config) (err error) { // TODO: add other, SCEP specific, options? // Initialize the x509 allow/deny policy engine - if s.x509Policy, err = newX509PolicyEngine(s.Options.GetX509Options()); err != nil { + if s.x509Policy, err = policy.NewX509PolicyEngine(s.Options.GetX509Options()); err != nil { return err } diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index 3327310b..082d765d 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -15,6 +15,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" @@ -407,18 +408,17 @@ func (v *validityValidator) Valid(cert *x509.Certificate, o SignOptions) error { // x509NamePolicyValidator validates that the certificate (to be signed) // contains only allowed SANs. type x509NamePolicyValidator struct { - policyEngine x509PolicyEngine + policyEngine policy.X509Policy } // newX509NamePolicyValidator return a new SANs allow/deny validator. -func newX509NamePolicyValidator(engine x509PolicyEngine) *x509NamePolicyValidator { +func newX509NamePolicyValidator(engine policy.X509Policy) *x509NamePolicyValidator { return &x509NamePolicyValidator{ policyEngine: engine, } } -// Valid validates validates that the certificate (to be signed) -// contains only allowed SANs. +// Valid validates that the certificate (to be signed) contains only allowed SANs. func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) error { if v.policyEngine == nil { return nil diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index 8f9cf466..a057b2b9 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -10,6 +10,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/keyutil" "golang.org/x/crypto/ssh" @@ -448,20 +449,19 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SignSSHOpti // sshNamePolicyValidator validates that the certificate (to be signed) // contains only allowed principals. type sshNamePolicyValidator struct { - hostPolicyEngine *hostPolicyEngine - userPolicyEngine *userPolicyEngine + hostPolicyEngine policy.HostPolicy + userPolicyEngine policy.UserPolicy } // newSSHNamePolicyValidator return a new SSH allow/deny validator. -func newSSHNamePolicyValidator(host *hostPolicyEngine, user *userPolicyEngine) *sshNamePolicyValidator { +func newSSHNamePolicyValidator(host policy.HostPolicy, user policy.UserPolicy) *sshNamePolicyValidator { return &sshNamePolicyValidator{ hostPolicyEngine: host, userPolicyEngine: user, } } -// Valid validates validates that the certificate (to be signed) -// contains only allowed principals. +// Valid validates that the certificate (to be signed) contains only allowed principals. func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error { if v.hostPolicyEngine == nil && v.userPolicyEngine == nil { // no policy configured at all; allow anything @@ -473,29 +473,25 @@ func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) // the same for host certs: if only a user policy engine is configured, host // certs are denied. When both policy engines are configured, the type of // cert determines which policy engine is used. - policyType, ok := certTypeToPolicyEngineType[cert.CertType] - if !ok { - return fmt.Errorf("unexpected SSH cert type %d", cert.CertType) - } - switch policyType { - case hostPolicyEngineType: + switch cert.CertType { + case ssh.HostCert: // when no host policy engine is configured, but a user policy engine is - // configured, we don't allow the host certificate. + // configured, the host certificate is denied. if v.hostPolicyEngine == nil && v.userPolicyEngine != nil { - return errors.New("SSH host certificate not authorized") // TODO: include principals in message? + return errors.New("SSH host certificate not authorized") } _, err := v.hostPolicyEngine.ArePrincipalsAllowed(cert) return err - case userPolicyEngineType: + case ssh.UserCert: // when no user policy engine is configured, but a host policy engine is - // configured, we don't allow the user certificate. + // configured, the user certificate is denied. if v.userPolicyEngine == nil && v.hostPolicyEngine != nil { - return errors.New("SSH user certificate not authorized") // TODO: include principals in message? + return errors.New("SSH user certificate not authorized") } _, err := v.userPolicyEngine.ArePrincipalsAllowed(cert) return err default: - return fmt.Errorf("unexpected policy engine type %q", policyType) // satisfy return; shouldn't happen + return fmt.Errorf("unexpected SSH certificate type %d", cert.CertType) // satisfy return; shouldn't happen } } diff --git a/authority/provisioner/ssh_options.go b/authority/provisioner/ssh_options.go index dacafc80..92c5826b 100644 --- a/authority/provisioner/ssh_options.go +++ b/authority/provisioner/ssh_options.go @@ -6,6 +6,8 @@ import ( "github.com/pkg/errors" "go.step.sm/crypto/sshutil" + + "github.com/smallstep/certificates/authority/policy" ) // SSHCertificateOptions is an interface that returns a list of options passed when @@ -35,32 +37,58 @@ type SSHOptions struct { TemplateData json.RawMessage `json:"templateData,omitempty"` // User contains SSH user certificate options. - User *SSHUserCertificateOptions `json:"user,omitempty"` + User *policy.SSHUserCertificateOptions // Host contains SSH host certificate options. - Host *SSHHostCertificateOptions `json:"host,omitempty"` + Host *policy.SSHHostCertificateOptions } -// SSHUserCertificateOptions is a collection of SSH user certificate options. -type SSHUserCertificateOptions struct { - // AllowedNames contains the names the provisioner is authorized to sign - AllowedNames *SSHNameOptions `json:"allow,omitempty"` - - // DeniedNames contains the names the provisioner is not authorized to sign - DeniedNames *SSHNameOptions `json:"deny,omitempty"` +// GetAllowedUserNameOptions returns the SSHNameOptions that are +// allowed when SSH User certificates are requested. +func (o *SSHOptions) GetAllowedUserNameOptions() *policy.SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.AllowedNames } -// SSHHostCertificateOptions is a collection of SSH host certificate options. -// It's an alias of SSHUserCertificateOptions, as the options are the same -// for both types of certificates. -type SSHHostCertificateOptions SSHUserCertificateOptions +// GetDeniedUserNameOptions returns the SSHNameOptions that are +// denied when SSH user certificates are requested. +func (o *SSHOptions) GetDeniedUserNameOptions() *policy.SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.DeniedNames +} -// SSHNameOptions models the SSH name policy configuration. -type SSHNameOptions struct { - DNSDomains []string `json:"dns,omitempty"` - IPRanges []string `json:"ip,omitempty"` - EmailAddresses []string `json:"email,omitempty"` - Principals []string `json:"principal,omitempty"` +// GetAllowedHostNameOptions returns the SSHNameOptions that are +// allowed when SSH host certificates are requested. +func (o *SSHOptions) GetAllowedHostNameOptions() *policy.SSHNameOptions { + if o == nil { + return nil + } + if o.Host == nil { + return nil + } + return o.Host.AllowedNames +} + +// GetDeniedHostNameOptions returns the SSHNameOptions that are +// denied when SSH host certificates are requested. +func (o *SSHOptions) GetDeniedHostNameOptions() *policy.SSHNameOptions { + if o == nil { + return nil + } + if o.Host == nil { + return nil + } + return o.Host.DeniedNames } // HasTemplate returns true if a template is defined in the provisioner options. @@ -68,32 +96,6 @@ func (o *SSHOptions) HasTemplate() bool { return o != nil && (o.Template != "" || o.TemplateFile != "") } -// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the -// names that a provisioner is authorized to sign SSH certificates for. -func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - return o.AllowedNames -} - -// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the -// names that a provisioner is NOT authorized to sign SSH certificates for. -func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - return o.DeniedNames -} - -// HasNames checks if the SSHNameOptions has one or more -// names configured. -func (o *SSHNameOptions) HasNames() bool { - return len(o.DNSDomains) > 0 || - len(o.EmailAddresses) > 0 || - len(o.Principals) > 0 -} - // TemplateSSHOptions generates a SSHCertificateOptions with the template and // data defined in the ProvisionerOptions, the provisioner generated data, and // the user data provided in the request. If no template has been provided, diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 12112cc6..a8275474 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -8,6 +8,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -35,9 +36,9 @@ type X5C struct { claimer *Claimer audiences Audiences rootPool *x509.CertPool - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // GetID returns the provisioner unique identifier. The name and credential id @@ -129,17 +130,17 @@ func (p *X5C) Init(config Config) error { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/ssh.go b/authority/ssh.go index 4a67b28c..7c3df192 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" @@ -241,6 +242,45 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType) } + switch certTpl.CertType { + case ssh.UserCert: + // when no user policy engine is configured, but a host policy engine is + // configured, the user certificate is denied. + if a.sshUserPolicy == nil && a.sshHostPolicy != nil { + return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign ssh user certificates"), "authority.SignSSH: error creating ssh user certificate") + } + if a.sshUserPolicy != nil { + allowed, err := a.sshUserPolicy.ArePrincipalsAllowed(certTpl) + if err != nil { + return nil, errs.InternalServerErr(err, + errs.WithMessage("authority.SignSSH: error creating ssh user certificate"), + ) + } + if !allowed { + return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: error creating ssh user certificate") + } + } + case ssh.HostCert: + // when no host policy engine is configured, but a user policy engine is + // configured, the host certificate is denied. + if a.sshHostPolicy == nil && a.sshUserPolicy != nil { + return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign ssh host certificates"), "authority.SignSSH: error creating ssh user certificate") + } + if a.sshHostPolicy != nil { + allowed, err := a.sshHostPolicy.ArePrincipalsAllowed(certTpl) + if err != nil { + return nil, errs.InternalServerErr(err, + errs.WithMessage("authority.SignSSH: error creating ssh host certificate"), + ) + } + if !allowed { + return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: error creating ssh host certificate") + } + } + default: + return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType) + } + // Sign certificate. cert, err := sshutil.CreateCertificate(certTpl, signer) if err != nil { diff --git a/authority/tls.go b/authority/tls.go index 58a1247c..d749e2ad 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -191,6 +191,25 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } } + // If a policy is configured, perform allow/deny policy check on authority level + if a.x509Policy != nil { + allowed, err := a.x509Policy.AreCertificateNamesAllowed(leaf) + if err != nil { + return nil, errs.InternalServerErr(err, + errs.WithKeyVal("csr", csr), + errs.WithKeyVal("signOptions", signOpts), + errs.WithMessage("error creating certificate"), + ) + } + 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 lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ From 3ec9a7310cf87bb0d576bc76f0ed6f65037cb8d2 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 8 Mar 2022 14:17:59 +0100 Subject: [PATCH 14/78] Fix ACME order identifier allow/deny check --- acme/api/order.go | 4 +++- acme/common.go | 6 +++--- authority/provisioner/acme.go | 18 +++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/acme/api/order.go b/acme/api/order.go index 3d22ec0f..e1adebb3 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -13,6 +13,7 @@ import ( "github.com/go-chi/chi" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/randutil" ) @@ -107,7 +108,8 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { 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? - err = prov.AuthorizeOrderIdentifier(ctx, identifier.Value) + orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value} + err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier) if err != nil { api.WriteError(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) return diff --git a/acme/common.go b/acme/common.go index 4b086dd7..9c5e732a 100644 --- a/acme/common.go +++ b/acme/common.go @@ -30,7 +30,7 @@ var clock Clock // Provisioner is an interface that implements a subset of the provisioner.Interface -- // only those methods required by the ACME api/authority. type Provisioner interface { - AuthorizeOrderIdentifier(ctx context.Context, identifier string) error + AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) AuthorizeRevoke(ctx context.Context, token string) error GetID() string @@ -45,7 +45,7 @@ type MockProvisioner struct { Merr error MgetID func() string MgetName func() string - MauthorizeOrderIdentifier func(ctx context.Context, identifier string) error + MauthorizeOrderIdentifier func(ctx context.Context, identifier provisioner.ACMEIdentifier) error MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error) MauthorizeRevoke func(ctx context.Context, token string) error MdefaultTLSCertDuration func() time.Duration @@ -61,7 +61,7 @@ func (m *MockProvisioner) GetName() string { } // AuthorizeOrderIdentifiers mock -func (m *MockProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier string) error { +func (m *MockProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error { if m.MauthorizeOrderIdentifier != nil { return m.MauthorizeOrderIdentifier(ctx, identifier) } diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 2d5f74ff..9f8ef690 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -90,8 +90,6 @@ func (p *ACME) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - // TODO(hs): ensure no race conditions happen when reloading settings and requesting certs? - // TODO(hs): implement memoization strategy, so that reloading is not required when no changes were made to allow/deny? if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } @@ -115,20 +113,22 @@ type ACMEIdentifier struct { Value string } -// AuthorizeOrderIdentifiers verifies the provisioner is authorized to issue a -// certificate for the Identifiers provided in an Order. -func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier string) error { +// AuthorizeOrderIdentifier verifies the provisioner is allowed to issue a +// certificate for an ACME Order Identifier. +func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIdentifier) error { + // identifier is allowed if no policy is configured if p.x509Policy == nil { return nil } // assuming only valid identifiers (IP or DNS) are provided var err error - if ip := net.ParseIP(identifier); ip != nil { - _, err = p.x509Policy.IsIPAllowed(ip) - } else { - _, err = p.x509Policy.IsDNSAllowed(identifier) + switch identifier.Type { + case IP: + _, err = p.x509Policy.IsIPAllowed(net.ParseIP(identifier.Value)) + case DNS: + _, err = p.x509Policy.IsDNSAllowed(identifier.Value) } return err From 81b0c6c37c6129930ffb659cb8758952a7834e91 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 15 Mar 2022 15:51:45 +0100 Subject: [PATCH 15/78] Add API implementation for authority and provisioner policy --- api/utils.go | 7 + authority/admin/api/admin.go | 4 + authority/admin/api/admin_test.go | 21 ++ authority/admin/api/handler.go | 52 +++- authority/admin/api/middleware.go | 23 ++ authority/admin/api/policy.go | 313 ++++++++++++++++++++++++ authority/admin/db.go | 37 +++ authority/admin/db/nosql/nosql.go | 7 +- authority/admin/db/nosql/policy.go | 144 +++++++++++ authority/admin/db/nosql/provisioner.go | 4 + authority/authority.go | 61 +++-- authority/linkedca.go | 20 ++ authority/policy.go | 132 ++++++++++ authority/policy/options.go | 34 ++- authority/provisioners.go | 53 ++++ authority/tls.go | 5 +- ca/ca.go | 3 +- go.mod | 2 +- go.sum | 4 - 19 files changed, 883 insertions(+), 43 deletions(-) create mode 100644 authority/admin/api/policy.go create mode 100644 authority/admin/db/nosql/policy.go create mode 100644 authority/policy.go diff --git a/api/utils.go b/api/utils.go index a7f4bf58..b6ff7960 100644 --- a/api/utils.go +++ b/api/utils.go @@ -66,6 +66,13 @@ func JSONStatus(w http.ResponseWriter, v interface{}, status int) { LogEnabledResponse(w, v) } +// JSONNotFound writes a HTTP Not Found response with empty body. +func JSONNotFound(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + LogEnabledResponse(w, nil) +} + // ProtoJSON writes the passed value into the http.ResponseWriter. func ProtoJSON(w http.ResponseWriter, m proto.Message) { ProtoJSONStatus(w, m, http.StatusOK) diff --git a/authority/admin/api/admin.go b/authority/admin/api/admin.go index 7aa66d0f..dd40784b 100644 --- a/authority/admin/api/admin.go +++ b/authority/admin/api/admin.go @@ -25,6 +25,10 @@ type adminAuthority interface { LoadProvisionerByID(id string) (provisioner.Interface, error) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error RemoveProvisioner(ctx context.Context, id string) error + GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) + StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error + UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error + RemoveAuthorityPolicy(ctx context.Context) error } // CreateAdminRequest represents the body for a CreateAdmin request. diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go index 8d223b52..f1698139 100644 --- a/authority/admin/api/admin_test.go +++ b/authority/admin/api/admin_test.go @@ -37,6 +37,11 @@ type mockAdminAuthority struct { MockLoadProvisionerByID func(id string) (provisioner.Interface, error) MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error MockRemoveProvisioner func(ctx context.Context, id string) error + + MockGetAuthorityPolicy func(ctx context.Context) (*linkedca.Policy, error) + MockStoreAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error + MockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error + MockRemoveAuthorityPolicy func(ctx context.Context) error } func (m *mockAdminAuthority) IsAdminAPIEnabled() bool { @@ -130,6 +135,22 @@ func (m *mockAdminAuthority) RemoveProvisioner(ctx context.Context, id string) e return m.MockErr } +func (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { + return nil, errors.New("not implemented yet") +} + +func (m *mockAdminAuthority) StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + return errors.New("not implemented yet") +} + +func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + return errors.New("not implemented yet") +} + +func (m *mockAdminAuthority) RemoveAuthorityPolicy(ctx context.Context) error { + return errors.New("not implemented yet") +} + func TestCreateAdminRequest_Validate(t *testing.T) { type fields struct { Subject string diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 99e74c88..e59b95e0 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -8,32 +8,44 @@ import ( // Handler is the Admin API request handler. type Handler struct { - adminDB admin.DB - auth adminAuthority - acmeDB acme.DB - acmeResponder acmeAdminResponderInterface + adminDB admin.DB + auth adminAuthority + acmeDB acme.DB + acmeResponder acmeAdminResponderInterface + policyResponder policyAdminResponderInterface } // NewHandler returns a new Authority Config Handler. -func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface) api.RouterHandler { +func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface, policyResponder policyAdminResponderInterface) api.RouterHandler { return &Handler{ - auth: auth, - adminDB: adminDB, - acmeDB: acmeDB, - acmeResponder: acmeResponder, + auth: auth, + adminDB: adminDB, + acmeDB: acmeDB, + acmeResponder: acmeResponder, + policyResponder: policyResponder, } } // Route traffic and implement the Router interface. func (h *Handler) Route(r api.Router) { + 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 { return h.requireEABEnabled(next) } + enabledInStandalone := func(next nextHTTP) nextHTTP { + return h.checkAction(next, true) + } + + disabledInStandalone := func(next nextHTTP) nextHTTP { + return h.checkAction(next, false) + } + // Provisioners r.MethodFunc("GET", "/provisioners/{name}", authnz(h.GetProvisioner)) r.MethodFunc("GET", "/provisioners", authnz(h.GetProvisioners)) @@ -53,4 +65,24 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys))) r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey))) r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey))) + + // Policy - Authority + r.MethodFunc("GET", "/policy", authnz(enabledInStandalone(h.policyResponder.GetAuthorityPolicy))) + r.MethodFunc("POST", "/policy", authnz(enabledInStandalone(h.policyResponder.CreateAuthorityPolicy))) + r.MethodFunc("PUT", "/policy", authnz(enabledInStandalone(h.policyResponder.UpdateAuthorityPolicy))) + r.MethodFunc("DELETE", "/policy", authnz(enabledInStandalone(h.policyResponder.DeleteAuthorityPolicy))) + + // Policy - Provisioner + //r.MethodFunc("GET", "/provisioners/{name}/policy", noauth(h.policyResponder.GetProvisionerPolicy)) + r.MethodFunc("GET", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.GetProvisionerPolicy))) + r.MethodFunc("POST", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.CreateProvisionerPolicy))) + r.MethodFunc("PUT", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.UpdateProvisionerPolicy))) + r.MethodFunc("DELETE", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.DeleteProvisionerPolicy))) + + // Policy - ACME Account + // TODO: ensure we don't clash with eab; might want to change eab paths slightly (as long as we don't have it released completely; needs changes in adminClient too) + r.MethodFunc("GET", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.GetACMEAccountPolicy))) + r.MethodFunc("POST", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.CreateACMEAccountPolicy))) + r.MethodFunc("PUT", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.UpdateACMEAccountPolicy))) + r.MethodFunc("DELETE", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.DeleteACMEAccountPolicy))) } diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 19025a9d..62aefdc3 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -6,6 +6,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/admin/db/nosql" ) type nextHTTP = func(http.ResponseWriter, *http.Request) @@ -44,6 +45,28 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { } } +// checkAction checks if an action is supported in standalone or not +func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTTP { + return func(w http.ResponseWriter, r *http.Request) { + + // actions allowed in standalone mode are always allowed + if supportedInStandalone { + next(w, r) + return + } + + // when in standalone mode, actions are not supported + if _, ok := h.adminDB.(*nosql.DB); ok { + api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, + "operation not supported in standalone mode")) + return + } + + // continue to next http handler + next(w, r) + } +} + // ContextKey is the key type for storing and searching for ACME request // essentials in the context of a request. type ContextKey string diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go new file mode 100644 index 00000000..c318e5e5 --- /dev/null +++ b/authority/admin/api/policy.go @@ -0,0 +1,313 @@ +package api + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" +) + +type policyAdminResponderInterface interface { + GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) + CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) + UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) + DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) + GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) + CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) + UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) + DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) + GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) + CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) + UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) + DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) +} + +// PolicyAdminResponder is responsible for writing ACME admin responses +type PolicyAdminResponder struct { + auth adminAuthority + adminDB admin.DB +} + +// NewACMEAdminResponder returns a new ACMEAdminResponder +func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB) *PolicyAdminResponder { + return &PolicyAdminResponder{ + auth: auth, + adminDB: adminDB, + } +} + +// GetAuthorityPolicy handles the GET /admin/authority/policy request +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) { + api.WriteError(w, admin.WrapErrorISE(ae, "error retrieving authority policy")) + return + } + } + + if policy == nil { + api.JSONNotFound(w) + return + } + + api.ProtoJSONStatus(w, policy, http.StatusOK) +} + +// CreateAuthorityPolicy handles the POST /admin/authority/policy request +func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + + 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 { + adminErr := admin.NewError(admin.ErrorBadRequestType, "authority already has a policy") + adminErr.Status = http.StatusConflict + api.WriteError(w, adminErr) + return + } + + var newPolicy = new(linkedca.Policy) + if err := api.ReadProtoJSON(r.Body, newPolicy); err != nil { + api.WriteError(w, err) + return + } + + if err := par.auth.StoreAuthorityPolicy(ctx, newPolicy); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error storing authority policy")) + return + } + + storedPolicy, err := par.auth.GetAuthorityPolicy(ctx) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating")) + return + } + + api.JSONStatus(w, storedPolicy, http.StatusCreated) +} + +// UpdateAuthorityPolicy handles the PUT /admin/authority/policy 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 { + api.WriteError(w, err) + return + } + + ctx := r.Context() + if err := par.auth.UpdateAuthorityPolicy(ctx, policy); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error updating authority policy")) + return + } + + newPolicy, err := par.auth.GetAuthorityPolicy(ctx) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating")) + return + } + + api.ProtoJSONStatus(w, newPolicy, http.StatusOK) +} + +// DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request +func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + policy, err := par.auth.GetAuthorityPolicy(ctx) + + if ae, ok := err.(*admin.Error); ok { + if !ae.IsType(admin.ErrorNotFoundType) { + api.WriteError(w, admin.WrapErrorISE(ae, "error retrieving authority policy")) + return + } + } + + if policy == nil { + api.JSONNotFound(w) + return + } + + err = par.auth.RemoveAuthorityPolicy(ctx) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error deleting authority policy")) + return + } + + api.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) +} + +// GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request +func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + // TODO: move getting provisioner to middleware? + ctx := r.Context() + name := chi.URLParam(r, "name") + var ( + p provisioner.Interface + err error + ) + if p, err = par.auth.LoadProvisionerByName(name); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) + return + } + + prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) + if err != nil { + api.WriteError(w, err) + return + } + + policy := prov.GetPolicy() + if policy == nil { + api.JSONNotFound(w) + return + } + + api.ProtoJSONStatus(w, policy, http.StatusOK) +} + +// CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request +func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + name := chi.URLParam(r, "name") + var ( + p provisioner.Interface + err error + ) + if p, err = par.auth.LoadProvisionerByName(name); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) + return + } + + prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) + if err != nil { + api.WriteError(w, err) + return + } + + policy := prov.GetPolicy() + if policy != nil { + adminErr := admin.NewError(admin.ErrorBadRequestType, "provisioner %s already has a policy", name) + adminErr.Status = http.StatusConflict + api.WriteError(w, adminErr) + } + + var newPolicy = new(linkedca.Policy) + if err := api.ReadProtoJSON(r.Body, newPolicy); err != nil { + api.WriteError(w, err) + return + } + + prov.Policy = newPolicy + + err = par.auth.UpdateProvisioner(ctx, prov) + if err != nil { + api.WriteError(w, err) + return + } + + api.ProtoJSONStatus(w, newPolicy, http.StatusCreated) +} + +// UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request +func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + name := chi.URLParam(r, "name") + var ( + p provisioner.Interface + err error + ) + if p, err = par.auth.LoadProvisionerByName(name); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) + return + } + + prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) + if err != nil { + api.WriteError(w, err) + return + } + + var policy = new(linkedca.Policy) + if err := api.ReadProtoJSON(r.Body, policy); err != nil { + api.WriteError(w, err) + return + } + + prov.Policy = policy + err = par.auth.UpdateProvisioner(ctx, prov) + if err != nil { + api.WriteError(w, err) + return + } + + api.ProtoJSONStatus(w, policy, http.StatusOK) +} + +// DeleteProvisionerPolicy ... +func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + name := chi.URLParam(r, "name") + var ( + p provisioner.Interface + err error + ) + if p, err = par.auth.LoadProvisionerByName(name); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) + return + } + + prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) + if err != nil { + api.WriteError(w, err) + return + } + + if prov.Policy == nil { + api.JSONNotFound(w) + return + } + + // remove the policy + prov.Policy = nil + + err = par.auth.UpdateProvisioner(ctx, prov) + if err != nil { + api.WriteError(w, err) + return + } + + api.JSON(w, &DeleteResponse{Status: "ok"}) +} + +// GetACMEAccountPolicy ... +func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + api.JSON(w, "ok") +} + +func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + api.JSON(w, "ok") +} + +func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + api.JSON(w, "ok") +} + +func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + api.JSON(w, "ok") +} diff --git a/authority/admin/db.go b/authority/admin/db.go index bf34a3c2..75ac1368 100644 --- a/authority/admin/db.go +++ b/authority/admin/db.go @@ -69,6 +69,11 @@ type DB interface { GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error DeleteAdmin(ctx context.Context, id string) error + + CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error + GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) + UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error + DeleteAuthorityPolicy(ctx context.Context) error } // MockDB is an implementation of the DB interface that should only be used as @@ -86,6 +91,11 @@ type MockDB struct { MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error MockDeleteAdmin func(ctx context.Context, id string) error + MockCreateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error + MockGetAuthorityPolicy func(ctx context.Context) (*linkedca.Policy, error) + MockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error + MockDeleteAuthorityPolicy func(ctx context.Context) error + MockError error MockRet1 interface{} } @@ -179,3 +189,30 @@ func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error { } return m.MockError } + +func (m *MockDB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + if m.MockCreateAuthorityPolicy != nil { + return m.MockCreateAuthorityPolicy(ctx, policy) + } + return m.MockError +} +func (m *MockDB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { + if m.MockGetAuthorityPolicy != nil { + return m.MockGetAuthorityPolicy(ctx) + } + return m.MockRet1.(*linkedca.Policy), m.MockError +} + +func (m *MockDB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + if m.MockUpdateAuthorityPolicy != nil { + return m.MockUpdateAuthorityPolicy(ctx, policy) + } + return m.MockError +} + +func (m *MockDB) DeleteAuthorityPolicy(ctx context.Context) error { + if m.MockDeleteAuthorityPolicy != nil { + return m.MockDeleteAuthorityPolicy(ctx) + } + return m.MockError +} diff --git a/authority/admin/db/nosql/nosql.go b/authority/admin/db/nosql/nosql.go index 22b049f5..32e05d92 100644 --- a/authority/admin/db/nosql/nosql.go +++ b/authority/admin/db/nosql/nosql.go @@ -11,8 +11,9 @@ import ( ) var ( - adminsTable = []byte("admins") - provisionersTable = []byte("provisioners") + adminsTable = []byte("admins") + provisionersTable = []byte("provisioners") + authorityPoliciesTable = []byte("authority_policies") ) // DB is a struct that implements the AdminDB interface. @@ -23,7 +24,7 @@ type DB struct { // New configures and returns a new Authority DB backend implemented using a nosql DB. func New(db nosqlDB.DB, authorityID string) (*DB, error) { - tables := [][]byte{adminsTable, provisionersTable} + tables := [][]byte{adminsTable, provisionersTable, authorityPoliciesTable} for _, b := range tables { if err := db.CreateTable(b); err != nil { return nil, errors.Wrapf(err, "error creating table %s", diff --git a/authority/admin/db/nosql/policy.go b/authority/admin/db/nosql/policy.go new file mode 100644 index 00000000..94ff2a0e --- /dev/null +++ b/authority/admin/db/nosql/policy.go @@ -0,0 +1,144 @@ +package nosql + +import ( + "context" + "encoding/json" + + "github.com/pkg/errors" + + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/nosql" + "go.step.sm/linkedca" +) + +type dbAuthorityPolicy struct { + ID string `json:"id"` + AuthorityID string `json:"authorityID"` + Policy *linkedca.Policy `json:"policy"` +} + +func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy { + return dbap.Policy +} + +func (dbap *dbAuthorityPolicy) clone() *dbAuthorityPolicy { + u := *dbap + return &u +} + +func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) { + data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID)) + if nosql.IsErrNotFound(err) { + return nil, admin.NewError(admin.ErrorNotFoundType, "policy %s not found", authorityID) + } else if err != nil { + return nil, errors.Wrapf(err, "error loading admin %s", authorityID) + } + return data, nil +} + +func (db *DB) unmarshalDBAuthorityPolicy(data []byte, authorityID string) (*dbAuthorityPolicy, error) { + var dba = new(dbAuthorityPolicy) + if err := json.Unmarshal(data, dba); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", authorityID) + } + // if !dba.DeletedAt.IsZero() { + // return nil, admin.NewError(admin.ErrorDeletedType, "admin %s is deleted", authorityID) + // } + if dba.AuthorityID != db.authorityID { + return nil, admin.NewError(admin.ErrorAuthorityMismatchType, + "admin %s is not owned by authority %s", dba.ID, db.authorityID) + } + return dba, nil +} + +func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*dbAuthorityPolicy, error) { + data, err := db.getDBAuthorityPolicyBytes(ctx, authorityID) + if err != nil { + return nil, err + } + dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID) + if err != nil { + return nil, err + } + return dbap, nil +} + +func (db *DB) unmarshalAuthorityPolicy(data []byte, authorityID string) (*linkedca.Policy, error) { + dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID) + if err != nil { + return nil, err + } + return dbap.convert(), nil +} + +func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + + dbap := &dbAuthorityPolicy{ + ID: db.authorityID, + AuthorityID: db.authorityID, + Policy: policy, + } + + old, err := db.getDBAuthorityPolicy(ctx, db.authorityID) + if err != nil { + return err + } + + return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable) +} + +func (db *DB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { + // policy := &linkedca.Policy{ + // X509: &linkedca.X509Policy{ + // Allow: &linkedca.X509Names{ + // Dns: []string{".localhost"}, + // }, + // Deny: &linkedca.X509Names{ + // Dns: []string{"denied.localhost"}, + // }, + // }, + // Ssh: &linkedca.SSHPolicy{ + // User: &linkedca.SSHUserPolicy{ + // Allow: &linkedca.SSHUserNames{}, + // Deny: &linkedca.SSHUserNames{}, + // }, + // Host: &linkedca.SSHHostPolicy{ + // Allow: &linkedca.SSHHostNames{}, + // Deny: &linkedca.SSHHostNames{}, + // }, + // }, + // } + + dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID) + if err != nil { + return nil, err + } + + return dbap.convert(), nil +} + +func (db *DB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + old, err := db.getDBAuthorityPolicy(ctx, db.authorityID) + if err != nil { + return err + } + + dbap := &dbAuthorityPolicy{ + ID: db.authorityID, + AuthorityID: db.authorityID, + Policy: policy, + } + + return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable) +} + +func (db *DB) DeleteAuthorityPolicy(ctx context.Context) error { + dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID) + if err != nil { + return err + } + old := dbap.clone() + + dbap.Policy = nil + return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable) +} diff --git a/authority/admin/db/nosql/provisioner.go b/authority/admin/db/nosql/provisioner.go index 71d9c8d6..540e3ae2 100644 --- a/authority/admin/db/nosql/provisioner.go +++ b/authority/admin/db/nosql/provisioner.go @@ -19,6 +19,7 @@ type dbProvisioner struct { Type linkedca.Provisioner_Type `json:"type"` Name string `json:"name"` Claims *linkedca.Claims `json:"claims"` + Policy *linkedca.Policy `json:"policy"` Details []byte `json:"details"` X509Template *linkedca.Template `json:"x509Template"` SSHTemplate *linkedca.Template `json:"sshTemplate"` @@ -43,6 +44,7 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) { Type: dbp.Type, Name: dbp.Name, Claims: dbp.Claims, + Policy: dbp.Policy, Details: details, X509Template: dbp.X509Template, SshTemplate: dbp.SSHTemplate, @@ -160,6 +162,7 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) Type: prov.Type, Name: prov.Name, Claims: prov.Claims, + Policy: prov.Policy, Details: details, X509Template: prov.X509Template, SSHTemplate: prov.SshTemplate, @@ -187,6 +190,7 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) } nu.Name = prov.Name nu.Claims = prov.Claims + nu.Policy = prov.Policy nu.Details, err = json.Marshal(prov.Details.GetData()) if err != nil { return admin.WrapErrorISE(err, "error marshaling details when updating provisioner %s", prov.Name) diff --git a/authority/authority.go b/authority/authority.go index 4eacfad7..aaf0e478 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -205,6 +205,47 @@ func (a *Authority) reloadAdminResources(ctx context.Context) error { a.provisioners = provClxn a.config.AuthorityConfig.Admins = adminList a.admins = adminClxn + + return nil +} + +// reloadPolicyEngines reloads x509 and SSH policy engines using +// configuration stored in the DB or from the configuration file. +func (a *Authority) reloadPolicyEngines(ctx context.Context) error { + var ( + err error + policyOptions *policy.Options + ) + if a.config.AuthorityConfig.EnableAdmin { + linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) + if err != nil { + return admin.WrapErrorISE(err, "error getting policy to initialize authority") + } + policyOptions = policyToCertificates(linkedPolicy) + } else { + policyOptions = a.config.AuthorityConfig.Policy + } + + // return early if no policy options set + if policyOptions == nil { + return nil + } + + // Initialize the x509 allow/deny policy engine + if a.x509Policy, err = policy.NewX509PolicyEngine(policyOptions.GetX509Options()); err != nil { + return err + } + + // // Initialize the SSH allow/deny policy engine for host certificates + if a.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(policyOptions.GetSSHOptions()); err != nil { + return err + } + + // // Initialize the SSH allow/deny policy engine for user certificates + if a.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(policyOptions.GetSSHOptions()); err != nil { + return err + } + return nil } @@ -533,6 +574,11 @@ func (a *Authority) init() error { return err } + // Load Policy Engines + if err := a.reloadPolicyEngines(context.Background()); err != nil { + return err + } + // Configure templates, currently only ssh templates are supported. if a.sshCAHostCertSignKey != nil || a.sshCAUserCertSignKey != nil { a.templates = a.config.Templates @@ -545,21 +591,6 @@ func (a *Authority) init() error { a.templates.Data["Step"] = tmplVars } - // Initialize the x509 allow/deny policy engine - if a.x509Policy, err = policy.NewX509PolicyEngine(a.config.AuthorityConfig.Policy.GetX509Options()); err != nil { - return err - } - - // // Initialize the SSH allow/deny policy engine for host certificates - if a.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(a.config.AuthorityConfig.Policy.GetSSHOptions()); err != nil { - return err - } - - // // Initialize the SSH allow/deny policy engine for user certificates - if a.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(a.config.AuthorityConfig.Policy.GetSSHOptions()); err != nil { - return err - } - // JWT numeric dates are seconds. a.startTime = time.Now().Truncate(time.Second) // Set flag indicating that initialization has been completed, and should diff --git a/authority/linkedca.go b/authority/linkedca.go index b568dcbb..11c8668c 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -15,6 +15,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/db" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" @@ -34,6 +35,9 @@ type linkedCaClient struct { authorityID string } +// interface guard +var _ admin.DB = (*linkedCaClient)(nil) + type linkedCAClaims struct { jose.Claims SANs []string `json:"sans"` @@ -310,6 +314,22 @@ func (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) { return resp.Status != linkedca.RevocationStatus_ACTIVE, nil } +func (c *linkedCaClient) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + return errors.New("not implemented yet") +} + +func (c *linkedCaClient) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { + return nil, errors.New("not implemented yet") +} + +func (c *linkedCaClient) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + return errors.New("not implemented yet") +} + +func (c *linkedCaClient) DeleteAuthorityPolicy(ctx context.Context) error { + return errors.New("not implemented yet") +} + func serializeCertificate(crt *x509.Certificate) string { if crt == nil { return "" diff --git a/authority/policy.go b/authority/policy.go new file mode 100644 index 00000000..8ef264d0 --- /dev/null +++ b/authority/policy.go @@ -0,0 +1,132 @@ +package authority + +import ( + "context" + + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/policy" + "go.step.sm/linkedca" +) + +func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { + a.adminMutex.Lock() + defer a.adminMutex.Unlock() + + policy, err := a.adminDB.GetAuthorityPolicy(ctx) + if err != nil { + return nil, err + } + + return policy, nil +} + +func (a *Authority) StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + a.adminMutex.Lock() + defer a.adminMutex.Unlock() + + if err := a.adminDB.CreateAuthorityPolicy(ctx, policy); err != nil { + return err + } + + if err := a.reloadPolicyEngines(ctx); err != nil { + return admin.WrapErrorISE(err, "error reloading admin resources when creating authority policy") + } + + return nil +} + +func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { + a.adminMutex.Lock() + defer a.adminMutex.Unlock() + + if err := a.adminDB.UpdateAuthorityPolicy(ctx, policy); err != nil { + return err + } + + if err := a.reloadPolicyEngines(ctx); err != nil { + return admin.WrapErrorISE(err, "error reloading admin resources when updating authority policy") + } + + return nil +} + +func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { + a.adminMutex.Lock() + defer a.adminMutex.Unlock() + + if err := a.adminDB.DeleteAuthorityPolicy(ctx); err != nil { + return err + } + + if err := a.reloadPolicyEngines(ctx); err != nil { + return admin.WrapErrorISE(err, "error reloading admin resources when deleting authority policy") + } + + return nil +} + +func policyToCertificates(p *linkedca.Policy) *policy.Options { + // return early + if p == nil { + return nil + } + // prepare full policy struct + opts := &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{}, + DeniedNames: &policy.X509NameOptions{}, + }, + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{}, + DeniedNames: &policy.SSHNameOptions{}, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{}, + DeniedNames: &policy.SSHNameOptions{}, + }, + }, + } + // fill x509 policy configuration + if p.X509 != nil { + if p.X509.Allow != nil { + opts.X509.AllowedNames.DNSDomains = p.X509.Allow.Dns + opts.X509.AllowedNames.IPRanges = p.X509.Allow.Ips + opts.X509.AllowedNames.EmailAddresses = p.X509.Allow.Emails + opts.X509.AllowedNames.URIDomains = p.X509.Allow.Uris + } + if p.X509.Deny != nil { + opts.X509.DeniedNames.DNSDomains = p.X509.Deny.Dns + opts.X509.DeniedNames.IPRanges = p.X509.Deny.Ips + opts.X509.DeniedNames.EmailAddresses = p.X509.Deny.Emails + opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris + } + } + // fill ssh policy configuration + if p.Ssh != nil { + if p.Ssh.Host != nil { + if p.Ssh.Host.Allow != nil { + opts.SSH.Host.AllowedNames.DNSDomains = p.Ssh.Host.Allow.Dns + opts.SSH.Host.AllowedNames.IPRanges = p.Ssh.Host.Allow.Ips + opts.SSH.Host.AllowedNames.EmailAddresses = p.Ssh.Host.Allow.Principals + } + if p.Ssh.Host.Deny != nil { + opts.SSH.Host.DeniedNames.DNSDomains = p.Ssh.Host.Deny.Dns + opts.SSH.Host.DeniedNames.IPRanges = p.Ssh.Host.Deny.Ips + opts.SSH.Host.DeniedNames.Principals = p.Ssh.Host.Deny.Principals + } + } + if p.Ssh.User != nil { + if p.Ssh.User.Allow != nil { + opts.SSH.User.AllowedNames.EmailAddresses = p.Ssh.User.Allow.Emails + opts.SSH.User.AllowedNames.Principals = p.Ssh.User.Allow.Principals + } + if p.Ssh.User.Deny != nil { + opts.SSH.User.DeniedNames.EmailAddresses = p.Ssh.User.Deny.Emails + opts.SSH.User.DeniedNames.Principals = p.Ssh.User.Deny.Principals + } + } + } + + return opts +} diff --git a/authority/policy/options.go b/authority/policy/options.go index f57f3bcf..5c6e6134 100644 --- a/authority/policy/options.go +++ b/authority/policy/options.go @@ -1,10 +1,14 @@ package policy +// Options is a container for authority level x509 and SSH +// policy configuration. type Options struct { X509 *X509PolicyOptions `json:"x509,omitempty"` SSH *SSHPolicyOptions `json:"ssh,omitempty"` } +// GetX509Options returns the x509 authority level policy +// configuration func (o *Options) GetX509Options() *X509PolicyOptions { if o == nil { return nil @@ -12,6 +16,8 @@ func (o *Options) GetX509Options() *X509PolicyOptions { return o.X509 } +// GetSSHOptions returns the SSH authority level policy +// configuration func (o *Options) GetSSHOptions() *SSHPolicyOptions { if o == nil { return nil @@ -19,16 +25,19 @@ func (o *Options) GetSSHOptions() *SSHPolicyOptions { return o.SSH } +// X509PolicyOptionsInterface is an interface for providers +// of x509 allowed and denied names. type X509PolicyOptionsInterface interface { GetAllowedNameOptions() *X509NameOptions GetDeniedNameOptions() *X509NameOptions } +// X509PolicyOptions is a container for x509 allowed and denied +// names. type X509PolicyOptions struct { - // AllowedNames ... + // AllowedNames contains the x509 allowed names AllowedNames *X509NameOptions `json:"allow,omitempty"` - - // DeniedNames ... + // DeniedNames contains the x509 denied names DeniedNames *X509NameOptions `json:"deny,omitempty"` } @@ -49,6 +58,8 @@ func (o *X509NameOptions) HasNames() bool { len(o.URIDomains) > 0 } +// SSHPolicyOptionsInterface is an interface for providers of +// SSH user and host name policy configuration. type SSHPolicyOptionsInterface interface { GetAllowedUserNameOptions() *SSHNameOptions GetDeniedUserNameOptions() *SSHNameOptions @@ -56,16 +67,16 @@ type SSHPolicyOptionsInterface interface { GetDeniedHostNameOptions() *SSHNameOptions } +// SSHPolicyOptions is a container for SSH user and host policy +// configuration type SSHPolicyOptions struct { // User contains SSH user certificate options. User *SSHUserCertificateOptions `json:"user,omitempty"` - // Host contains SSH host certificate options. Host *SSHHostCertificateOptions `json:"host,omitempty"` } -// GetAllowedNameOptions returns AllowedNames, which models the -// SANs that ... +// GetAllowedNameOptions returns x509 allowed name policy configuration func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions { if o == nil { return nil @@ -73,8 +84,7 @@ func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions { return o.AllowedNames } -// GetDeniedNameOptions returns the DeniedNames, which models the -// SANs that ... +// GetDeniedNameOptions returns the x509 denied name policy configuration func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { if o == nil { return nil @@ -82,6 +92,8 @@ func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { return o.DeniedNames } +// GetAllowedUserNameOptions returns the SSH allowed user name policy +// configuration. func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { if o == nil { return nil @@ -92,6 +104,8 @@ func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { return o.User.AllowedNames } +// GetDeniedUserNameOptions returns the SSH denied user name policy +// configuration. func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions { if o == nil { return nil @@ -102,6 +116,8 @@ func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions { return o.User.DeniedNames } +// GetAllowedHostNameOptions returns the SSH allowed host name policy +// configuration. func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions { if o == nil { return nil @@ -112,6 +128,8 @@ func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions { return o.Host.AllowedNames } +// GetDeniedHostNameOptions returns the SSH denied host name policy +// configuration. func (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions { if o == nil { return nil diff --git a/authority/provisioners.go b/authority/provisioners.go index 8dc27c6a..7a579267 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" "go.step.sm/cli-utils/step" @@ -395,6 +396,58 @@ func optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options { ops.SSH.Template = string(p.SshTemplate.Template) ops.SSH.TemplateData = p.SshTemplate.Data } + if p.Policy != nil { + if p.Policy.X509 != nil { + if p.Policy.X509.Allow != nil { + ops.X509.AllowedNames = &policy.X509NameOptions{ + DNSDomains: p.Policy.X509.Allow.Dns, + IPRanges: p.Policy.X509.Allow.Ips, + EmailAddresses: p.Policy.X509.Allow.Emails, + URIDomains: p.Policy.X509.Allow.Uris, + } + } + if p.Policy.X509.Deny != nil { + ops.X509.DeniedNames = &policy.X509NameOptions{ + DNSDomains: p.Policy.X509.Deny.Dns, + IPRanges: p.Policy.X509.Deny.Ips, + EmailAddresses: p.Policy.X509.Deny.Emails, + URIDomains: p.Policy.X509.Deny.Uris, + } + } + } + if p.Policy.Ssh != nil { + if p.Policy.Ssh.Host != nil { + ops.SSH.Host = &policy.SSHHostCertificateOptions{} + if p.Policy.Ssh.Host.Allow != nil { + ops.SSH.Host.AllowedNames = &policy.SSHNameOptions{ + DNSDomains: p.Policy.Ssh.Host.Allow.Dns, + IPRanges: p.Policy.Ssh.Host.Allow.Ips, + } + } + 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, + } + } + } + if p.Policy.Ssh.User != nil { + ops.SSH.User = &policy.SSHUserCertificateOptions{} + if p.Policy.Ssh.User.Allow != nil { + ops.SSH.User.AllowedNames = &policy.SSHNameOptions{ + EmailAddresses: p.Policy.Ssh.User.Allow.Emails, + Principals: p.Policy.Ssh.User.Allow.Principals, + } + } + if p.Policy.Ssh.User.Deny != nil { + ops.SSH.User.DeniedNames = &policy.SSHNameOptions{ + EmailAddresses: p.Policy.Ssh.User.Deny.Emails, + Principals: p.Policy.Ssh.User.Deny.Principals, + } + } + } + } + } return ops } diff --git a/authority/tls.go b/authority/tls.go index d749e2ad..96c80e9a 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -192,7 +192,10 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } // If a policy is configured, perform allow/deny policy check on authority level - if a.x509Policy != nil { + // TODO: policy currently also applies to admin token certs; how to circumvent? + // Allow any name of an admin in the DB? Or in the admin collection? + todoRemoveThis := false + if todoRemoveThis && a.x509Policy != nil { allowed, err := a.x509Policy.AreCertificateNamesAllowed(leaf) if err != nil { return nil, errs.InternalServerErr(err, diff --git a/ca/ca.go b/ca/ca.go index c95ba22f..f4585aba 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -208,7 +208,8 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { adminDB := auth.GetAdminDatabase() if adminDB != nil { acmeAdminResponder := adminAPI.NewACMEAdminResponder() - adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder) + policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB) + adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder, policyAdminResponder) mux.Route("/admin", func(r chi.Router) { adminHandler.Route(r) }) diff --git a/go.mod b/go.mod index 46fe260c..76cdff9a 100644 --- a/go.mod +++ b/go.mod @@ -49,4 +49,4 @@ require ( // replace github.com/smallstep/nosql => ../nosql // replace go.step.sm/crypto => ../crypto // replace go.step.sm/cli-utils => ../cli-utils -// replace go.step.sm/linkedca => ../linkedca +replace go.step.sm/linkedca => ../linkedca diff --git a/go.sum b/go.sum index 1cd8e2e7..ba7cb531 100644 --- a/go.sum +++ b/go.sum @@ -685,10 +685,6 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/ go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.15.0 h1:VioBln+x3+RoejgeBhvxkLGVYdWRy6PFiAaUUN29/E0= go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= -go.step.sm/linkedca v0.9.2 h1:CpAkd174sLXFfrOZrbPEiTzik91QRj3+L0omsiwsiok= -go.step.sm/linkedca v0.9.2/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= -go.step.sm/linkedca v0.10.0 h1:+bqymMRulHYkVde4l16FnqFVskoS6HCWJN5Z5cxAqF8= -go.step.sm/linkedca v0.10.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= From 101ca6a2d379a67ac8a4347b0fa5fe74f021ca87 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 21 Mar 2022 15:53:59 +0100 Subject: [PATCH 16/78] Check admin subjects before changing policy --- acme/api/order.go | 2 + authority/admin/api/acme.go | 2 +- authority/admin/api/admin.go | 4 +- authority/admin/api/admin_test.go | 4 +- authority/admin/api/handler.go | 3 +- authority/admin/api/middleware.go | 26 ++++--- authority/admin/api/middleware_test.go | 10 +-- authority/admin/api/policy.go | 48 +++++++++--- authority/admin/context.go | 10 +++ authority/administrator/collection.go | 2 +- authority/authority.go | 10 ++- authority/policy.go | 102 ++++++++++++++++++++----- authority/tls.go | 89 ++++++++++++++++----- policy/engine.go | 31 ++++---- policy/engine_test.go | 2 +- policy/options_test.go | 1 + 16 files changed, 255 insertions(+), 91 deletions(-) create mode 100644 authority/admin/context.go diff --git a/acme/api/order.go b/acme/api/order.go index e1adebb3..8fe37656 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -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, // 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. + // 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 { // TODO: gather all errors, so that we can build subproblems; include the nor.Validate() error here too, like in example? diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 27c3ba6f..131a8fff 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -14,7 +14,7 @@ import ( const ( // provisionerContextKey provisioner key - provisionerContextKey = ContextKey("provisioner") + provisionerContextKey = admin.ContextKey("provisioner") ) // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests diff --git a/authority/admin/api/admin.go b/authority/admin/api/admin.go index dd40784b..34db5ea2 100644 --- a/authority/admin/api/admin.go +++ b/authority/admin/api/admin.go @@ -26,8 +26,8 @@ type adminAuthority interface { UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error RemoveProvisioner(ctx context.Context, id string) error GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) - StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error - UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error + StoreAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error + UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error RemoveAuthorityPolicy(ctx context.Context) error } diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go index f1698139..bcea31b5 100644 --- a/authority/admin/api/admin_test.go +++ b/authority/admin/api/admin_test.go @@ -139,11 +139,11 @@ func (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca. 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") } -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") } diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index e59b95e0..0dd45cb0 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -30,8 +30,7 @@ func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeRespo func (h *Handler) Route(r api.Router) { authnz := func(next nextHTTP) nextHTTP { - //return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) - return h.requireAPIEnabled(next) // TODO(hs): remove this; temporarily no auth checks for simple testing... + return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) } requireEABEnabled := func(next nextHTTP) nextHTTP { diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 62aefdc3..c30c7219 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -7,6 +7,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin/db/nosql" + "go.step.sm/linkedca" ) 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. func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { + tok := r.Header.Get("Authorization") if tok == "" { api.WriteError(w, admin.NewError(admin.ErrorUnauthorizedType, @@ -40,7 +42,7 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { return } - ctx := context.WithValue(r.Context(), adminContextKey, adm) + ctx := context.WithValue(r.Context(), admin.AdminContextKey, adm) 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 { 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 { next(w, r) 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 { api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "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 -// essentials in the context of a request. -type ContextKey string - -const ( - // adminContextKey account key - adminContextKey = ContextKey("admin") -) +// adminFromContext searches the context for a *linkedca.Admin. +// Returns the admin or an error. +func adminFromContext(ctx context.Context) (*linkedca.Admin, error) { + val, ok := ctx.Value(admin.AdminContextKey).(*linkedca.Admin) + if !ok || val == nil { + return nil, admin.NewError(admin.ErrorBadRequestType, "admin not in context") + } + return val, nil +} diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index 7fb4671a..ffa319db 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -152,7 +152,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { req.Header["Authorization"] = []string{"token"} createdAt := time.Now() var deletedAt time.Time - admin := &linkedca.Admin{ + adm := &linkedca.Admin{ Id: "adminID", AuthorityId: "authorityID", Subject: "admin", @@ -164,20 +164,20 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { auth := &mockAdminAuthority{ MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) { assert.Equals(t, "token", token) - return admin, nil + return adm, nil }, } next := func(w http.ResponseWriter, r *http.Request) { 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) if !ok { t.Errorf("expected *linkedca.Admin; got %T", a) return } opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} - if !cmp.Equal(admin, adm, opts...) { - t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(admin, adm, opts...)) + if !cmp.Equal(adm, adm, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(adm, adm, opts...)) } w.Write(nil) // mock response with status 200 } diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index c318e5e5..2f64802f 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -87,8 +87,14 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r return } - if err := par.auth.StoreAuthorityPolicy(ctx, newPolicy); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error storing authority policy")) + adm, err := adminFromContext(ctx) + 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 } @@ -103,25 +109,49 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r // UpdateAuthorityPolicy handles the PUT /admin/authority/policy 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) return } - ctx := r.Context() - if err := par.auth.UpdateAuthorityPolicy(ctx, policy); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error updating authority policy")) + adm, err := adminFromContext(ctx) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error retrieving admin from context")) 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 { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating")) return } - api.ProtoJSONStatus(w, newPolicy, http.StatusOK) + api.ProtoJSONStatus(w, newlyStoredPolicy, http.StatusOK) } // DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request diff --git a/authority/admin/context.go b/authority/admin/context.go new file mode 100644 index 00000000..87bf3e03 --- /dev/null +++ b/authority/admin/context.go @@ -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") +) diff --git a/authority/administrator/collection.go b/authority/administrator/collection.go index 88d7bb2c..300c3e4f 100644 --- a/authority/administrator/collection.go +++ b/authority/administrator/collection.go @@ -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 -// 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 { // Input validation. if adm.ProvisionerId != prov.GetID() { diff --git a/authority/authority.go b/authority/authority.go index aaf0e478..29a10d7e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -219,15 +219,19 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { if a.config.AuthorityConfig.EnableAdmin { linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) 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) } else { 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 { + a.x509Policy = nil + a.sshHostPolicy = nil + a.sshUserPolicy = nil return nil } @@ -574,7 +578,7 @@ func (a *Authority) init() error { return err } - // Load Policy Engines + // Load x509 and SSH Policy Engines if err := a.reloadPolicyEngines(context.Background()); err != nil { return err } diff --git a/authority/policy.go b/authority/policy.go index 8ef264d0..db44e5f4 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -2,10 +2,15 @@ package authority import ( "context" + "fmt" + + "github.com/pkg/errors" + + "go.step.sm/linkedca" "github.com/smallstep/certificates/authority/admin" - "github.com/smallstep/certificates/authority/policy" - "go.step.sm/linkedca" + authPolicy "github.com/smallstep/certificates/authority/policy" + policy "github.com/smallstep/certificates/policy" ) 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 } -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() defer a.adminMutex.Unlock() + if err := a.checkPolicy(ctx, adm, policy); err != nil { + return err + } + if err := a.adminDB.CreateAuthorityPolicy(ctx, policy); err != nil { return err } 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 } -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() defer a.adminMutex.Unlock() + if err := a.checkPolicy(ctx, adm, policy); err != nil { + return err + } + if err := a.adminDB.UpdateAuthorityPolicy(ctx, policy); err != nil { return err } 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 @@ -59,34 +72,84 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { } 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 } -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 if p == nil { return nil } + // prepare full policy struct - opts := &policy.Options{ - X509: &policy.X509PolicyOptions{ - AllowedNames: &policy.X509NameOptions{}, - DeniedNames: &policy.X509NameOptions{}, + opts := &authPolicy.Options{ + X509: &authPolicy.X509PolicyOptions{ + AllowedNames: &authPolicy.X509NameOptions{}, + DeniedNames: &authPolicy.X509NameOptions{}, }, - SSH: &policy.SSHPolicyOptions{ - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{}, - DeniedNames: &policy.SSHNameOptions{}, + SSH: &authPolicy.SSHPolicyOptions{ + Host: &authPolicy.SSHHostCertificateOptions{ + AllowedNames: &authPolicy.SSHNameOptions{}, + DeniedNames: &authPolicy.SSHNameOptions{}, }, - User: &policy.SSHUserCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{}, - DeniedNames: &policy.SSHNameOptions{}, + User: &authPolicy.SSHUserCertificateOptions{ + AllowedNames: &authPolicy.SSHNameOptions{}, + DeniedNames: &authPolicy.SSHNameOptions{}, }, }, } + // fill x509 policy configuration if p.X509 != 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 } } + // fill ssh policy configuration if p.Ssh != nil { if p.Ssh.Host != nil { diff --git a/authority/tls.go b/authority/tls.go index 96c80e9a..297c796e 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -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 - // TODO: policy currently also applies to admin token certs; how to circumvent? - // Allow any name of an admin in the DB? Or in the admin collection? - todoRemoveThis := false - if todoRemoveThis && a.x509Policy != nil { - allowed, err := a.x509Policy.AreCertificateNamesAllowed(leaf) - if err != nil { - return nil, errs.InternalServerErr(err, - errs.WithKeyVal("csr", csr), - errs.WithKeyVal("signOptions", signOpts), - errs.WithMessage("error creating certificate"), - ) - } - 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..., - ) - } + // Check if authority is allowed to sign the certificate + var allowedToSign bool + if allowedToSign, err = a.isAllowedToSign(leaf); err != nil { + return nil, errs.InternalServerErr(err, + errs.WithKeyVal("csr", csr), + errs.WithKeyVal("signOptions", signOpts), + errs.WithMessage("error creating certificate"), + ) + } + if !allowedToSign { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(errors.New("authority not allowed to sign"), "error creating certificate"), + opts..., + ) } // Sign certificate @@ -236,6 +230,61 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign 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 // with a validity window that begins 'now'. func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { diff --git a/policy/engine.go b/policy/engine.go index e9038dd0..63d8452a 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -10,9 +10,10 @@ import ( "reflect" "strings" - "go.step.sm/crypto/x509util" "golang.org/x/crypto/ssh" "golang.org/x/net/idna" + + "go.step.sm/crypto/x509util" ) type NamePolicyReason int @@ -39,7 +40,7 @@ type NamePolicyError struct { Detail string } -func (e NamePolicyError) Error() string { +func (e *NamePolicyError) Error() string { switch e.Reason { case NotAuthorizedForThisName: 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 // (other) excluded constraints, we'll allow a DNS (implicit allow; currently). if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return NamePolicyError{ + return &NamePolicyError{ Reason: NotAuthorizedForThisName, 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) if err != nil { - return NamePolicyError{ + return &NamePolicyError{ Reason: CannotParseDomain, 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 } if _, ok := domainToReverseLabels(parsedDNS); !ok { - return NamePolicyError{ + return &NamePolicyError{ Reason: CannotParseDomain, 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 { if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return NamePolicyError{ + return &NamePolicyError{ Reason: NotAuthorizedForThisName, 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 { if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return NamePolicyError{ + return &NamePolicyError{ Reason: NotAuthorizedForThisName, Detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email), } } mailbox, ok := parseRFC2821Mailbox(email) if !ok { - return NamePolicyError{ + return &NamePolicyError{ Reason: CannotParseRFC822Name, 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 domainASCII, err := idna.ToASCII(mailbox.domain) if err != nil { - return NamePolicyError{ + return &NamePolicyError{ Reason: CannotParseDomain, 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 { if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return NamePolicyError{ + return &NamePolicyError{ Reason: NotAuthorizedForThisName, 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 { if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return NamePolicyError{ + return &NamePolicyError{ Reason: NotAuthorizedForThisName, 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() match, err := match(parsedName, constraint) if err != nil { - return NamePolicyError{ + return &NamePolicyError{ Reason: CannotMatchNameToConstraint, Detail: err.Error(), } } if match { - return NamePolicyError{ + return &NamePolicyError{ Reason: NotAuthorizedForThisName, Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), } @@ -452,7 +453,7 @@ func checkNameConstraints( constraint := permittedValue.Index(i).Interface() var err error if ok, err = match(parsedName, constraint); err != nil { - return NamePolicyError{ + return &NamePolicyError{ Reason: CannotMatchNameToConstraint, Detail: err.Error(), } @@ -464,7 +465,7 @@ func checkNameConstraints( } if !ok { - return NamePolicyError{ + return &NamePolicyError{ Reason: NotAuthorizedForThisName, Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), } diff --git a/policy/engine_test.go b/policy/engine_test.go index 0259e8de..f7a4b20a 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -13,7 +13,7 @@ import ( ) // 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) { tests := []struct { diff --git a/policy/options_test.go b/policy/options_test.go index 0fc54aa2..8a64f282 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/smallstep/assert" ) From 6b620c8e9c844f66d4f41cd5a1796c48e38086aa Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 24 Mar 2022 10:54:45 +0100 Subject: [PATCH 17/78] Improve protobuf unmarshaling error handling --- api/utils.go | 52 +++++++++++++++++++++++++++++-- authority/admin/api/admin.go | 4 +-- authority/admin/api/admin_test.go | 16 +++++----- authority/admin/api/middleware.go | 5 +-- authority/admin/api/policy.go | 41 +++++++----------------- authority/policy.go | 20 ++++++------ go.sum | 3 +- policy/engine.go | 26 ++++++++++++++++ policy/engine_test.go | 3 +- 9 files changed, 116 insertions(+), 54 deletions(-) diff --git a/api/utils.go b/api/utils.go index b6ff7960..91091e25 100644 --- a/api/utils.go +++ b/api/utils.go @@ -2,14 +2,16 @@ package api import ( "encoding/json" + "errors" "io" "log" "net/http" - "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/logging" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + + "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/logging" ) // EnableLogger is an interface that enables response logging for an object. @@ -114,3 +116,49 @@ func ReadProtoJSON(r io.Reader, m proto.Message) error { } return protojson.Unmarshal(data, m) } + +// ReadProtoJSONWithCheck reads JSON from the request body and stores it in the value +// pointed by v. TODO(hs): move this to and integrate with render package. +func ReadProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) bool { + data, err := io.ReadAll(r) + if err != nil { + var wrapper = struct { + Status int `json:"code"` + Message string `json:"message"` + }{ + Status: http.StatusBadRequest, + Message: err.Error(), + } + data, err := json.Marshal(wrapper) // TODO(hs): handle err; even though it's very unlikely to fail + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + w.Write(data) + return false + } + if err := protojson.Unmarshal(data, m); err != nil { + if errors.Is(err, proto.Error) { + var wrapper = struct { + Message string `json:"message"` + }{ + Message: err.Error(), + } + data, err := json.Marshal(wrapper) // TODO(hs): handle err; even though it's very unlikely to fail + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + w.Write(data) + return false + } + + // fallback to the default error writer + WriteError(w, err) + return false + } + + return true +} diff --git a/authority/admin/api/admin.go b/authority/admin/api/admin.go index 34db5ea2..95b9ba98 100644 --- a/authority/admin/api/admin.go +++ b/authority/admin/api/admin.go @@ -26,8 +26,8 @@ type adminAuthority interface { UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error RemoveProvisioner(ctx context.Context, id string) error GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) - StoreAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error - UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error + CreateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) + UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) RemoveAuthorityPolicy(ctx context.Context) error } diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go index bcea31b5..d9592ff2 100644 --- a/authority/admin/api/admin_test.go +++ b/authority/admin/api/admin_test.go @@ -14,11 +14,13 @@ import ( "github.com/go-chi/chi" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/types/known/timestamppb" + + "go.step.sm/linkedca" + "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/linkedca" - "google.golang.org/protobuf/types/known/timestamppb" ) type mockAdminAuthority struct { @@ -39,7 +41,7 @@ type mockAdminAuthority struct { MockRemoveProvisioner func(ctx context.Context, id string) error MockGetAuthorityPolicy func(ctx context.Context) (*linkedca.Policy, error) - MockStoreAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error + MockCreateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) (*linkedca.Policy, error) MockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error MockRemoveAuthorityPolicy func(ctx context.Context) error } @@ -139,12 +141,12 @@ func (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca. return nil, errors.New("not implemented yet") } -func (m *mockAdminAuthority) StoreAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error { - return errors.New("not implemented yet") +func (m *mockAdminAuthority) CreateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { + return nil, errors.New("not implemented yet") } -func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) error { - return errors.New("not implemented yet") +func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { + return nil, errors.New("not implemented yet") } func (m *mockAdminAuthority) RemoveAuthorityPolicy(ctx context.Context) error { diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index c30c7219..74bb2234 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -4,10 +4,11 @@ import ( "context" "net/http" + "go.step.sm/linkedca" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin/db/nosql" - "go.step.sm/linkedca" ) type nextHTTP = func(http.ResponseWriter, *http.Request) @@ -42,7 +43,7 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { return } - ctx := context.WithValue(r.Context(), admin.AdminContextKey, adm) + ctx := linkedca.WithAdmin(r.Context(), adm) next(w, r.WithContext(ctx)) } } diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 2f64802f..30e05c48 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -4,10 +4,12 @@ import ( "net/http" "github.com/go-chi/chi" + + "go.step.sm/linkedca" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/linkedca" ) type policyAdminResponderInterface interface { @@ -82,29 +84,19 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r } var newPolicy = new(linkedca.Policy) - if err := api.ReadProtoJSON(r.Body, newPolicy); err != nil { - api.WriteError(w, err) + if !api.ReadProtoJSONWithCheck(w, r.Body, newPolicy) { return } - adm, err := adminFromContext(ctx) - if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error retrieving admin from context")) - return - } + adm := linkedca.AdminFromContext(ctx) - if err := par.auth.StoreAuthorityPolicy(ctx, adm, newPolicy); err != nil { + var createdPolicy *linkedca.Policy + if createdPolicy, err = par.auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil { api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy")) return } - storedPolicy, err := par.auth.GetAuthorityPolicy(ctx) - if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating")) - return - } - - api.JSONStatus(w, storedPolicy, http.StatusCreated) + api.JSONStatus(w, createdPolicy, http.StatusCreated) } // UpdateAuthorityPolicy handles the PUT /admin/authority/policy request @@ -134,24 +126,15 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r return } - adm, err := adminFromContext(ctx) - if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error retrieving admin from context")) - return - } + adm := linkedca.AdminFromContext(ctx) - if err := par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil { + var updatedPolicy *linkedca.Policy + if updatedPolicy, 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 { - api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating")) - return - } - - api.ProtoJSONStatus(w, newlyStoredPolicy, http.StatusOK) + api.ProtoJSONStatus(w, updatedPolicy, http.StatusOK) } // DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request diff --git a/authority/policy.go b/authority/policy.go index db44e5f4..ee132f31 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -25,42 +25,42 @@ func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, e return policy, nil } -func (a *Authority) StoreAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) error { +func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { a.adminMutex.Lock() defer a.adminMutex.Unlock() if err := a.checkPolicy(ctx, adm, policy); err != nil { - return err + return nil, err } if err := a.adminDB.CreateAuthorityPolicy(ctx, policy); err != nil { - return err + return nil, err } if err := a.reloadPolicyEngines(ctx); err != nil { - return admin.WrapErrorISE(err, "error reloading policy engines when creating authority policy") + return nil, admin.WrapErrorISE(err, "error reloading policy engines when creating authority policy") } - return nil + return policy, nil // TODO: return the newly stored policy } -func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) error { +func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { a.adminMutex.Lock() defer a.adminMutex.Unlock() if err := a.checkPolicy(ctx, adm, policy); err != nil { - return err + return nil, err } if err := a.adminDB.UpdateAuthorityPolicy(ctx, policy); err != nil { - return err + return nil, err } if err := a.reloadPolicyEngines(ctx); err != nil { - return admin.WrapErrorISE(err, "error reloading policy engines when updating authority policy") + return nil, admin.WrapErrorISE(err, "error reloading policy engines when updating authority policy") } - return nil + return policy, nil // TODO: return the updated stored policy } func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { diff --git a/go.sum b/go.sum index ba7cb531..e7681592 100644 --- a/go.sum +++ b/go.sum @@ -639,8 +639,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= diff --git a/policy/engine.go b/policy/engine.go index 63d8452a..c37e1f59 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -4,7 +4,9 @@ import ( "bytes" "crypto/x509" "crypto/x509/pkix" + "errors" "fmt" + "io" "net" "net/url" "reflect" @@ -40,6 +42,30 @@ type NamePolicyError struct { Detail string } +type NameError struct { + error + Reason NamePolicyReason +} + +func a() { + err := io.EOF + var ne *NameError + errors.As(err, ne) + errors.Is(err, ne) +} + +func newPolicyError(reason NamePolicyReason, err error) error { + return &NameError{ + error: err, + Reason: reason, + } +} + +func newPolicyErrorf(reason NamePolicyReason, format string, args ...interface{}) error { + err := fmt.Errorf(format, args...) + return newPolicyError(reason, err) +} + func (e *NamePolicyError) Error() string { switch e.Reason { case NotAuthorizedForThisName: diff --git a/policy/engine_test.go b/policy/engine_test.go index f7a4b20a..cf406e71 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -8,8 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/smallstep/assert" "golang.org/x/crypto/ssh" + + "github.com/smallstep/assert" ) // TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on From 613c99f00f8fb156e732f80d352c3371da025d55 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 24 Mar 2022 13:10:49 +0100 Subject: [PATCH 18/78] Fix linting issues --- api/utils.go | 8 +++--- authority/admin/api/admin_test.go | 4 +-- authority/admin/api/middleware.go | 11 ------- authority/admin/api/middleware_test.go | 7 +---- authority/admin/api/policy.go | 10 +++---- authority/admin/db/nosql/policy.go | 14 ++++----- authority/policy.go | 25 ++++++++-------- authority/provisioner/aws.go | 5 ++-- authority/provisioner/sign_options.go | 9 +++--- authority/tls.go | 40 -------------------------- policy/engine.go | 26 ----------------- 11 files changed, 37 insertions(+), 122 deletions(-) diff --git a/api/utils.go b/api/utils.go index 67b46aa9..761430ed 100644 --- a/api/utils.go +++ b/api/utils.go @@ -83,13 +83,13 @@ func ReadProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) Status: http.StatusBadRequest, Message: err.Error(), } - data, err := json.Marshal(wrapper) // TODO(hs): handle err; even though it's very unlikely to fail + errData, err := json.Marshal(wrapper) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - w.Write(data) + w.Write(errData) return false } if err := protojson.Unmarshal(data, m); err != nil { @@ -99,13 +99,13 @@ func ReadProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) }{ Message: err.Error(), } - data, err := json.Marshal(wrapper) // TODO(hs): handle err; even though it's very unlikely to fail + errData, err := json.Marshal(wrapper) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - w.Write(data) + w.Write(errData) return false } diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go index d9592ff2..678cf6a1 100644 --- a/authority/admin/api/admin_test.go +++ b/authority/admin/api/admin_test.go @@ -141,11 +141,11 @@ func (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca. return nil, errors.New("not implemented yet") } -func (m *mockAdminAuthority) CreateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { +func (m *mockAdminAuthority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { return nil, errors.New("not implemented yet") } -func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { +func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { return nil, errors.New("not implemented yet") } diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 74bb2234..4ca62bfc 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -1,7 +1,6 @@ package api import ( - "context" "net/http" "go.step.sm/linkedca" @@ -70,13 +69,3 @@ func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTT next(w, r) } } - -// adminFromContext searches the context for a *linkedca.Admin. -// Returns the admin or an error. -func adminFromContext(ctx context.Context) (*linkedca.Admin, error) { - val, ok := ctx.Value(admin.AdminContextKey).(*linkedca.Admin) - if !ok || val == nil { - return nil, admin.NewError(admin.ErrorBadRequestType, "admin not in context") - } - return val, nil -} diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index ffa319db..158374d0 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -169,12 +169,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { } next := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - a := ctx.Value(admin.AdminContextKey) // verifying that the context now has a linkedca.Admin - adm, ok := a.(*linkedca.Admin) - if !ok { - t.Errorf("expected *linkedca.Admin; got %T", a) - return - } + adm := linkedca.AdminFromContext(ctx) // verifying that the context now has a linkedca.Admin opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} if !cmp.Equal(adm, adm, opts...) { t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(adm, adm, opts...)) diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 30e05c48..6b59803f 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -8,6 +8,7 @@ import ( "go.step.sm/linkedca" "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/api/read" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" ) @@ -121,7 +122,7 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r } var newPolicy = new(linkedca.Policy) - if err := api.ReadProtoJSON(r.Body, newPolicy); err != nil { + if err := read.ProtoJSON(r.Body, newPolicy); err != nil { api.WriteError(w, err) return } @@ -220,7 +221,7 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, } var newPolicy = new(linkedca.Policy) - if err := api.ReadProtoJSON(r.Body, newPolicy); err != nil { + if err := read.ProtoJSON(r.Body, newPolicy); err != nil { api.WriteError(w, err) return } @@ -256,7 +257,7 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, } var policy = new(linkedca.Policy) - if err := api.ReadProtoJSON(r.Body, policy); err != nil { + if err := read.ProtoJSON(r.Body, policy); err != nil { api.WriteError(w, err) return } @@ -271,7 +272,7 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, api.ProtoJSONStatus(w, policy, http.StatusOK) } -// DeleteProvisionerPolicy ... +// DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -308,7 +309,6 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, api.JSON(w, &DeleteResponse{Status: "ok"}) } -// GetACMEAccountPolicy ... func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { api.JSON(w, "ok") } diff --git a/authority/admin/db/nosql/policy.go b/authority/admin/db/nosql/policy.go index 94ff2a0e..8e11ddb0 100644 --- a/authority/admin/db/nosql/policy.go +++ b/authority/admin/db/nosql/policy.go @@ -63,13 +63,13 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db return dbap, nil } -func (db *DB) unmarshalAuthorityPolicy(data []byte, authorityID string) (*linkedca.Policy, error) { - dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID) - if err != nil { - return nil, err - } - return dbap.convert(), nil -} +// func (db *DB) unmarshalAuthorityPolicy(data []byte, authorityID string) (*linkedca.Policy, error) { +// dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID) +// if err != nil { +// return nil, err +// } +// return dbap.convert(), nil +// } func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { diff --git a/authority/policy.go b/authority/policy.go index ee132f31..88f301e0 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -17,23 +17,23 @@ func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, e a.adminMutex.Lock() defer a.adminMutex.Unlock() - policy, err := a.adminDB.GetAuthorityPolicy(ctx) + p, err := a.adminDB.GetAuthorityPolicy(ctx) if err != nil { return nil, err } - return policy, nil + return p, nil } -func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { +func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) { a.adminMutex.Lock() defer a.adminMutex.Unlock() - if err := a.checkPolicy(ctx, adm, policy); err != nil { + if err := a.checkPolicy(ctx, adm, p); err != nil { return nil, err } - if err := a.adminDB.CreateAuthorityPolicy(ctx, policy); err != nil { + if err := a.adminDB.CreateAuthorityPolicy(ctx, p); err != nil { return nil, err } @@ -41,18 +41,18 @@ func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm return nil, admin.WrapErrorISE(err, "error reloading policy engines when creating authority policy") } - return policy, nil // TODO: return the newly stored policy + return p, nil // TODO: return the newly stored policy } -func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { +func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) { a.adminMutex.Lock() defer a.adminMutex.Unlock() - if err := a.checkPolicy(ctx, adm, policy); err != nil { + if err := a.checkPolicy(ctx, adm, p); err != nil { return nil, err } - if err := a.adminDB.UpdateAuthorityPolicy(ctx, policy); err != nil { + if err := a.adminDB.UpdateAuthorityPolicy(ctx, p); err != nil { return nil, err } @@ -60,7 +60,7 @@ func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm return nil, admin.WrapErrorISE(err, "error reloading policy engines when updating authority policy") } - return policy, nil // TODO: return the updated stored policy + return p, nil // TODO: return the updated stored policy } func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { @@ -111,11 +111,10 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error { ) if allowed, err = engine.AreSANsAllowed(sans); err != nil { var policyErr *policy.NamePolicyError - if errors.As(err, &policyErr); policyErr.Reason == policy.NotAuthorizedForThisName { + 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) - } else { - return err } + return err } if !allowed { diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 0bbe546b..f8f14671 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -266,7 +266,6 @@ type AWS struct { Claims *Claims `json:"claims,omitempty"` Options *Options `json:"options,omitempty"` config *awsConfig - audiences Audiences ctl *Controller x509Policy policy.X509Policy sshHostPolicy policy.HostPolicy @@ -557,7 +556,7 @@ func (p *AWS) readURL(url string) ([]byte, error) { if err != nil { return nil, err } - return nil, fmt.Errorf("Request for metadata returned non-successful status code %d", + return nil, fmt.Errorf("request for metadata returned non-successful status code %d", resp.StatusCode) } @@ -590,7 +589,7 @@ func (p *AWS) readURLv2(url string) (*http.Response, error) { } defer resp.Body.Close() if resp.StatusCode >= 400 { - return nil, fmt.Errorf("Request for API token returned non-successful status code %d", resp.StatusCode) + return nil, fmt.Errorf("request for API token returned non-successful status code %d", resp.StatusCode) } token, err := io.ReadAll(resp.Body) if err != nil { diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index 2d8a13c3..df2551a3 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -6,7 +6,6 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" - "encoding/asn1" "encoding/json" "net" "net/http" @@ -427,10 +426,10 @@ func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) e return err } -var ( - stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} - stepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 1)...) -) +// var ( +// stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} +// stepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 1)...) +// ) // type stepProvisionerASN1 struct { // Type int diff --git a/authority/tls.go b/authority/tls.go index 297c796e..df38091c 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -237,14 +237,6 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // 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 @@ -253,38 +245,6 @@ func (a *Authority) isAllowedToSign(cert *x509.Certificate) (bool, error) { 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 // with a validity window that begins 'now'. func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { diff --git a/policy/engine.go b/policy/engine.go index c37e1f59..63d8452a 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -4,9 +4,7 @@ import ( "bytes" "crypto/x509" "crypto/x509/pkix" - "errors" "fmt" - "io" "net" "net/url" "reflect" @@ -42,30 +40,6 @@ type NamePolicyError struct { Detail string } -type NameError struct { - error - Reason NamePolicyReason -} - -func a() { - err := io.EOF - var ne *NameError - errors.As(err, ne) - errors.Is(err, ne) -} - -func newPolicyError(reason NamePolicyReason, err error) error { - return &NameError{ - error: err, - Reason: reason, - } -} - -func newPolicyErrorf(reason NamePolicyReason, format string, args ...interface{}) error { - err := fmt.Errorf(format, args...) - return newPolicyError(reason, err) -} - func (e *NamePolicyError) Error() string { switch e.Reason { case NotAuthorizedForThisName: From 9e0edc7b50cec7311ce27f1a54ffdde2d7347f6e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 24 Mar 2022 14:55:40 +0100 Subject: [PATCH 19/78] Add early authority policy evaluation to ACME order API --- .golangci.yml | 1 - acme/api/eab.go | 3 ++- acme/api/eab_test.go | 4 +++- acme/api/order.go | 24 ++++++++++++++++-------- acme/api/revoke_test.go | 4 ++++ acme/common.go | 1 + acme/order_test.go | 8 ++++++++ authority/policy.go | 3 +-- authority/provisioner/acme.go | 3 +++ authority/tls.go | 16 ++++++++++++++++ 10 files changed, 54 insertions(+), 13 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 59c58490..67aac2df 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -73,4 +73,3 @@ issues: - error strings should not be capitalized or end with punctuation or a newline - Wrapf call needs 1 arg but has 2 args - cs.NegotiatedProtocolIsMutual is deprecated - - rewrite if-else to switch statement diff --git a/acme/api/eab.go b/acme/api/eab.go index 3660d066..1780a173 100644 --- a/acme/api/eab.go +++ b/acme/api/eab.go @@ -4,8 +4,9 @@ import ( "context" "encoding/json" - "github.com/smallstep/certificates/acme" "go.step.sm/crypto/jose" + + "github.com/smallstep/certificates/acme" ) // ExternalAccountBinding represents the ACME externalAccountBinding JWS diff --git a/acme/api/eab_test.go b/acme/api/eab_test.go index dce9f36d..f9bce970 100644 --- a/acme/api/eab_test.go +++ b/acme/api/eab_test.go @@ -9,10 +9,12 @@ import ( "time" "github.com/pkg/errors" + + "go.step.sm/crypto/jose" + "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/crypto/jose" ) func Test_keysAreEqual(t *testing.T) { diff --git a/acme/api/order.go b/acme/api/order.go index 8fe37656..10f180a3 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -11,10 +11,12 @@ import ( "time" "github.com/go-chi/chi" + + "go.step.sm/crypto/randutil" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/crypto/randutil" ) // NewOrderRequest represents the body for a NewOrder request. @@ -36,8 +38,8 @@ func (n *NewOrderRequest) Validate() error { if id.Type == acme.IP && net.ParseIP(id.Value) == nil { return acme.NewError(acme.ErrorMalformedType, "invalid IP address: %s", id.Value) } - // TODO: add some validations for DNS domains? - // TODO: combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1 + // TODO(hs): add some validations for DNS domains? + // TODO(hs): combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1 } return nil } @@ -99,23 +101,29 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { return } - // TODO(hs): this should also verify rules set in the Account (i.e. allowed/denied - // DNS and IPs; it's probably good to connect those to the EAB credentials and management? Or + // TODO(hs): the policy evaluation below should also verify rules set in the Account (i.e. allowed/denied + // DNS and IPs). It's probably good to connect those to the EAB credentials and management? Or // should we do it fully properly and connect them to the Account directly? The latter would allow // 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 // 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?) + // TODO(hs): gather all errors, so that we can build one response with subproblems; include the nor.Validate() + // error here too, like in example? 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? + // evaluate the provisioner level policy orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value} err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier) if err != nil { api.WriteError(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) return } + // evaluate the authority level policy + err = h.ca.AreSANsAllowed(ctx, []string{identifier.Value}) + if err != nil { + api.WriteError(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) + return + } } now := clock.Now() diff --git a/acme/api/revoke_test.go b/acme/api/revoke_test.go index 4ff54405..aa3dda10 100644 --- a/acme/api/revoke_test.go +++ b/acme/api/revoke_test.go @@ -282,6 +282,10 @@ func (m *mockCA) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, return nil, nil } +func (m *mockCA) AreSANsAllowed(ctx context.Context, sans []string) error { + return nil +} + func (m *mockCA) IsRevoked(sn string) (bool, error) { if m.MockIsRevoked != nil { return m.MockIsRevoked(sn) diff --git a/acme/common.go b/acme/common.go index 9c5e732a..e0d96deb 100644 --- a/acme/common.go +++ b/acme/common.go @@ -12,6 +12,7 @@ import ( // CertificateAuthority is the interface implemented by a CA authority. type CertificateAuthority interface { Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) + AreSANsAllowed(ctx context.Context, sans []string) error IsRevoked(sn string) (bool, error) Revoke(context.Context, *authority.RevokeOptions) error LoadProvisionerByName(string) (provisioner.Interface, error) diff --git a/acme/order_test.go b/acme/order_test.go index 493b40b7..f1f28e40 100644 --- a/acme/order_test.go +++ b/acme/order_test.go @@ -268,6 +268,7 @@ func TestOrder_UpdateStatus(t *testing.T) { type mockSignAuth struct { sign func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) + areSANsAllowed func(ctx context.Context, sans []string) error loadProvisionerByName func(string) (provisioner.Interface, error) ret1, ret2 interface{} err error @@ -282,6 +283,13 @@ func (m *mockSignAuth) Sign(csr *x509.CertificateRequest, signOpts provisioner.S return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err } +func (m *mockSignAuth) AreSANsAllowed(ctx context.Context, sans []string) error { + if m.areSANsAllowed != nil { + return m.areSANsAllowed(ctx, sans) + } + return m.err +} + func (m *mockSignAuth) LoadProvisionerByName(name string) (provisioner.Interface, error) { if m.loadProvisionerByName != nil { return m.loadProvisionerByName(name) diff --git a/authority/policy.go b/authority/policy.go index 88f301e0..4f93899f 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -2,10 +2,9 @@ package authority import ( "context" + "errors" "fmt" - "github.com/pkg/errors" - "go.step.sm/linkedca" "github.com/smallstep/certificates/authority/admin" diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index ffd9cb38..2bcaeef2 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -3,6 +3,7 @@ package provisioner import ( "context" "crypto/x509" + "fmt" "net" "time" @@ -126,6 +127,8 @@ func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIden _, err = p.x509Policy.IsIPAllowed(net.ParseIP(identifier.Value)) case DNS: _, err = p.x509Policy.IsDNSAllowed(identifier.Value) + default: + err = fmt.Errorf("invalid ACME identifier type '%s' provided", identifier.Type) } return err diff --git a/authority/tls.go b/authority/tls.go index df38091c..867e2c51 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -245,6 +245,22 @@ func (a *Authority) isAllowedToSign(cert *x509.Certificate) (bool, error) { return a.x509Policy.AreCertificateNamesAllowed(cert) } +// AreSANsAllowed evaluates the provided sans against the +// authority X.509 policy. +func (a *Authority) AreSANsAllowed(ctx context.Context, sans []string) error { + + // no policy configured; return early + if a.x509Policy == nil { + return nil + } + + // evaluate the X.509 policy for the provided sans + var err error + _, err = a.x509Policy.AreSANsAllowed(sans) + + return err +} + // Renew creates a new Certificate identical to the old certificate, except // with a validity window that begins 'now'. func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { From b49307f326c69b7c02f287b553f7cdefc32b9cf5 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 24 Mar 2022 18:34:04 +0100 Subject: [PATCH 20/78] Fix ACME order tests with mock ACME CA --- acme/api/order_test.go | 10 +++++++++- authority/admin/api/acme.go | 11 ++++------- authority/admin/api/middleware.go | 6 +++--- authority/admin/context.go | 10 ---------- authority/provisioner/scep.go | 2 +- authority/tls.go | 4 ---- 6 files changed, 17 insertions(+), 26 deletions(-) delete mode 100644 authority/admin/context.go diff --git a/acme/api/order_test.go b/acme/api/order_test.go index 1ce034e7..ccaef176 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -667,6 +667,7 @@ func TestHandler_NewOrder(t *testing.T) { baseURL.String(), escProvName) type test struct { + ca acme.CertificateAuthority db acme.DB ctx context.Context nor *NewOrderRequest @@ -771,6 +772,7 @@ func TestHandler_NewOrder(t *testing.T) { return test{ ctx: ctx, statusCode: 500, + ca: &mockCA{}, db: &acme.MockDB{ MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { assert.Equals(t, ch.AccountID, "accID") @@ -804,6 +806,7 @@ func TestHandler_NewOrder(t *testing.T) { return test{ ctx: ctx, statusCode: 500, + ca: &mockCA{}, db: &acme.MockDB{ MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { switch count { @@ -876,6 +879,7 @@ func TestHandler_NewOrder(t *testing.T) { ctx: ctx, statusCode: 201, nor: nor, + ca: &mockCA{}, db: &acme.MockDB{ MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { switch chCount { @@ -991,6 +995,7 @@ func TestHandler_NewOrder(t *testing.T) { ctx: ctx, statusCode: 201, nor: nor, + ca: &mockCA{}, db: &acme.MockDB{ MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { switch count { @@ -1083,6 +1088,7 @@ func TestHandler_NewOrder(t *testing.T) { ctx: ctx, statusCode: 201, nor: nor, + ca: &mockCA{}, db: &acme.MockDB{ MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { switch count { @@ -1174,6 +1180,7 @@ func TestHandler_NewOrder(t *testing.T) { ctx: ctx, statusCode: 201, nor: nor, + ca: &mockCA{}, db: &acme.MockDB{ MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { switch count { @@ -1266,6 +1273,7 @@ func TestHandler_NewOrder(t *testing.T) { ctx: ctx, statusCode: 201, nor: nor, + ca: &mockCA{}, db: &acme.MockDB{ MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { switch count { @@ -1334,7 +1342,7 @@ func TestHandler_NewOrder(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{linker: NewLinker("dns", "acme"), db: tc.db} + h := &Handler{linker: NewLinker("dns", "acme"), db: tc.db, ca: tc.ca} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 131a8fff..39be50c7 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -6,15 +6,12 @@ import ( "net/http" "github.com/go-chi/chi" + + "go.step.sm/linkedca" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/linkedca" -) - -const ( - // provisionerContextKey provisioner key - provisionerContextKey = admin.ContextKey("provisioner") ) // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests @@ -51,7 +48,7 @@ func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov.GetName())) return } - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) next(w, r.WithContext(ctx)) } } diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 4ca62bfc..1acc661e 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -42,7 +42,7 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { return } - ctx := linkedca.WithAdmin(r.Context(), adm) + ctx := linkedca.NewContextWithAdmin(r.Context(), adm) next(w, r.WithContext(ctx)) } } @@ -57,8 +57,8 @@ func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTT return } - // when not in standalone mode and using a nosql.DB backend, - // actions are not supported + // when an action is not supported in standalone mode and when + // using a nosql.DB backend, actions are not supported if _, ok := h.adminDB.(*nosql.DB); ok { api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "operation not supported in standalone mode")) diff --git a/authority/admin/context.go b/authority/admin/context.go deleted file mode 100644 index 87bf3e03..00000000 --- a/authority/admin/context.go +++ /dev/null @@ -1,10 +0,0 @@ -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") -) diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index 04aac0b7..e5b8e406 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -34,9 +34,9 @@ type SCEP struct { Options *Options `json:"options,omitempty"` Claims *Claims `json:"claims,omitempty"` ctl *Controller - x509Policy policy.X509Policy secretChallengePassword string encryptionAlgorithm int + x509Policy policy.X509Policy } // GetID returns the provisioner unique identifier. diff --git a/authority/tls.go b/authority/tls.go index 867e2c51..13babdf1 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -231,10 +231,6 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } // 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) { // if no policy is configured, the cert is implicitly allowed From 0e052fe2993d2fa398b013e0ffef845eeb35661d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 30 Mar 2022 14:21:39 +0200 Subject: [PATCH 21/78] Add authority policy API --- authority/admin/api/acme.go | 2 +- authority/admin/api/acme_test.go | 11 ++- authority/admin/api/handler.go | 18 ++-- authority/admin/api/middleware.go | 38 +++++++- authority/admin/api/middleware_test.go | 10 +- authority/admin/api/policy.go | 122 +++++++---------------- authority/authority.go | 1 + authority/policy.go | 82 ++++++++++++---- ca/adminClient.go | 129 +++++++++++++++++++++++++ 9 files changed, 284 insertions(+), 129 deletions(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 39be50c7..88bed2f5 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -35,7 +35,7 @@ type GetExternalAccountKeysResponse struct { // requireEABEnabled is a middleware that ensures ACME EAB is enabled // before serving requests that act on ACME EAB credentials. -func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { +func (h *Handler) requireEABEnabled(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() provName := chi.URLParam(r, "provisionerName") diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 6ffe1418..5c61656d 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -12,12 +12,15 @@ import ( "testing" "github.com/go-chi/chi" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "go.step.sm/linkedca" + "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/linkedca" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" ) func readProtoJSON(r io.ReadCloser, m proto.Message) error { @@ -34,7 +37,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { ctx context.Context adminDB admin.DB auth adminAuthority - next nextHTTP + next http.HandlerFunc err *admin.Error statusCode int } diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 0dd45cb0..aa7b6300 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -1,6 +1,8 @@ package api import ( + "net/http" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" @@ -29,19 +31,19 @@ func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeRespo // Route traffic and implement the Router interface. func (h *Handler) Route(r api.Router) { - authnz := func(next nextHTTP) nextHTTP { + authnz := func(next http.HandlerFunc) http.HandlerFunc { return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) } - requireEABEnabled := func(next nextHTTP) nextHTTP { + requireEABEnabled := func(next http.HandlerFunc) http.HandlerFunc { return h.requireEABEnabled(next) } - enabledInStandalone := func(next nextHTTP) nextHTTP { + enabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc { return h.checkAction(next, true) } - disabledInStandalone := func(next nextHTTP) nextHTTP { + disabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc { return h.checkAction(next, false) } @@ -73,10 +75,10 @@ func (h *Handler) Route(r api.Router) { // Policy - Provisioner //r.MethodFunc("GET", "/provisioners/{name}/policy", noauth(h.policyResponder.GetProvisionerPolicy)) - r.MethodFunc("GET", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.GetProvisionerPolicy))) - r.MethodFunc("POST", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.CreateProvisionerPolicy))) - r.MethodFunc("PUT", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.UpdateProvisionerPolicy))) - r.MethodFunc("DELETE", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.DeleteProvisionerPolicy))) + r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.GetProvisionerPolicy)))) + r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.CreateProvisionerPolicy)))) + r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.UpdateProvisionerPolicy)))) + r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.DeleteProvisionerPolicy)))) // Policy - ACME Account // TODO: ensure we don't clash with eab; might want to change eab paths slightly (as long as we don't have it released completely; needs changes in adminClient too) diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 1acc661e..426d1d42 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -5,16 +5,17 @@ import ( "go.step.sm/linkedca" + "github.com/go-chi/chi" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin/db/nosql" + "github.com/smallstep/certificates/authority/provisioner" ) -type nextHTTP = func(http.ResponseWriter, *http.Request) - // requireAPIEnabled is a middleware that ensures the Administration API // is enabled before servicing requests. -func (h *Handler) requireAPIEnabled(next nextHTTP) nextHTTP { +func (h *Handler) requireAPIEnabled(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !h.auth.IsAdminAPIEnabled() { api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, @@ -26,7 +27,7 @@ func (h *Handler) requireAPIEnabled(next nextHTTP) nextHTTP { } // extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token. -func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { +func (h *Handler) extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { tok := r.Header.Get("Authorization") @@ -47,8 +48,35 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { } } +// loadProvisioner is a middleware that searches for a provisioner +// by name and stores it in the context. +func (h *Handler) loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + name := chi.URLParam(r, "provisionerName") + var ( + p provisioner.Interface + err error + ) + if p, err = h.auth.LoadProvisionerByName(name); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) + return + } + + prov, err := h.adminDB.GetProvisioner(ctx, p.GetID()) + if err != nil { + api.WriteError(w, err) + return + } + + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + next(w, r.WithContext(ctx)) + } +} + // checkAction checks if an action is supported in standalone or not -func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTTP { +func (h *Handler) checkAction(next http.HandlerFunc, supportedInStandalone bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // actions allowed in standalone mode are always supported diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index 158374d0..54732dc6 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -12,17 +12,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/types/known/timestamppb" + + "go.step.sm/linkedca" + "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" - "go.step.sm/linkedca" - "google.golang.org/protobuf/types/known/timestamppb" ) func TestHandler_requireAPIEnabled(t *testing.T) { type test struct { ctx context.Context auth adminAuthority - next nextHTTP + next http.HandlerFunc err *admin.Error statusCode int } @@ -102,7 +104,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { ctx context.Context auth adminAuthority req *http.Request - next nextHTTP + next http.HandlerFunc err *admin.Error statusCode int } diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 6b59803f..eb08e38b 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -3,14 +3,11 @@ package api import ( "net/http" - "github.com/go-chi/chi" - "go.step.sm/linkedca" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api/read" "github.com/smallstep/certificates/authority/admin" - "github.com/smallstep/certificates/authority/provisioner" ) type policyAdminResponderInterface interface { @@ -54,7 +51,7 @@ func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht } if policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } @@ -117,7 +114,7 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r } if policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } @@ -152,7 +149,7 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r } if policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } @@ -167,27 +164,12 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r // GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) { - // TODO: move getting provisioner to middleware? - ctx := r.Context() - name := chi.URLParam(r, "name") - var ( - p provisioner.Interface - err error - ) - if p, err = par.auth.LoadProvisionerByName(name); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) - return - } - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) - if err != nil { - api.WriteError(w, err) - return - } + prov := linkedca.ProvisionerFromContext(r.Context()) policy := prov.GetPolicy() if policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } @@ -196,41 +178,28 @@ func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r * // CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - name := chi.URLParam(r, "name") - var ( - p provisioner.Interface - err error - ) - if p, err = par.auth.LoadProvisionerByName(name); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) - return - } - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) - if err != nil { - api.WriteError(w, err) - return - } + ctx := r.Context() + prov := linkedca.ProvisionerFromContext(ctx) policy := prov.GetPolicy() if policy != nil { - adminErr := admin.NewError(admin.ErrorBadRequestType, "provisioner %s already has a policy", name) + adminErr := admin.NewError(admin.ErrorBadRequestType, "provisioner %s already has a policy", prov.Name) adminErr.Status = http.StatusConflict api.WriteError(w, adminErr) + return } var newPolicy = new(linkedca.Policy) - if err := read.ProtoJSON(r.Body, newPolicy); err != nil { - api.WriteError(w, err) + if !api.ReadProtoJSONWithCheck(w, r.Body, newPolicy) { return } prov.Policy = newPolicy - err = par.auth.UpdateProvisioner(ctx, prov) + err := par.auth.UpdateProvisioner(ctx, prov) if err != nil { - api.WriteError(w, err) + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy")) return } @@ -239,88 +208,65 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, // UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() - name := chi.URLParam(r, "name") - var ( - p provisioner.Interface - err error - ) - if p, err = par.auth.LoadProvisionerByName(name); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) + prov := linkedca.ProvisionerFromContext(ctx) + + if prov.Policy == nil { + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) + var newPolicy = new(linkedca.Policy) + if !api.ReadProtoJSONWithCheck(w, r.Body, newPolicy) { + return + } + + prov.Policy = newPolicy + err := par.auth.UpdateProvisioner(ctx, prov) if err != nil { - api.WriteError(w, err) + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy")) return } - var policy = new(linkedca.Policy) - if err := read.ProtoJSON(r.Body, policy); err != nil { - api.WriteError(w, err) - return - } - - prov.Policy = policy - err = par.auth.UpdateProvisioner(ctx, prov) - if err != nil { - api.WriteError(w, err) - return - } - - api.ProtoJSONStatus(w, policy, http.StatusOK) + api.ProtoJSONStatus(w, newPolicy, http.StatusOK) } // DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - name := chi.URLParam(r, "name") - var ( - p provisioner.Interface - err error - ) - if p, err = par.auth.LoadProvisionerByName(name); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) - return - } - - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) - if err != nil { - api.WriteError(w, err) - return - } + prov := linkedca.ProvisionerFromContext(ctx) if prov.Policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } // remove the policy prov.Policy = nil - err = par.auth.UpdateProvisioner(ctx, prov) + err := par.auth.UpdateProvisioner(ctx, prov) if err != nil { api.WriteError(w, err) return } - api.JSON(w, &DeleteResponse{Status: "ok"}) + api.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) } func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - api.JSON(w, "ok") + api.JSON(w, "not implemented yet") } func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - api.JSON(w, "ok") + api.JSON(w, "not implemented yet") } func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - api.JSON(w, "ok") + api.JSON(w, "not implemented yet") } func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - api.JSON(w, "ok") + api.JSON(w, "not implemented yet") } diff --git a/authority/authority.go b/authority/authority.go index f77cc876..4352bc23 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -218,6 +218,7 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { err error policyOptions *policy.Options ) + // if admin API is enabled, the CA is running in linked mode if a.config.AuthorityConfig.EnableAdmin { linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) if err != nil { diff --git a/authority/policy.go b/authority/policy.go index 4f93899f..f94cd302 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -77,6 +77,8 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { return nil } +// 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 { // convert the policy; return early if nil @@ -90,9 +92,15 @@ func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *lin return admin.WrapErrorISE(err, "error creating temporary policy engine") } + // when an empty policy is provided, the resulting engine is nil + // and there's no policy to evaluate. + if engine == nil { + return nil + } + // TODO(hs): Provide option to force the policy, even when the admin subject would be locked out? - sans := []string{adm.Subject} + sans := []string{adm.GetSubject()} if err := isAllowed(engine, sans); err != nil { return err } @@ -151,16 +159,32 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { // fill x509 policy configuration if p.X509 != nil { if p.X509.Allow != nil { - opts.X509.AllowedNames.DNSDomains = p.X509.Allow.Dns - opts.X509.AllowedNames.IPRanges = p.X509.Allow.Ips - opts.X509.AllowedNames.EmailAddresses = p.X509.Allow.Emails - opts.X509.AllowedNames.URIDomains = p.X509.Allow.Uris + if p.X509.Allow.Dns != nil { + opts.X509.AllowedNames.DNSDomains = p.X509.Allow.Dns + } + if p.X509.Allow.Ips != nil { + opts.X509.AllowedNames.IPRanges = p.X509.Allow.Ips + } + if p.X509.Allow.Emails != nil { + opts.X509.AllowedNames.EmailAddresses = p.X509.Allow.Emails + } + if p.X509.Allow.Uris != nil { + opts.X509.AllowedNames.URIDomains = p.X509.Allow.Uris + } } if p.X509.Deny != nil { - opts.X509.DeniedNames.DNSDomains = p.X509.Deny.Dns - opts.X509.DeniedNames.IPRanges = p.X509.Deny.Ips - opts.X509.DeniedNames.EmailAddresses = p.X509.Deny.Emails - opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris + if p.X509.Deny.Dns != nil { + opts.X509.DeniedNames.DNSDomains = p.X509.Deny.Dns + } + if p.X509.Deny.Ips != nil { + opts.X509.DeniedNames.IPRanges = p.X509.Deny.Ips + } + if p.X509.Deny.Emails != nil { + opts.X509.DeniedNames.EmailAddresses = p.X509.Deny.Emails + } + if p.X509.Deny.Uris != nil { + opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris + } } } @@ -168,24 +192,44 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { if p.Ssh != nil { if p.Ssh.Host != nil { if p.Ssh.Host.Allow != nil { - opts.SSH.Host.AllowedNames.DNSDomains = p.Ssh.Host.Allow.Dns - opts.SSH.Host.AllowedNames.IPRanges = p.Ssh.Host.Allow.Ips - opts.SSH.Host.AllowedNames.EmailAddresses = p.Ssh.Host.Allow.Principals + if p.Ssh.Host.Allow.Dns != nil { + opts.SSH.Host.AllowedNames.DNSDomains = p.Ssh.Host.Allow.Dns + } + if p.Ssh.Host.Allow.Ips != nil { + opts.SSH.Host.AllowedNames.IPRanges = p.Ssh.Host.Allow.Ips + } + if p.Ssh.Host.Allow.Principals != nil { + opts.SSH.Host.AllowedNames.Principals = p.Ssh.Host.Allow.Principals + } } if p.Ssh.Host.Deny != nil { - opts.SSH.Host.DeniedNames.DNSDomains = p.Ssh.Host.Deny.Dns - opts.SSH.Host.DeniedNames.IPRanges = p.Ssh.Host.Deny.Ips - opts.SSH.Host.DeniedNames.Principals = p.Ssh.Host.Deny.Principals + if p.Ssh.Host.Deny.Dns != nil { + opts.SSH.Host.DeniedNames.DNSDomains = p.Ssh.Host.Deny.Dns + } + if p.Ssh.Host.Deny.Ips != nil { + opts.SSH.Host.DeniedNames.IPRanges = p.Ssh.Host.Deny.Ips + } + if p.Ssh.Host.Deny.Principals != nil { + opts.SSH.Host.DeniedNames.Principals = p.Ssh.Host.Deny.Principals + } } } if p.Ssh.User != nil { if p.Ssh.User.Allow != nil { - opts.SSH.User.AllowedNames.EmailAddresses = p.Ssh.User.Allow.Emails - opts.SSH.User.AllowedNames.Principals = p.Ssh.User.Allow.Principals + if p.Ssh.User.Allow.Emails != nil { + opts.SSH.User.AllowedNames.EmailAddresses = p.Ssh.User.Allow.Emails + } + if p.Ssh.User.Allow.Principals != nil { + opts.SSH.User.AllowedNames.Principals = p.Ssh.User.Allow.Principals + } } if p.Ssh.User.Deny != nil { - opts.SSH.User.DeniedNames.EmailAddresses = p.Ssh.User.Deny.Emails - opts.SSH.User.DeniedNames.Principals = p.Ssh.User.Deny.Principals + if p.Ssh.User.Deny.Emails != nil { + opts.SSH.User.DeniedNames.EmailAddresses = p.Ssh.User.Deny.Emails + } + if p.Ssh.User.Deny.Principals != nil { + opts.SSH.User.DeniedNames.Principals = p.Ssh.User.Deny.Principals + } } } } diff --git a/ca/adminClient.go b/ca/adminClient.go index 5f3993b1..f972f9f8 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/x509" "encoding/json" + "fmt" "io" "net/http" "net/url" @@ -679,6 +680,134 @@ retry: return nil } +func (c *AdminClient) GetAuthorityPolicy() (*linkedca.Policy, error) { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("creating GET %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client GET %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) CreateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Policy, error) { + var retried bool + body, err := protojson.Marshal(p) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating POST %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client POST %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) UpdateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Policy, error) { + var retried bool + body, err := protojson.Marshal(p) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client PUT %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) RemoveAuthorityPolicy() error { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody) + if err != nil { + return fmt.Errorf("creating DELETE %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("client DELETE %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return readAdminError(resp.Body) + } + return nil +} + func readAdminError(r io.ReadCloser) error { // TODO: not all errors can be read (i.e. 404); seems to be a bigger issue defer r.Close() From 628d7448de81cca51e356c7c332bbd673f2ded1c Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 30 Mar 2022 15:20:38 +0200 Subject: [PATCH 22/78] Don't return policy in provisioner JSON --- api/read/read.go | 2 +- authority/provisioner/options.go | 4 ++-- authority/provisioner/ssh_options.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/read/read.go b/api/read/read.go index dd101dcf..30f55886 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -34,7 +34,7 @@ func ProtoJSON(r io.Reader, m proto.Message) error { } // ProtoJSONWithCheck reads JSON from the request body and stores it in the value -// pointed to by v. +// pointed to by v. Returns false if an error was written; true if not. func ProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) bool { data, err := io.ReadAll(r) if err != nil { diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 7725c8b0..0975a4c2 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -61,10 +61,10 @@ type X509Options struct { TemplateData json.RawMessage `json:"templateData,omitempty"` // AllowedNames contains the SANs the provisioner is authorized to sign - AllowedNames *policy.X509NameOptions + AllowedNames *policy.X509NameOptions `json:"-"` // DeniedNames contains the SANs the provisioner is not authorized to sign - DeniedNames *policy.X509NameOptions + DeniedNames *policy.X509NameOptions `json:"-"` } // HasTemplate returns true if a template is defined in the provisioner options. diff --git a/authority/provisioner/ssh_options.go b/authority/provisioner/ssh_options.go index 92c5826b..93633a21 100644 --- a/authority/provisioner/ssh_options.go +++ b/authority/provisioner/ssh_options.go @@ -37,10 +37,10 @@ type SSHOptions struct { TemplateData json.RawMessage `json:"templateData,omitempty"` // User contains SSH user certificate options. - User *policy.SSHUserCertificateOptions + User *policy.SSHUserCertificateOptions `json:"-"` // Host contains SSH host certificate options. - Host *policy.SSHHostCertificateOptions + Host *policy.SSHHostCertificateOptions `json:"-"` } // GetAllowedUserNameOptions returns the SSHNameOptions that are From 6da243c34d293d003984ae874b2a6f6cba83c9c8 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 30 Mar 2022 15:39:03 +0200 Subject: [PATCH 23/78] Add policy precheck for all admins --- authority/policy.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/authority/policy.go b/authority/policy.go index f94cd302..bb57a7d0 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -100,13 +100,32 @@ func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *lin // TODO(hs): Provide option to force the policy, even when the admin subject would be locked out? + // 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()} 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? + // 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 { + sans = []string{adm.GetSubject()} + if err := isAllowed(engine, sans); err != nil { + return err + } + } + + // TODO(hs): mask the error message for non-super admins? return nil } From bfa4d809fd63d862b09fad7913b4245af80db035 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 30 Mar 2022 18:21:25 +0200 Subject: [PATCH 24/78] Improve middleware test coverage --- authority/admin/api/acme.go | 55 ++-- authority/admin/api/acme_test.go | 334 ++++--------------------- authority/admin/api/handler.go | 18 +- authority/admin/api/middleware.go | 4 +- authority/admin/api/middleware_test.go | 136 ++++++++++ 5 files changed, 206 insertions(+), 341 deletions(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 01d706ab..f671059e 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -1,17 +1,13 @@ package api import ( - "context" "fmt" "net/http" - "github.com/go-chi/chi" - "go.step.sm/linkedca" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority/admin" - "github.com/smallstep/certificates/authority/provisioner" ) // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests @@ -38,50 +34,29 @@ type GetExternalAccountKeysResponse struct { func (h *Handler) requireEABEnabled(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - provName := chi.URLParam(r, "provisionerName") - eabEnabled, prov, err := h.provisionerHasEABEnabled(ctx, provName) - if err != nil { - render.Error(w, err) + prov := linkedca.ProvisionerFromContext(ctx) + + details := prov.GetDetails() + if details == nil { + render.Error(w, admin.NewErrorISE("error getting details for provisioner '%s'", prov.GetName())) return } - if !eabEnabled { - render.Error(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov.GetName())) + + acmeProvisioner := details.GetACME() + if acmeProvisioner == nil { + render.Error(w, admin.NewErrorISE("error getting ACME details for provisioner '%s'", prov.GetName())) return } - ctx = linkedca.NewContextWithProvisioner(ctx, prov) + + if !acmeProvisioner.RequireEab { + render.Error(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner '%s'", prov.GetName())) + return + } + next(w, r.WithContext(ctx)) } } -// provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME -// provisioner is set to true and thus has EAB enabled. -func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, *linkedca.Provisioner, error) { - var ( - p provisioner.Interface - err error - ) - if p, err = h.auth.LoadProvisionerByName(provisionerName); err != nil { - return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName) - } - - prov, err := h.adminDB.GetProvisioner(ctx, p.GetID()) - if err != nil { - return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID()) - } - - details := prov.GetDetails() - if details == nil { - return false, nil, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID()) - } - - acmeProvisioner := details.GetACME() - if acmeProvisioner == nil { - return false, nil, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID()) - } - - return acmeProvisioner.GetRequireEab(), prov, nil -} - type acmeAdminResponderInterface interface { GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 5c61656d..2c7bbd37 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "io" "net/http" "net/http/httptest" @@ -20,7 +19,6 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" - "github.com/smallstep/certificates/authority/provisioner" ) func readProtoJSON(r io.ReadCloser, m proto.Message) error { @@ -35,106 +33,76 @@ func readProtoJSON(r io.ReadCloser, m proto.Message) error { func TestHandler_requireEABEnabled(t *testing.T) { type test struct { ctx context.Context - adminDB admin.DB - auth adminAuthority next http.HandlerFunc err *admin.Error statusCode int } var tests = map[string]func(t *testing.T) test{ - "fail/h.provisionerHasEABEnabled": func(t *testing.T) test { - chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("provisionerName", "provName") - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "provName", name) - return nil, errors.New("force") - }, + "fail/prov.GetDetails": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", } - err := admin.NewErrorISE("error loading provisioner provName: force") - err.Message = "error loading provisioner provName: force" + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewErrorISE("error getting details for provisioner 'provName'") + err.Message = "error getting details for provisioner 'provName'" + return test{ + ctx: ctx, + err: err, + statusCode: 500, + } + }, + "fail/details.GetACME": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewErrorISE("error getting ACME details for provisioner 'provName'") + err.Message = "error getting ACME details for provisioner 'provName'" return test{ ctx: ctx, - auth: auth, err: err, statusCode: 500, } }, "ok/eab-disabled": func(t *testing.T) test { - chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("provisionerName", "provName") - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "provName", name) - return &provisioner.MockProvisioner{ - MgetID: func() string { - return "provID" + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: false, }, - }, nil - }, - } - db := &admin.MockDB{ - MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { - assert.Equals(t, "provID", id) - return &linkedca.Provisioner{ - Id: "provID", - Name: "provName", - Details: &linkedca.ProvisionerDetails{ - Data: &linkedca.ProvisionerDetails_ACME{ - ACME: &linkedca.ACMEProvisioner{ - RequireEab: false, - }, - }, - }, - }, nil + }, }, } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) err := admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner provName") - err.Message = "ACME EAB not enabled for provisioner provName" + err.Message = "ACME EAB not enabled for provisioner 'provName'" return test{ ctx: ctx, - auth: auth, - adminDB: db, err: err, statusCode: 400, } }, "ok/eab-enabled": func(t *testing.T) test { - chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("provisionerName", "provName") - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "provName", name) - return &provisioner.MockProvisioner{ - MgetID: func() string { - return "provID" + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: true, }, - }, nil - }, - } - db := &admin.MockDB{ - MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { - assert.Equals(t, "provID", id) - return &linkedca.Provisioner{ - Id: "provID", - Name: "provName", - Details: &linkedca.ProvisionerDetails{ - Data: &linkedca.ProvisionerDetails_ACME{ - ACME: &linkedca.ACMEProvisioner{ - RequireEab: true, - }, - }, - }, - }, nil + }, }, } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) return test{ - ctx: ctx, - auth: auth, - adminDB: db, + ctx: ctx, next: func(w http.ResponseWriter, r *http.Request) { w.Write(nil) // mock response with status 200 }, @@ -146,13 +114,9 @@ func TestHandler_requireEABEnabled(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: nil, - } + h := &Handler{} - req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req := httptest.NewRequest("GET", "/foo", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.requireEABEnabled(tc.next)(w, req) @@ -179,216 +143,6 @@ func TestHandler_requireEABEnabled(t *testing.T) { } } -func TestHandler_provisionerHasEABEnabled(t *testing.T) { - type test struct { - adminDB admin.DB - auth adminAuthority - provisionerName string - want bool - err *admin.Error - } - var tests = map[string]func(t *testing.T) test{ - "fail/auth.LoadProvisionerByName": func(t *testing.T) test { - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "provName", name) - return nil, errors.New("force") - }, - } - return test{ - auth: auth, - provisionerName: "provName", - want: false, - err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), - } - }, - "fail/db.GetProvisioner": func(t *testing.T) test { - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "provName", name) - return &provisioner.MockProvisioner{ - MgetID: func() string { - return "provID" - }, - }, nil - }, - } - db := &admin.MockDB{ - MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { - assert.Equals(t, "provID", id) - return nil, errors.New("force") - }, - } - return test{ - auth: auth, - adminDB: db, - provisionerName: "provName", - want: false, - err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), - } - }, - "fail/prov.GetDetails": func(t *testing.T) test { - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "provName", name) - return &provisioner.MockProvisioner{ - MgetID: func() string { - return "provID" - }, - }, nil - }, - } - db := &admin.MockDB{ - MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { - assert.Equals(t, "provID", id) - return &linkedca.Provisioner{ - Id: "provID", - Name: "provName", - Details: nil, - }, nil - }, - } - return test{ - auth: auth, - adminDB: db, - provisionerName: "provName", - want: false, - err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), - } - }, - "fail/details.GetACME": func(t *testing.T) test { - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "provName", name) - return &provisioner.MockProvisioner{ - MgetID: func() string { - return "provID" - }, - }, nil - }, - } - db := &admin.MockDB{ - MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { - assert.Equals(t, "provID", id) - return &linkedca.Provisioner{ - Id: "provID", - Name: "provName", - Details: &linkedca.ProvisionerDetails{ - Data: &linkedca.ProvisionerDetails_ACME{ - ACME: nil, - }, - }, - }, nil - }, - } - return test{ - auth: auth, - adminDB: db, - provisionerName: "provName", - want: false, - err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), - } - }, - "ok/eab-disabled": func(t *testing.T) test { - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "eab-disabled", name) - return &provisioner.MockProvisioner{ - MgetID: func() string { - return "provID" - }, - }, nil - }, - } - db := &admin.MockDB{ - MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { - assert.Equals(t, "provID", id) - return &linkedca.Provisioner{ - Id: "provID", - Name: "eab-disabled", - Details: &linkedca.ProvisionerDetails{ - Data: &linkedca.ProvisionerDetails_ACME{ - ACME: &linkedca.ACMEProvisioner{ - RequireEab: false, - }, - }, - }, - }, nil - }, - } - return test{ - adminDB: db, - auth: auth, - provisionerName: "eab-disabled", - want: false, - } - }, - "ok/eab-enabled": func(t *testing.T) test { - auth := &mockAdminAuthority{ - MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { - assert.Equals(t, "eab-enabled", name) - return &provisioner.MockProvisioner{ - MgetID: func() string { - return "provID" - }, - }, nil - }, - } - db := &admin.MockDB{ - MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { - assert.Equals(t, "provID", id) - return &linkedca.Provisioner{ - Id: "provID", - Name: "eab-enabled", - Details: &linkedca.ProvisionerDetails{ - Data: &linkedca.ProvisionerDetails_ACME{ - ACME: &linkedca.ACMEProvisioner{ - RequireEab: true, - }, - }, - }, - }, nil - }, - } - return test{ - adminDB: db, - auth: auth, - provisionerName: "eab-enabled", - want: true, - } - }, - } - for name, prep := range tests { - tc := prep(t) - t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: nil, - } - got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName) - if (err != nil) != (tc.err != nil) { - t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err) - return - } - if tc.err != nil { - assert.Type(t, &linkedca.Provisioner{}, prov) - assert.Type(t, &admin.Error{}, err) - adminError, _ := err.(*admin.Error) - assert.Equals(t, tc.err.Type, adminError.Type) - assert.Equals(t, tc.err.Status, adminError.Status) - assert.Equals(t, tc.err.StatusCode(), adminError.StatusCode()) - assert.Equals(t, tc.err.Message, adminError.Message) - assert.Equals(t, tc.err.Detail, adminError.Detail) - return - } - if got != tc.want { - t.Errorf("Handler.provisionerHasEABEnabled() = %v, want %v", got, tc.want) - } - }) - } -} - func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { type fields struct { Reference string diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index aa7b6300..c8ad316b 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -62,10 +62,10 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin)) // ACME External Account Binding Keys - r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys))) - r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys))) - r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey))) - r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey))) + r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(h.loadProvisionerByName(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))) + r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(h.loadProvisionerByName(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))) + r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(h.loadProvisionerByName(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey)))) + r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(h.loadProvisionerByName(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey)))) // Policy - Authority r.MethodFunc("GET", "/policy", authnz(enabledInStandalone(h.policyResponder.GetAuthorityPolicy))) @@ -74,16 +74,14 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("DELETE", "/policy", authnz(enabledInStandalone(h.policyResponder.DeleteAuthorityPolicy))) // Policy - Provisioner - //r.MethodFunc("GET", "/provisioners/{name}/policy", noauth(h.policyResponder.GetProvisionerPolicy)) r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.GetProvisionerPolicy)))) r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.CreateProvisionerPolicy)))) r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.UpdateProvisionerPolicy)))) r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.DeleteProvisionerPolicy)))) // Policy - ACME Account - // TODO: ensure we don't clash with eab; might want to change eab paths slightly (as long as we don't have it released completely; needs changes in adminClient too) - r.MethodFunc("GET", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.GetACMEAccountPolicy))) - r.MethodFunc("POST", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.CreateACMEAccountPolicy))) - r.MethodFunc("PUT", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.UpdateACMEAccountPolicy))) - r.MethodFunc("DELETE", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.DeleteACMEAccountPolicy))) + r.MethodFunc("GET", "/acme/policy/{provisionerName}/{accountID}", authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.policyResponder.GetACMEAccountPolicy))))) + r.MethodFunc("POST", "/acme/policy/{provisionerName}/{accountID}", authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.policyResponder.CreateACMEAccountPolicy))))) + r.MethodFunc("PUT", "/acme/policy/{provisionerName}/{accountID}", authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.policyResponder.UpdateACMEAccountPolicy))))) + r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/{accountID}", authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.policyResponder.DeleteACMEAccountPolicy))))) } diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 01d6d61f..98477a5e 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -59,6 +59,8 @@ func (h *Handler) loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc p provisioner.Interface err error ) + + // TODO(hs): distinguish 404 vs. 500 if p, err = h.auth.LoadProvisionerByName(name); err != nil { render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) return @@ -66,7 +68,7 @@ func (h *Handler) loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc prov, err := h.adminDB.GetProvisioner(ctx, p.GetID()) if err != nil { - render.Error(w, err) + render.Error(w, admin.WrapErrorISE(err, "error retrieving provisioner %s", name)) return } diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index 54732dc6..c7314e71 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -4,12 +4,14 @@ import ( "bytes" "context" "encoding/json" + "errors" "io" "net/http" "net/http/httptest" "testing" "time" + "github.com/go-chi/chi" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/types/known/timestamppb" @@ -18,6 +20,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" ) func TestHandler_requireAPIEnabled(t *testing.T) { @@ -220,3 +223,136 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { }) } } + +func TestHandler_loadProvisionerByName(t *testing.T) { + type test struct { + adminDB admin.DB + auth adminAuthority + ctx context.Context + next http.HandlerFunc + err *admin.Error + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + err := admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName") + err.Message = "error loading provisioner provName: force" + return test{ + ctx: ctx, + auth: auth, + statusCode: 500, + err: err, + } + }, + "fail/db.GetProvisioner": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + err := admin.WrapErrorISE(errors.New("force"), "error retrieving provisioner provName") + err.Message = "error retrieving provisioner provName: force" + return test{ + ctx: ctx, + auth: auth, + adminDB: db, + statusCode: 500, + err: err, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + }, nil + }, + } + return test{ + ctx: ctx, + auth: auth, + adminDB: db, + statusCode: 200, + next: func(w http.ResponseWriter, r *http.Request) { + prov := linkedca.ProvisionerFromContext(r.Context()) + assert.NotNil(t, prov) + assert.Equals(t, "provID", prov.GetId()) + assert.Equals(t, "provName", prov.GetName()) + w.Write(nil) // mock response with status 200 + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + adminDB: tc.adminDB, + } + + req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + + w := httptest.NewRecorder() + h.loadProvisionerByName(tc.next)(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 + } + }) + } +} From 571b21abbc5989fbda0bfdb748df6b2bc1496cdf Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 31 Mar 2022 16:12:29 +0200 Subject: [PATCH 25/78] Fix (most) PR comments --- api/read/read.go | 3 +- authority/admin/api/acme.go | 2 +- authority/admin/api/handler.go | 52 +- authority/admin/api/middleware.go | 9 +- authority/admin/api/policy.go | 22 +- authority/admin/db/nosql/policy.go | 32 -- authority/admin/db/nosql/provisioner.go | 4 - authority/authority.go | 19 +- authority/config/config.go | 4 +- authority/linkedca.go | 12 +- authority/policy/options.go | 1 + authority/provisioner/jwk.go | 13 - authority/provisioner/k8sSA.go | 8 +- authority/provisioner/nebula.go | 8 +- authority/provisioner/oidc.go | 8 - authority/provisioner/sign_options.go | 2 +- authority/provisioner/sign_ssh_options.go | 4 +- authority/provisioner/sshpop.go | 2 - authority/ssh.go | 6 +- authority/tls.go | 2 +- policy/engine.go | 583 +--------------------- policy/engine_test.go | 14 +- policy/ssh.go | 2 +- policy/validate.go | 580 +++++++++++++++++++++ policy/x509.go | 4 +- 25 files changed, 682 insertions(+), 714 deletions(-) create mode 100644 policy/validate.go diff --git a/api/read/read.go b/api/read/read.go index 30f55886..f4067cb8 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -34,7 +34,7 @@ func ProtoJSON(r io.Reader, m proto.Message) error { } // ProtoJSONWithCheck reads JSON from the request body and stores it in the value -// pointed to by v. Returns false if an error was written; true if not. +// pointed to by m. Returns false if an error was written; true if not. func ProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) bool { data, err := io.ReadAll(r) if err != nil { @@ -57,6 +57,7 @@ func ProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) boo if err := protojson.Unmarshal(data, m); err != nil { if errors.Is(err, proto.Error) { var wrapper = struct { + // TODO(hs): more properties in the error response? Message string `json:"message"` }{ Message: err.Error(), diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index f671059e..0f01b009 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -53,7 +53,7 @@ func (h *Handler) requireEABEnabled(next http.HandlerFunc) http.HandlerFunc { return } - next(w, r.WithContext(ctx)) + next(w, r) } } diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index c8ad316b..eb0b791a 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -35,10 +35,6 @@ func (h *Handler) Route(r api.Router) { return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) } - requireEABEnabled := func(next http.HandlerFunc) http.HandlerFunc { - return h.requireEABEnabled(next) - } - enabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc { return h.checkAction(next, true) } @@ -47,6 +43,22 @@ func (h *Handler) Route(r api.Router) { return h.checkAction(next, false) } + acmeEABMiddleware := func(next http.HandlerFunc) http.HandlerFunc { + return authnz(h.loadProvisionerByName(h.requireEABEnabled(next))) + } + + authorityPolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc { + return authnz(enabledInStandalone(next)) + } + + provisionerPolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc { + return authnz(disabledInStandalone(h.loadProvisionerByName(next))) + } + + acmePolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc { + return authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(next)))) + } + // Provisioners r.MethodFunc("GET", "/provisioners/{name}", authnz(h.GetProvisioner)) r.MethodFunc("GET", "/provisioners", authnz(h.GetProvisioners)) @@ -62,26 +74,26 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin)) // ACME External Account Binding Keys - r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(h.loadProvisionerByName(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))) - r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(h.loadProvisionerByName(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))) - r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(h.loadProvisionerByName(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey)))) - r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(h.loadProvisionerByName(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey)))) + r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(h.acmeResponder.GetExternalAccountKeys)) + r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(h.acmeResponder.GetExternalAccountKeys)) + r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(h.acmeResponder.CreateExternalAccountKey)) + r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(h.acmeResponder.DeleteExternalAccountKey)) // Policy - Authority - r.MethodFunc("GET", "/policy", authnz(enabledInStandalone(h.policyResponder.GetAuthorityPolicy))) - r.MethodFunc("POST", "/policy", authnz(enabledInStandalone(h.policyResponder.CreateAuthorityPolicy))) - r.MethodFunc("PUT", "/policy", authnz(enabledInStandalone(h.policyResponder.UpdateAuthorityPolicy))) - r.MethodFunc("DELETE", "/policy", authnz(enabledInStandalone(h.policyResponder.DeleteAuthorityPolicy))) + r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(h.policyResponder.GetAuthorityPolicy)) + r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(h.policyResponder.CreateAuthorityPolicy)) + r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(h.policyResponder.UpdateAuthorityPolicy)) + r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(h.policyResponder.DeleteAuthorityPolicy)) // Policy - Provisioner - r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.GetProvisionerPolicy)))) - r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.CreateProvisionerPolicy)))) - r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.UpdateProvisionerPolicy)))) - r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.DeleteProvisionerPolicy)))) + r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(h.policyResponder.GetProvisionerPolicy)) + r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(h.policyResponder.CreateProvisionerPolicy)) + r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(h.policyResponder.UpdateProvisionerPolicy)) + r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(h.policyResponder.DeleteProvisionerPolicy)) // Policy - ACME Account - r.MethodFunc("GET", "/acme/policy/{provisionerName}/{accountID}", authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.policyResponder.GetACMEAccountPolicy))))) - r.MethodFunc("POST", "/acme/policy/{provisionerName}/{accountID}", authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.policyResponder.CreateACMEAccountPolicy))))) - r.MethodFunc("PUT", "/acme/policy/{provisionerName}/{accountID}", authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.policyResponder.UpdateACMEAccountPolicy))))) - r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/{accountID}", authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.policyResponder.DeleteACMEAccountPolicy))))) + r.MethodFunc("GET", "/acme/policy/{provisionerName}/{accountID}", acmePolicyMiddleware(h.policyResponder.GetACMEAccountPolicy)) + r.MethodFunc("POST", "/acme/policy/{provisionerName}/{accountID}", acmePolicyMiddleware(h.policyResponder.CreateACMEAccountPolicy)) + r.MethodFunc("PUT", "/acme/policy/{provisionerName}/{accountID}", acmePolicyMiddleware(h.policyResponder.UpdateACMEAccountPolicy)) + r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/{accountID}", acmePolicyMiddleware(h.policyResponder.DeleteACMEAccountPolicy)) } diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 98477a5e..c30eee10 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -48,7 +48,7 @@ func (h *Handler) extractAuthorizeTokenAdmin(next http.HandlerFunc) http.Handler } } -// loadProvisioner is a middleware that searches for a provisioner +// loadProvisionerByName is a middleware that searches for a provisioner // by name and stores it in the context. func (h *Handler) loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -81,6 +81,13 @@ func (h *Handler) loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc func (h *Handler) checkAction(next http.HandlerFunc, supportedInStandalone bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + // temporarily only support the admin nosql DB + if _, ok := h.adminDB.(*nosql.DB); !ok { + render.Error(w, admin.NewError(admin.ErrorNotImplementedType, + "operation not supported")) + return + } + // actions allowed in standalone mode are always supported if supportedInStandalone { next(w, r) diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 012f497f..da0e1d9c 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -64,12 +64,7 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r 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 { + if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) { render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy")) return } @@ -103,12 +98,7 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r 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 { + if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) { render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy")) return } @@ -256,17 +246,17 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, } func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - render.JSON(w, "not implemented yet") + render.JSONStatus(w, "not implemented yet", http.StatusNotImplemented) } func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - render.JSON(w, "not implemented yet") + render.JSONStatus(w, "not implemented yet", http.StatusNotImplemented) } func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - render.JSON(w, "not implemented yet") + render.JSONStatus(w, "not implemented yet", http.StatusNotImplemented) } func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - render.JSON(w, "not implemented yet") + render.JSONStatus(w, "not implemented yet", http.StatusNotImplemented) } diff --git a/authority/admin/db/nosql/policy.go b/authority/admin/db/nosql/policy.go index 8e11ddb0..d26e44a0 100644 --- a/authority/admin/db/nosql/policy.go +++ b/authority/admin/db/nosql/policy.go @@ -41,9 +41,6 @@ func (db *DB) unmarshalDBAuthorityPolicy(data []byte, authorityID string) (*dbAu if err := json.Unmarshal(data, dba); err != nil { return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", authorityID) } - // if !dba.DeletedAt.IsZero() { - // return nil, admin.NewError(admin.ErrorDeletedType, "admin %s is deleted", authorityID) - // } if dba.AuthorityID != db.authorityID { return nil, admin.NewError(admin.ErrorAuthorityMismatchType, "admin %s is not owned by authority %s", dba.ID, db.authorityID) @@ -63,14 +60,6 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db return dbap, nil } -// func (db *DB) unmarshalAuthorityPolicy(data []byte, authorityID string) (*linkedca.Policy, error) { -// dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID) -// if err != nil { -// return nil, err -// } -// return dbap.convert(), nil -// } - func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { dbap := &dbAuthorityPolicy{ @@ -88,27 +77,6 @@ func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy } func (db *DB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { - // policy := &linkedca.Policy{ - // X509: &linkedca.X509Policy{ - // Allow: &linkedca.X509Names{ - // Dns: []string{".localhost"}, - // }, - // Deny: &linkedca.X509Names{ - // Dns: []string{"denied.localhost"}, - // }, - // }, - // Ssh: &linkedca.SSHPolicy{ - // User: &linkedca.SSHUserPolicy{ - // Allow: &linkedca.SSHUserNames{}, - // Deny: &linkedca.SSHUserNames{}, - // }, - // Host: &linkedca.SSHHostPolicy{ - // Allow: &linkedca.SSHHostNames{}, - // Deny: &linkedca.SSHHostNames{}, - // }, - // }, - // } - dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID) if err != nil { return nil, err diff --git a/authority/admin/db/nosql/provisioner.go b/authority/admin/db/nosql/provisioner.go index 540e3ae2..71d9c8d6 100644 --- a/authority/admin/db/nosql/provisioner.go +++ b/authority/admin/db/nosql/provisioner.go @@ -19,7 +19,6 @@ type dbProvisioner struct { Type linkedca.Provisioner_Type `json:"type"` Name string `json:"name"` Claims *linkedca.Claims `json:"claims"` - Policy *linkedca.Policy `json:"policy"` Details []byte `json:"details"` X509Template *linkedca.Template `json:"x509Template"` SSHTemplate *linkedca.Template `json:"sshTemplate"` @@ -44,7 +43,6 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) { Type: dbp.Type, Name: dbp.Name, Claims: dbp.Claims, - Policy: dbp.Policy, Details: details, X509Template: dbp.X509Template, SshTemplate: dbp.SSHTemplate, @@ -162,7 +160,6 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) Type: prov.Type, Name: prov.Name, Claims: prov.Claims, - Policy: prov.Policy, Details: details, X509Template: prov.X509Template, SSHTemplate: prov.SshTemplate, @@ -190,7 +187,6 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) } nu.Name = prov.Name nu.Claims = prov.Claims - nu.Policy = prov.Policy nu.Details, err = json.Marshal(prov.Details.GetData()) if err != nil { return admin.WrapErrorISE(err, "error marshaling details when updating provisioner %s", prov.Name) diff --git a/authority/authority.go b/authority/authority.go index 4352bc23..5caec0fb 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -12,6 +12,11 @@ import ( "time" "github.com/pkg/errors" + "golang.org/x/crypto/ssh" + + "go.step.sm/crypto/pemutil" + "go.step.sm/linkedca" + "github.com/smallstep/certificates/authority/admin" adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql" "github.com/smallstep/certificates/authority/administrator" @@ -27,9 +32,6 @@ import ( "github.com/smallstep/certificates/scep" "github.com/smallstep/certificates/templates" "github.com/smallstep/nosql" - "go.step.sm/crypto/pemutil" - "go.step.sm/linkedca" - "golang.org/x/crypto/ssh" ) // Authority implements the Certificate Authority internal interface. @@ -220,6 +222,17 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { ) // if admin API is enabled, the CA is running in linked mode if a.config.AuthorityConfig.EnableAdmin { + + // temporarily disable policy loading when LinkedCA is in use + if _, ok := a.adminDB.(*linkedCaClient); ok { + return nil + } + + // temporarily only support the admin nosql DB + if _, ok := a.adminDB.(*adminDBNosql.DB); !ok { + return nil + } + linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) if err != nil { return admin.WrapErrorISE(err, "error getting policy to (re)load policy engines") diff --git a/authority/config/config.go b/authority/config/config.go index 12503965..f23722d9 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -8,13 +8,15 @@ import ( "time" "github.com/pkg/errors" + + "go.step.sm/linkedca" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" cas "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" kms "github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/certificates/templates" - "go.step.sm/linkedca" ) const ( diff --git a/authority/linkedca.go b/authority/linkedca.go index 11c8668c..95812895 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -15,16 +15,18 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/authority/admin" - "github.com/smallstep/certificates/db" + "golang.org/x/crypto/ssh" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/tlsutil" "go.step.sm/crypto/x509util" "go.step.sm/linkedca" - "golang.org/x/crypto/ssh" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" + + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/db" ) const uuidPattern = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" diff --git a/authority/policy/options.go b/authority/policy/options.go index 5c6e6134..e1c33104 100644 --- a/authority/policy/options.go +++ b/authority/policy/options.go @@ -183,6 +183,7 @@ func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions { // names configured. func (o *SSHNameOptions) HasNames() bool { return len(o.DNSDomains) > 0 || + len(o.IPRanges) > 0 || len(o.EmailAddresses) > 0 || len(o.Principals) > 0 } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 36713824..99e49c85 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -210,23 +210,10 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // revocation status. Just confirms that the provisioner that created the // certificate was configured to allow renewals. func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { - // if p.claimer.IsDisableRenewal() { - // return errs.Unauthorized("jwk.AuthorizeRenew; renew is disabled for jwk provisioner '%s'", p.GetName()) - // } // TODO(hs): authorize the SANs using x509 name policy allow/deny rules (also for other provisioners with AuthorizeRewew and AuthorizeSSHRenew) - //return p.authorizeRenew(cert) - // return nil return p.ctl.AuthorizeRenew(ctx, cert) } -// func (p *JWK) authorizeRenew(cert *x509.Certificate) error { -// if p.x509PolicyEngine == nil { -// return nil -// } -// _, err := p.x509PolicyEngine.AreCertificateNamesAllowed(cert) -// return err -// } - // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.ctl.Claimer.IsSSHCAEnabled() { diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index b127ed13..ec813b6c 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -56,7 +56,6 @@ type K8sSA struct { ctl *Controller x509Policy policy.X509Policy sshHostPolicy policy.HostPolicy - sshUserPolicy policy.UserPolicy } // GetID returns the provisioner unique identifier. The name and credential id @@ -149,11 +148,6 @@ func (p *K8sSA) Init(config Config) (err error) { return err } - // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - // Initialize the SSH allow/deny policy engine for host certificates if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err @@ -304,7 +298,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, nil), ), nil } diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index 44212f35..78da1f4c 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -45,7 +45,6 @@ type Nebula struct { ctl *Controller x509Policy policy.X509Policy sshHostPolicy policy.HostPolicy - sshUserPolicy policy.UserPolicy } // Init verifies and initializes the Nebula provisioner. @@ -69,11 +68,6 @@ func (p *Nebula) Init(config Config) (err error) { return err } - // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - // Initialize the SSH allow/deny policy engine for host certificates if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err @@ -276,7 +270,7 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), + newSSHNamePolicyValidator(p.sshHostPolicy, nil), ), nil } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 369ef056..b56d5fa5 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -95,7 +95,6 @@ type OIDC struct { Options *Options `json:"options,omitempty"` configuration openIDConfiguration keyStore *keyStore - getIdentityFunc GetIdentityFunc ctl *Controller x509Policy policy.X509Policy sshHostPolicy policy.HostPolicy @@ -202,13 +201,6 @@ func (o *OIDC) Init(config Config) (err error) { return err } - // Set the identity getter if it exists, otherwise use the default. - if config.GetIdentityFunc == nil { - o.getIdentityFunc = DefaultIdentityFunc - } else { - o.getIdentityFunc = config.GetIdentityFunc - } - // Initialize the x509 allow/deny policy engine if o.x509Policy, err = policy.NewX509PolicyEngine(o.Options.GetX509Options()); err != nil { return err diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index df2551a3..bac40e69 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -422,7 +422,7 @@ func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) e if v.policyEngine == nil { return nil } - _, err := v.policyEngine.AreCertificateNamesAllowed(cert) + _, err := v.policyEngine.IsX509CertificateAllowed(cert) return err } diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index a057b2b9..a41b8bc1 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -480,7 +480,7 @@ func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) if v.hostPolicyEngine == nil && v.userPolicyEngine != nil { return errors.New("SSH host certificate not authorized") } - _, err := v.hostPolicyEngine.ArePrincipalsAllowed(cert) + _, err := v.hostPolicyEngine.IsSSHCertificateAllowed(cert) return err case ssh.UserCert: // when no user policy engine is configured, but a host policy engine is @@ -488,7 +488,7 @@ func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) if v.userPolicyEngine == nil && v.hostPolicyEngine != nil { return errors.New("SSH user certificate not authorized") } - _, err := v.userPolicyEngine.ArePrincipalsAllowed(cert) + _, err := v.userPolicyEngine.IsSSHCertificateAllowed(cert) return err default: return fmt.Errorf("unexpected SSH certificate type %d", cert.CertType) // satisfy return; shouldn't happen diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index 1e841db6..e8bcce7e 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -97,8 +97,6 @@ func (p *SSHPOP) Init(config Config) (err error) { p.sshPubKeys = config.SSHKeys config.Audiences = config.Audiences.WithFragment(p.GetIDForToken()) - - // Update claims with global ones p.ctl, err = NewController(p, p.Claims, config) return } diff --git a/authority/ssh.go b/authority/ssh.go index 7c3df192..f2913566 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -5,11 +5,11 @@ import ( "crypto/rand" "crypto/x509" "encoding/binary" + "errors" "net/http" "strings" "time" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" @@ -250,7 +250,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign ssh user certificates"), "authority.SignSSH: error creating ssh user certificate") } if a.sshUserPolicy != nil { - allowed, err := a.sshUserPolicy.ArePrincipalsAllowed(certTpl) + allowed, err := a.sshUserPolicy.IsSSHCertificateAllowed(certTpl) if err != nil { return nil, errs.InternalServerErr(err, errs.WithMessage("authority.SignSSH: error creating ssh user certificate"), @@ -267,7 +267,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign ssh host certificates"), "authority.SignSSH: error creating ssh user certificate") } if a.sshHostPolicy != nil { - allowed, err := a.sshHostPolicy.ArePrincipalsAllowed(certTpl) + allowed, err := a.sshHostPolicy.IsSSHCertificateAllowed(certTpl) if err != nil { return nil, errs.InternalServerErr(err, errs.WithMessage("authority.SignSSH: error creating ssh host certificate"), diff --git a/authority/tls.go b/authority/tls.go index 13babdf1..bae69279 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -238,7 +238,7 @@ func (a *Authority) isAllowedToSign(cert *x509.Certificate) (bool, error) { return true, nil } - return a.x509Policy.AreCertificateNamesAllowed(cert) + return a.x509Policy.IsX509CertificateAllowed(cert) } // AreSANsAllowed evaluates the provided sans against the diff --git a/policy/engine.go b/policy/engine.go index 63d8452a..afaa2416 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -1,17 +1,13 @@ package policy import ( - "bytes" "crypto/x509" "crypto/x509/pkix" "fmt" "net" "net/url" - "reflect" - "strings" "golang.org/x/crypto/ssh" - "golang.org/x/net/idna" "go.step.sm/crypto/x509util" ) @@ -161,8 +157,8 @@ func removeDuplicateIPRanges(ipRanges []*net.IPNet) []*net.IPNet { return result } -// AreCertificateNamesAllowed verifies that all SANs in a Certificate are allowed. -func (e *NamePolicyEngine) AreCertificateNamesAllowed(cert *x509.Certificate) (bool, error) { +// IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed. +func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) (bool, error) { dnsNames, ips, emails, uris := cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs // when Subject Common Name must be verified in addition to the SANs, it is // added to the appropriate slice of names. @@ -175,8 +171,8 @@ func (e *NamePolicyEngine) AreCertificateNamesAllowed(cert *x509.Certificate) (b return true, nil } -// AreCSRNamesAllowed verifies that all names in the CSR are allowed. -func (e *NamePolicyEngine) AreCSRNamesAllowed(csr *x509.CertificateRequest) (bool, error) { +// IsX509CertificateRequestAllowed verifies that all names in the CSR are allowed. +func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) (bool, error) { dnsNames, ips, emails, uris := csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs // when Subject Common Name must be verified in addition to the SANs, it is // added to the appropriate slice of names. @@ -215,8 +211,8 @@ func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) { return true, nil } -// ArePrincipalsAllowed verifies that all principals in an SSH certificate are allowed. -func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) { +// IsSSHCertificateAllowed verifies that all principals in an SSH certificate are allowed. +func (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) (bool, error) { dnsNames, ips, emails, principals, err := splitSSHPrincipals(cert) if err != nil { return false, err @@ -259,7 +255,7 @@ func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, case ssh.UserCert: // re-using SplitSANs results in anything that can't be parsed as an IP, URI or email // to be considered a username principal. This allows usernames like h.slatman to be present - // in the SSH certificate. We're exluding IPs and URIs, because they can be confusing + // in the SSH certificate. We're exluding URIs, because they can be confusing // when used in a SSH user certificate. principals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) if len(uris) > 0 { @@ -271,568 +267,3 @@ func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, return } - -// 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 - if e.totalNumberOfConstraints == 0 { - 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 - // 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 - // executed so far. - - // TODO: gather all errors, or return early? Currently we return early on the first wrong name; check might fail for multiple names. - // Perhaps make that an option? - for _, dns := range dnsNames { - // if there are DNS names to check, no DNS constraints set, but there are other permitted constraints, - // 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). - if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), - } - } - didCutWildcard := false - if strings.HasPrefix(dns, "*.") { - dns = dns[1:] - didCutWildcard = true - } - parsedDNS, err := idna.Lookup.ToASCII(dns) - if err != nil { - return &NamePolicyError{ - Reason: CannotParseDomain, - Detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns), - } - } - if didCutWildcard { - parsedDNS = "*" + parsedDNS - } - if _, ok := domainToReverseLabels(parsedDNS); !ok { - return &NamePolicyError{ - Reason: CannotParseDomain, - Detail: fmt.Sprintf("cannot parse dns %q", dns), - } - } - if err := checkNameConstraints("dns", dns, parsedDNS, - func(parsedName, constraint interface{}) (bool, error) { - return e.matchDomainConstraint(parsedName.(string), constraint.(string)) - }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { - return err - } - } - - for _, ip := range ips { - if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()), - } - } - if err := checkNameConstraints("ip", ip.String(), ip, - func(parsedName, constraint interface{}) (bool, error) { - return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet)) - }, e.permittedIPRanges, e.excludedIPRanges); err != nil { - return err - } - } - - for _, email := range emailAddresses { - if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email), - } - } - mailbox, ok := parseRFC2821Mailbox(email) - if !ok { - return &NamePolicyError{ - Reason: CannotParseRFC822Name, - Detail: fmt.Sprintf("invalid rfc822Name %q", mailbox), - } - } - // According to RFC 5280, section 7.5, emails are considered to match if the local part is - // an exact match and the host (domain) part matches the ASCII representation (case-insensitive): - // https://datatracker.ietf.org/doc/html/rfc5280#section-7.5 - domainASCII, err := idna.ToASCII(mailbox.domain) - if err != nil { - return &NamePolicyError{ - Reason: CannotParseDomain, - Detail: fmt.Sprintf("cannot parse email domain %q", email), - } - } - mailbox.domain = domainASCII - if err := checkNameConstraints("email", email, mailbox, - func(parsedName, constraint interface{}) (bool, error) { - return e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) - }, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil { - return err - } - } - - // TODO(hs): fix internationalization for URIs (IRIs) - - for _, uri := range uris { - if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()), - } - } - if err := checkNameConstraints("uri", uri.String(), uri, - func(parsedName, constraint interface{}) (bool, error) { - return e.matchURIConstraint(parsedName.(*url.URL), constraint.(string)) - }, e.permittedURIDomains, e.excludedURIDomains); err != nil { - return err - } - } - - for _, principal := range principals { - if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { - return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal), - } - } - // TODO: some validation? I.e. allowed characters? - if err := checkNameConstraints("principal", principal, principal, - func(parsedName, constraint interface{}) (bool, error) { - return matchUsernameConstraint(parsedName.(string), constraint.(string)) - }, e.permittedPrincipals, e.excludedPrincipals); err != nil { - return err - } - } - - // if all checks out, all SANs are allowed - return nil -} - -// 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, - parsedName interface{}, - match func(parsedName, constraint interface{}) (match bool, err error), - permitted, excluded interface{}) error { - - excludedValue := reflect.ValueOf(excluded) - - for i := 0; i < excludedValue.Len(); i++ { - constraint := excludedValue.Index(i).Interface() - match, err := match(parsedName, constraint) - if err != nil { - return &NamePolicyError{ - Reason: CannotMatchNameToConstraint, - Detail: err.Error(), - } - } - - if match { - return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), - } - } - } - - permittedValue := reflect.ValueOf(permitted) - - ok := true - for i := 0; i < permittedValue.Len(); i++ { - constraint := permittedValue.Index(i).Interface() - var err error - if ok, err = match(parsedName, constraint); err != nil { - return &NamePolicyError{ - Reason: CannotMatchNameToConstraint, - Detail: err.Error(), - } - } - - if ok { - break - } - } - - if !ok { - return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), - } - } - - return nil -} - -// 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 { - reverseLabels = append(reverseLabels, domain) - domain = "" - } else { - reverseLabels = append(reverseLabels, domain[i+1:]) - domain = domain[:i] - } - } - - if len(reverseLabels) > 0 && reverseLabels[0] == "" { - // An empty label at the end indicates an absolute value. - return nil, false - } - - for _, label := range reverseLabels { - if label == "" { - // Empty labels are otherwise invalid. - return nil, false - } - - for _, c := range label { - if c < 33 || c > 126 { - // Invalid character. - return nil, false - } - } - } - - return reverseLabels, true -} - -// 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 -} - -// parseRFC2821Mailbox parses an email address into local and domain parts, -// 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 - } - - localPartBytes := make([]byte, 0, len(in)/2) - - if in[0] == '"' { - // Quoted-string = DQUOTE *qcontent DQUOTE - // non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127 - // qcontent = qtext / quoted-pair - // qtext = non-whitespace-control / - // %d33 / %d35-91 / %d93-126 - // quoted-pair = ("\" text) / obs-qp - // text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text - // - // (Names beginning with “obs-” are the obsolete syntax from RFC 2822, - // Section 4. Since it has been 16 years, we no longer accept that.) - in = in[1:] - QuotedString: - for { - if in == "" { - return mailbox, false - } - c := in[0] - in = in[1:] - - switch { - case c == '"': - break QuotedString - - case c == '\\': - // quoted-pair - if in == "" { - return mailbox, false - } - if in[0] == 11 || - in[0] == 12 || - (1 <= in[0] && in[0] <= 9) || - (14 <= in[0] && in[0] <= 127) { - localPartBytes = append(localPartBytes, in[0]) - in = in[1:] - } else { - return mailbox, false - } - - case c == 11 || - c == 12 || - // Space (char 32) is not allowed based on the - // BNF, but RFC 3696 gives an example that - // assumes that it is. Several “verified” - // errata continue to argue about this point. - // We choose to accept it. - c == 32 || - c == 33 || - c == 127 || - (1 <= c && c <= 8) || - (14 <= c && c <= 31) || - (35 <= c && c <= 91) || - (93 <= c && c <= 126): - // qtext - localPartBytes = append(localPartBytes, c) - - default: - return mailbox, false - } - } - } else { - // Atom ("." Atom)* - NextChar: - for len(in) > 0 { - // atext from RFC 2822, Section 3.2.4 - c := in[0] - - switch { - case c == '\\': - // Examples given in RFC 3696 suggest that - // escaped characters can appear outside of a - // quoted string. Several “verified” errata - // continue to argue the point. We choose to - // accept it. - in = in[1:] - if in == "" { - return mailbox, false - } - fallthrough - - case ('0' <= c && c <= '9') || - ('a' <= c && c <= 'z') || - ('A' <= c && c <= 'Z') || - c == '!' || c == '#' || c == '$' || c == '%' || - c == '&' || c == '\'' || c == '*' || c == '+' || - c == '-' || c == '/' || c == '=' || c == '?' || - c == '^' || c == '_' || c == '`' || c == '{' || - c == '|' || c == '}' || c == '~' || c == '.': - localPartBytes = append(localPartBytes, in[0]) - in = in[1:] - - default: - break NextChar - } - } - - if len(localPartBytes) == 0 { - return mailbox, false - } - - // From RFC 3696, Section 3: - // “period (".") may also appear, but may not be used to start - // or end the local part, nor may two or more consecutive - // periods appear.” - twoDots := []byte{'.', '.'} - if localPartBytes[0] == '.' || - localPartBytes[len(localPartBytes)-1] == '.' || - bytes.Contains(localPartBytes, twoDots) { - return mailbox, false - } - } - - if in == "" || in[0] != '@' { - return mailbox, false - } - in = in[1:] - - // The RFC species a format for domains, but that's known to be - // violated in practice so we accept that anything after an '@' is the - // domain part. - if _, ok := domainToReverseLabels(in); !ok { - return mailbox, false - } - - mailbox.local = string(localPartBytes) - mailbox.domain = in - return mailbox, true -} - -// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -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. - if constraint == "" { - return true, nil - } - - // A single whitespace seems to be considered a valid domain, but we don't allow it. - if domain == " " { - return false, nil - } - - // Block domains that start with just a period - if domain[0] == '.' { - return false, nil - } - - // Block wildcard domains that don't start with exactly "*." (i.e. double wildcards and such) - if domain[0] == '*' && domain[1] != '.' { - return false, nil - } - - // Check if the domain starts with a wildcard and return early if not allowed - if strings.HasPrefix(domain, "*.") && !e.allowLiteralWildcardNames { - return false, nil - } - - // Only allow asterisk at the start of the domain; we don't allow them as part of a domain label or as a (sub)domain label (currently) - if strings.LastIndex(domain, "*") > 0 { - return false, nil - } - - // Don't allow constraints with empty labels in any position - if strings.Contains(constraint, "..") { - return false, nil - } - - domainLabels, ok := domainToReverseLabels(domain) - if !ok { - return false, fmt.Errorf("cannot parse domain %q", domain) - } - - // RFC 5280 says that a leading period in a domain name means that at - // least one label must be prepended, but only for URI and email - // constraints, not DNS constraints. The code also supports that - // behavior for DNS constraints. In our adaptation of the original - // Go stdlib x509 Name Constraint implementation we look for exactly - // one subdomain, currently. - - mustHaveSubdomains := false - if constraint[0] == '.' { - mustHaveSubdomains = true - constraint = constraint[1:] - } - - constraintLabels, ok := domainToReverseLabels(constraint) - if !ok { - 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": "." - // in the future we could extend this to support multiple additional labels and/or more - // complex matching. - expectedNumberOfLabels++ - } - - if len(domainLabels) != expectedNumberOfLabels { - return false, nil - } - - for i, constraintLabel := range constraintLabels { - if !strings.EqualFold(constraintLabel, domainLabels[i]) { - return false, nil - } - } - - return true, nil -} - -// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) { - - // TODO(hs): this is code from Go library, but I got some unexpected result: - // with permitted net 127.0.0.0/24, 127.0.0.1 is NOT allowed. When parsing 127.0.0.1 as net.IP - // which is in the IPAddresses slice, the underlying length is 16. The contraint.IP has a length - // of 4 instead. I currently don't believe that this is a bug in Go now, but why is it like that? - // Is there a difference because we're not operating on a sans []string slice? Or is the Go - // implementation stricter regarding IPv4 vs. IPv6? I've been bitten by some unfortunate differences - // between the two before (i.e. IPv4 in IPv6; IP SANS in ACME) - // if len(ip) != len(constraint.IP) { - // return false, nil - // } - - // for i := range ip { - // if mask := constraint.Mask[i]; ip[i]&mask != constraint.IP[i]&mask { - // return false, nil - // } - // } - - contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior; also check IPv4-in-IPv6 (again) - - return contained, nil -} - -// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { - // TODO(hs): handle literal wildcard case for emails? Does that even make sense? - // If the constraint contains an @, then it specifies an exact mailbox name (currently) - if strings.Contains(constraint, "*") { - return false, fmt.Errorf("email constraint %q cannot contain asterisk", constraint) - } - if strings.Contains(constraint, "@") { - constraintMailbox, ok := parseRFC2821Mailbox(constraint) - if !ok { - return false, fmt.Errorf("cannot parse constraint %q", constraint) - } - return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil - } - - // Otherwise the constraint is like a DNS constraint of the domain part - // of the mailbox. - return e.matchDomainConstraint(mailbox.domain, constraint) -} - -// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go -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 - // component with a host name specified as a fully qualified domain - // name (e.g., if the URI either does not include an authority - // component or includes an authority component in which the host name - // is specified as an IP address), then the application MUST reject the - // certificate.” - - host := uri.Host - if host == "" { - return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String()) - } - - // Block hosts with the wildcard character; no exceptions, also not when wildcards allowed. - if strings.Contains(host, "*") { - return false, fmt.Errorf("URI host %q cannot contain asterisk", uri.String()) - } - - if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") { - var err error - host, _, err = net.SplitHostPort(uri.Host) - if err != nil { - return false, err - } - } - - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") || - net.ParseIP(host) != nil { - return false, fmt.Errorf("URI with IP %q cannot be matched against constraints", uri.String()) - } - - // TODO(hs): add checks for scheme, path, etc.; either here, or in a different constraint matcher (to keep this one simple) - - return e.matchDomainConstraint(host, constraint) -} - -// matchUsernameConstraint performs a string literal match against a constraint. -func matchUsernameConstraint(username, constraint string) (bool, error) { - // allow any plain principal username - if constraint == "*" { - return true, nil - } - return strings.EqualFold(username, constraint), nil -} diff --git a/policy/engine_test.go b/policy/engine_test.go index cf406e71..603ef6ce 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -2223,16 +2223,16 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { t.Run(tt.name, func(t *testing.T) { engine, err := New(tt.options...) assert.FatalError(t, err) - got, err := engine.AreCertificateNamesAllowed(tt.cert) + got, err := engine.IsX509CertificateAllowed(tt.cert) if (err != nil) != tt.wantErr { - t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("NamePolicyEngine.IsX509CertificateAllowed() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { assert.NotEquals(t, "", err.Error()) // TODO(hs): implement a more specific error comparison? } if got != tt.want { - t.Errorf("NamePolicyEngine.AreCertificateNamesAllowed() = %v, want %v", got, tt.want) + t.Errorf("NamePolicyEngine.IsX509CertificateAllowed() = %v, want %v", got, tt.want) } // Perform the same tests for a CSR, which are similar to Certificates @@ -2243,7 +2243,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { IPAddresses: tt.cert.IPAddresses, URIs: tt.cert.URIs, } - got, err = engine.AreCSRNamesAllowed(csr) + got, err = engine.IsX509CertificateRequestAllowed(csr) if (err != nil) != tt.wantErr { t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) return @@ -2705,13 +2705,13 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { t.Run(tt.name, func(t *testing.T) { engine, err := New(tt.options...) assert.FatalError(t, err) - got, err := engine.ArePrincipalsAllowed(tt.cert) + got, err := engine.IsSSHCertificateAllowed(tt.cert) if (err != nil) != tt.wantErr { - t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("NamePolicyEngine.IsSSHCertificateAllowed() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("NamePolicyEngine.ArePrincipalsAllowed() = %v, want %v", got, tt.want) + t.Errorf("NamePolicyEngine.IsSSHCertificateAllowed() = %v, want %v", got, tt.want) } }) } diff --git a/policy/ssh.go b/policy/ssh.go index 0b4290d2..1ebecb2e 100644 --- a/policy/ssh.go +++ b/policy/ssh.go @@ -5,5 +5,5 @@ import ( ) type SSHNamePolicyEngine interface { - ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) + IsSSHCertificateAllowed(cert *ssh.Certificate) (bool, error) } diff --git a/policy/validate.go b/policy/validate.go new file mode 100644 index 00000000..f259515f --- /dev/null +++ b/policy/validate.go @@ -0,0 +1,580 @@ +// 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. +package policy + +import ( + "bytes" + "fmt" + "net" + "net/url" + "reflect" + "strings" + + "golang.org/x/net/idna" +) + +// 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 + if e.totalNumberOfConstraints == 0 { + 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 + // 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 + // executed so far. + + // TODO: gather all errors, or return early? Currently we return early on the first wrong name; check might fail for multiple names. + // Perhaps make that an option? + for _, dns := range dnsNames { + // if there are DNS names to check, no DNS constraints set, but there are other permitted constraints, + // 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). + if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return &NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), + } + } + didCutWildcard := false + if strings.HasPrefix(dns, "*.") { + dns = dns[1:] + didCutWildcard = true + } + parsedDNS, err := idna.Lookup.ToASCII(dns) + if err != nil { + return &NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns), + } + } + if didCutWildcard { + parsedDNS = "*" + parsedDNS + } + if _, ok := domainToReverseLabels(parsedDNS); !ok { + return &NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("cannot parse dns %q", dns), + } + } + if err := checkNameConstraints("dns", dns, parsedDNS, + func(parsedName, constraint interface{}) (bool, error) { + return e.matchDomainConstraint(parsedName.(string), constraint.(string)) + }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { + return err + } + } + + for _, ip := range ips { + if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return &NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()), + } + } + if err := checkNameConstraints("ip", ip.String(), ip, + func(parsedName, constraint interface{}) (bool, error) { + return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet)) + }, e.permittedIPRanges, e.excludedIPRanges); err != nil { + return err + } + } + + for _, email := range emailAddresses { + if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return &NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email), + } + } + mailbox, ok := parseRFC2821Mailbox(email) + if !ok { + return &NamePolicyError{ + Reason: CannotParseRFC822Name, + Detail: fmt.Sprintf("invalid rfc822Name %q", mailbox), + } + } + // According to RFC 5280, section 7.5, emails are considered to match if the local part is + // an exact match and the host (domain) part matches the ASCII representation (case-insensitive): + // https://datatracker.ietf.org/doc/html/rfc5280#section-7.5 + domainASCII, err := idna.ToASCII(mailbox.domain) + if err != nil { + return &NamePolicyError{ + Reason: CannotParseDomain, + Detail: fmt.Sprintf("cannot parse email domain %q", email), + } + } + mailbox.domain = domainASCII + if err := checkNameConstraints("email", email, mailbox, + func(parsedName, constraint interface{}) (bool, error) { + return e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) + }, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil { + return err + } + } + + // TODO(hs): fix internationalization for URIs (IRIs) + + for _, uri := range uris { + if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return &NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()), + } + } + if err := checkNameConstraints("uri", uri.String(), uri, + func(parsedName, constraint interface{}) (bool, error) { + return e.matchURIConstraint(parsedName.(*url.URL), constraint.(string)) + }, e.permittedURIDomains, e.excludedURIDomains); err != nil { + return err + } + } + + for _, principal := range principals { + if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { + return &NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal), + } + } + // TODO: some validation? I.e. allowed characters? + if err := checkNameConstraints("principal", principal, principal, + func(parsedName, constraint interface{}) (bool, error) { + return matchUsernameConstraint(parsedName.(string), constraint.(string)) + }, e.permittedPrincipals, e.excludedPrincipals); err != nil { + return err + } + } + + // if all checks out, all SANs are allowed + return nil +} + +// 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, + parsedName interface{}, + match func(parsedName, constraint interface{}) (match bool, err error), + permitted, excluded interface{}) error { + + excludedValue := reflect.ValueOf(excluded) + + for i := 0; i < excludedValue.Len(); i++ { + constraint := excludedValue.Index(i).Interface() + match, err := match(parsedName, constraint) + if err != nil { + return &NamePolicyError{ + Reason: CannotMatchNameToConstraint, + Detail: err.Error(), + } + } + + if match { + return &NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), + } + } + } + + permittedValue := reflect.ValueOf(permitted) + + ok := true + for i := 0; i < permittedValue.Len(); i++ { + constraint := permittedValue.Index(i).Interface() + var err error + if ok, err = match(parsedName, constraint); err != nil { + return &NamePolicyError{ + Reason: CannotMatchNameToConstraint, + Detail: err.Error(), + } + } + + if ok { + break + } + } + + if !ok { + return &NamePolicyError{ + Reason: NotAuthorizedForThisName, + Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), + } + } + + return nil +} + +// 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 { + reverseLabels = append(reverseLabels, domain) + domain = "" + } else { + reverseLabels = append(reverseLabels, domain[i+1:]) + domain = domain[:i] + } + } + + if len(reverseLabels) > 0 && reverseLabels[0] == "" { + // An empty label at the end indicates an absolute value. + return nil, false + } + + for _, label := range reverseLabels { + if label == "" { + // Empty labels are otherwise invalid. + return nil, false + } + + for _, c := range label { + if c < 33 || c > 126 { + // Invalid character. + return nil, false + } + } + } + + return reverseLabels, true +} + +// 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 +} + +// parseRFC2821Mailbox parses an email address into local and domain parts, +// 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 + } + + localPartBytes := make([]byte, 0, len(in)/2) + + if in[0] == '"' { + // Quoted-string = DQUOTE *qcontent DQUOTE + // non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127 + // qcontent = qtext / quoted-pair + // qtext = non-whitespace-control / + // %d33 / %d35-91 / %d93-126 + // quoted-pair = ("\" text) / obs-qp + // text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text + // + // (Names beginning with “obs-” are the obsolete syntax from RFC 2822, + // Section 4. Since it has been 16 years, we no longer accept that.) + in = in[1:] + QuotedString: + for { + if in == "" { + return mailbox, false + } + c := in[0] + in = in[1:] + + switch { + case c == '"': + break QuotedString + + case c == '\\': + // quoted-pair + if in == "" { + return mailbox, false + } + if in[0] == 11 || + in[0] == 12 || + (1 <= in[0] && in[0] <= 9) || + (14 <= in[0] && in[0] <= 127) { + localPartBytes = append(localPartBytes, in[0]) + in = in[1:] + } else { + return mailbox, false + } + + case c == 11 || + c == 12 || + // Space (char 32) is not allowed based on the + // BNF, but RFC 3696 gives an example that + // assumes that it is. Several “verified” + // errata continue to argue about this point. + // We choose to accept it. + c == 32 || + c == 33 || + c == 127 || + (1 <= c && c <= 8) || + (14 <= c && c <= 31) || + (35 <= c && c <= 91) || + (93 <= c && c <= 126): + // qtext + localPartBytes = append(localPartBytes, c) + + default: + return mailbox, false + } + } + } else { + // Atom ("." Atom)* + NextChar: + for len(in) > 0 { + // atext from RFC 2822, Section 3.2.4 + c := in[0] + + switch { + case c == '\\': + // Examples given in RFC 3696 suggest that + // escaped characters can appear outside of a + // quoted string. Several “verified” errata + // continue to argue the point. We choose to + // accept it. + in = in[1:] + if in == "" { + return mailbox, false + } + fallthrough + + case ('0' <= c && c <= '9') || + ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + c == '!' || c == '#' || c == '$' || c == '%' || + c == '&' || c == '\'' || c == '*' || c == '+' || + c == '-' || c == '/' || c == '=' || c == '?' || + c == '^' || c == '_' || c == '`' || c == '{' || + c == '|' || c == '}' || c == '~' || c == '.': + localPartBytes = append(localPartBytes, in[0]) + in = in[1:] + + default: + break NextChar + } + } + + if len(localPartBytes) == 0 { + return mailbox, false + } + + // From RFC 3696, Section 3: + // “period (".") may also appear, but may not be used to start + // or end the local part, nor may two or more consecutive + // periods appear.” + twoDots := []byte{'.', '.'} + if localPartBytes[0] == '.' || + localPartBytes[len(localPartBytes)-1] == '.' || + bytes.Contains(localPartBytes, twoDots) { + return mailbox, false + } + } + + if in == "" || in[0] != '@' { + return mailbox, false + } + in = in[1:] + + // The RFC species a format for domains, but that's known to be + // violated in practice so we accept that anything after an '@' is the + // domain part. + if _, ok := domainToReverseLabels(in); !ok { + return mailbox, false + } + + mailbox.local = string(localPartBytes) + mailbox.domain = in + return mailbox, true +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +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. + if constraint == "" { + return true, nil + } + + // A single whitespace seems to be considered a valid domain, but we don't allow it. + if domain == " " { + return false, nil + } + + // Block domains that start with just a period + if domain[0] == '.' { + return false, nil + } + + // Block wildcard domains that don't start with exactly "*." (i.e. double wildcards and such) + if domain[0] == '*' && domain[1] != '.' { + return false, nil + } + + // Check if the domain starts with a wildcard and return early if not allowed + if strings.HasPrefix(domain, "*.") && !e.allowLiteralWildcardNames { + return false, nil + } + + // Only allow asterisk at the start of the domain; we don't allow them as part of a domain label or as a (sub)domain label (currently) + if strings.LastIndex(domain, "*") > 0 { + return false, nil + } + + // Don't allow constraints with empty labels in any position + if strings.Contains(constraint, "..") { + return false, nil + } + + domainLabels, ok := domainToReverseLabels(domain) + if !ok { + return false, fmt.Errorf("cannot parse domain %q", domain) + } + + // RFC 5280 says that a leading period in a domain name means that at + // least one label must be prepended, but only for URI and email + // constraints, not DNS constraints. The code also supports that + // behavior for DNS constraints. In our adaptation of the original + // Go stdlib x509 Name Constraint implementation we look for exactly + // one subdomain, currently. + + mustHaveSubdomains := false + if constraint[0] == '.' { + mustHaveSubdomains = true + constraint = constraint[1:] + } + + constraintLabels, ok := domainToReverseLabels(constraint) + if !ok { + 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": "." + // in the future we could extend this to support multiple additional labels and/or more + // complex matching. + expectedNumberOfLabels++ + } + + if len(domainLabels) != expectedNumberOfLabels { + return false, nil + } + + for i, constraintLabel := range constraintLabels { + if !strings.EqualFold(constraintLabel, domainLabels[i]) { + return false, nil + } + } + + return true, nil +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) { + + // TODO(hs): this is code from Go library, but I got some unexpected result: + // with permitted net 127.0.0.0/24, 127.0.0.1 is NOT allowed. When parsing 127.0.0.1 as net.IP + // which is in the IPAddresses slice, the underlying length is 16. The contraint.IP has a length + // of 4 instead. I currently don't believe that this is a bug in Go now, but why is it like that? + // Is there a difference because we're not operating on a sans []string slice? Or is the Go + // implementation stricter regarding IPv4 vs. IPv6? I've been bitten by some unfortunate differences + // between the two before (i.e. IPv4 in IPv6; IP SANS in ACME) + // if len(ip) != len(constraint.IP) { + // return false, nil + // } + + // for i := range ip { + // if mask := constraint.Mask[i]; ip[i]&mask != constraint.IP[i]&mask { + // return false, nil + // } + // } + + contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior; also check IPv4-in-IPv6 (again) + + return contained, nil +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { + // TODO(hs): handle literal wildcard case for emails? Does that even make sense? + // If the constraint contains an @, then it specifies an exact mailbox name (currently) + if strings.Contains(constraint, "*") { + return false, fmt.Errorf("email constraint %q cannot contain asterisk", constraint) + } + if strings.Contains(constraint, "@") { + constraintMailbox, ok := parseRFC2821Mailbox(constraint) + if !ok { + return false, fmt.Errorf("cannot parse constraint %q", constraint) + } + return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil + } + + // Otherwise the constraint is like a DNS constraint of the domain part + // of the mailbox. + return e.matchDomainConstraint(mailbox.domain, constraint) +} + +// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go +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 + // component with a host name specified as a fully qualified domain + // name (e.g., if the URI either does not include an authority + // component or includes an authority component in which the host name + // is specified as an IP address), then the application MUST reject the + // certificate.” + + host := uri.Host + if host == "" { + return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String()) + } + + // Block hosts with the wildcard character; no exceptions, also not when wildcards allowed. + if strings.Contains(host, "*") { + return false, fmt.Errorf("URI host %q cannot contain asterisk", uri.String()) + } + + if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") { + var err error + host, _, err = net.SplitHostPort(uri.Host) + if err != nil { + return false, err + } + } + + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") || + net.ParseIP(host) != nil { + return false, fmt.Errorf("URI with IP %q cannot be matched against constraints", uri.String()) + } + + // TODO(hs): add checks for scheme, path, etc.; either here, or in a different constraint matcher (to keep this one simple) + + return e.matchDomainConstraint(host, constraint) +} + +// matchUsernameConstraint performs a string literal match against a constraint. +func matchUsernameConstraint(username, constraint string) (bool, error) { + // allow any plain principal username + if constraint == "*" { + return true, nil + } + return strings.EqualFold(username, constraint), nil +} diff --git a/policy/x509.go b/policy/x509.go index 0bc35d89..666e1b5c 100644 --- a/policy/x509.go +++ b/policy/x509.go @@ -6,8 +6,8 @@ import ( ) type X509NamePolicyEngine interface { - AreCertificateNamesAllowed(cert *x509.Certificate) (bool, error) - AreCSRNamesAllowed(csr *x509.CertificateRequest) (bool, error) + IsX509CertificateAllowed(cert *x509.Certificate) (bool, error) + IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) (bool, error) AreSANsAllowed(sans []string) (bool, error) IsDNSAllowed(dns string) (bool, error) IsIPAllowed(ip net.IP) (bool, error) From 235a2c9d04f039cc460e1851cce7cd9f7ee4c75e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 31 Mar 2022 16:40:49 +0200 Subject: [PATCH 26/78] Pin to specific version of go.step.sm/linkedca --- go.mod | 13 +++++++------ go.sum | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index f16bb91d..0b5e4a8b 100644 --- a/go.mod +++ b/go.mod @@ -38,17 +38,18 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.0 go.step.sm/crypto v0.16.1 - go.step.sm/linkedca v0.11.0 + go.step.sm/linkedca v0.12.1-0.20220331143637-69bee7065785 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd + golang.org/x/net v0.0.0-20220325170049-de3da57026de + golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect google.golang.org/api v0.70.0 - google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf - google.golang.org/grpc v1.44.0 - google.golang.org/protobuf v1.27.1 + google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7 + google.golang.org/grpc v1.45.0 + google.golang.org/protobuf v1.28.0 gopkg.in/square/go-jose.v2 v2.6.0 ) // replace github.com/smallstep/nosql => ../nosql // replace go.step.sm/crypto => ../crypto // replace go.step.sm/cli-utils => ../cli-utils -replace go.step.sm/linkedca => ../linkedca +// replace go.step.sm/linkedca => ../linkedca diff --git a/go.sum b/go.sum index ba718b16..d042d982 100644 --- a/go.sum +++ b/go.sum @@ -711,6 +711,12 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/ go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk= go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= +go.step.sm/linkedca v0.11.0 h1:jkG5XDQz9VSz2PH+cGjDvJTwiIziN0SWExTnicWpb8o= +go.step.sm/linkedca v0.11.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= +go.step.sm/linkedca v0.12.0 h1:FA18uJO5P6W2pklcezMs+w+N3dVbpKEE1LP9HLsJgg4= +go.step.sm/linkedca v0.12.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= +go.step.sm/linkedca v0.12.1-0.20220331143637-69bee7065785 h1:14HYoAd9P7DNpf8OkXq4OWTzEq5E6iX4hNkYu/NH4Wo= +go.step.sm/linkedca v0.12.1-0.20220331143637-69bee7065785/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -831,6 +837,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -944,6 +952,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= @@ -1144,6 +1154,8 @@ google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb2Gm8ivSlbNQzL2Z/NpPKE3RG2jWk= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7 h1:HOL66YCI20JvN2hVk6o2YIp9i/3RvzVUz82PqNr7fXw= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1177,6 +1189,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1192,6 +1206,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 5f0dc42b1e1280e3d1cf44847332ea07b8985cd2 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 31 Mar 2022 17:16:11 +0200 Subject: [PATCH 27/78] Fix tests on Go 1.18 due to IDNA deviations In Go 1.18 the behavior for looking up domains with non-ASCII characters was changed to be in accordance with UTS#46 (https://unicode.org/reports/tr46/). There's a slight difference in how IDNA2003 and IDNA2008 process these. Go 1.18 handles the deviations in accordance with IDNA2008 now. --- policy/options_117_test.go | 119 +++++++++++++++++++++++++++++++++++++ policy/options_118_test.go | 119 +++++++++++++++++++++++++++++++++++++ policy/options_test.go | 111 ---------------------------------- 3 files changed, 238 insertions(+), 111 deletions(-) create mode 100644 policy/options_117_test.go create mode 100644 policy/options_118_test.go diff --git a/policy/options_117_test.go b/policy/options_117_test.go new file mode 100644 index 00000000..bd3d287d --- /dev/null +++ b/policy/options_117_test.go @@ -0,0 +1,119 @@ +//go:build !go1.18 +// +build !go1.18 + +package policy + +import "testing" + +func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/empty-constraint", + constraint: "", + want: "", + wantErr: true, + }, + { + name: "fail/scheme-https", + constraint: `https://*.local`, + want: "", + wantErr: true, + }, + { + name: "fail/too-many-asterisks", + constraint: "**.local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-label", + constraint: "..local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-reverse", + constraint: ".", + want: "", + wantErr: true, + }, + { + name: "fail/domain-with-port", + constraint: "host.local:8443", + want: "", + wantErr: true, + }, + { + name: "fail/ipv4", + constraint: "127.0.0.1", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-brackets", + constraint: "[::1]", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-no-brackets", + constraint: "::1", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-no-brackets", + constraint: "[::1", + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain-name-lookup", + constraint: `\00local`, + want: "", + wantErr: true, + }, + { + name: "ok/wildcard", + constraint: "*.local", + want: ".local", + wantErr: false, + }, + { + name: "ok/specific-domain", + constraint: "example.local", + want: "example.local", + wantErr: false, + }, + { + name: "ok/idna-internationalized-domain-name-lookup", + constraint: `*.bücher.example.com`, + want: ".xn--bcher-kva.example.com", + wantErr: false, + }, + { + // IDNA2003 vs. 2008 deviation: https://unicode.org/reports/tr46/#Deviations results + // in a difference between Go 1.18 and lower versions. Go 1.18 expects ".xn--fa-hia.de"; not .fass.de. + name: "ok/idna-internationalized-domain-name-lookup-deviation", + constraint: `*.faß.de`, + want: ".fass.de", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeAndValidateURIDomainConstraint(tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/policy/options_118_test.go b/policy/options_118_test.go new file mode 100644 index 00000000..059f1177 --- /dev/null +++ b/policy/options_118_test.go @@ -0,0 +1,119 @@ +//go:build go1.18 +// +build go1.18 + +package policy + +import "testing" + +func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/empty-constraint", + constraint: "", + want: "", + wantErr: true, + }, + { + name: "fail/scheme-https", + constraint: `https://*.local`, + want: "", + wantErr: true, + }, + { + name: "fail/too-many-asterisks", + constraint: "**.local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-label", + constraint: "..local", + want: "", + wantErr: true, + }, + { + name: "fail/empty-reverse", + constraint: ".", + want: "", + wantErr: true, + }, + { + name: "fail/domain-with-port", + constraint: "host.local:8443", + want: "", + wantErr: true, + }, + { + name: "fail/ipv4", + constraint: "127.0.0.1", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-brackets", + constraint: "[::1]", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-no-brackets", + constraint: "::1", + want: "", + wantErr: true, + }, + { + name: "fail/ipv6-no-brackets", + constraint: "[::1", + want: "", + wantErr: true, + }, + { + name: "fail/idna-internationalized-domain-name-lookup", + constraint: `\00local`, + want: "", + wantErr: true, + }, + { + name: "ok/wildcard", + constraint: "*.local", + want: ".local", + wantErr: false, + }, + { + name: "ok/specific-domain", + constraint: "example.local", + want: "example.local", + wantErr: false, + }, + { + name: "ok/idna-internationalized-domain-name-lookup", + constraint: `*.bücher.example.com`, + want: ".xn--bcher-kva.example.com", + wantErr: false, + }, + { + // IDNA2003 vs. 2008 deviation: https://unicode.org/reports/tr46/#Deviations results + // in a difference between Go 1.18 and lower versions. Go 1.18 expects ".xn--fa-hia.de"; not .fass.de. + name: "ok/idna-internationalized-domain-name-lookup-deviation", + constraint: `*.faß.de`, + want: ".xn--fa-hia.de", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeAndValidateURIDomainConstraint(tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/policy/options_test.go b/policy/options_test.go index 8a64f282..b7390545 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -184,117 +184,6 @@ func Test_normalizeAndValidateEmailConstraint(t *testing.T) { } } -func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { - tests := []struct { - name string - constraint string - want string - wantErr bool - }{ - { - name: "fail/empty-constraint", - constraint: "", - want: "", - wantErr: true, - }, - { - name: "fail/scheme-https", - constraint: `https://*.local`, - want: "", - wantErr: true, - }, - { - name: "fail/too-many-asterisks", - constraint: "**.local", - want: "", - wantErr: true, - }, - { - name: "fail/empty-label", - constraint: "..local", - want: "", - wantErr: true, - }, - { - name: "fail/empty-reverse", - constraint: ".", - want: "", - wantErr: true, - }, - { - name: "fail/domain-with-port", - constraint: "host.local:8443", - want: "", - wantErr: true, - }, - { - name: "fail/ipv4", - constraint: "127.0.0.1", - want: "", - wantErr: true, - }, - { - name: "fail/ipv6-brackets", - constraint: "[::1]", - want: "", - wantErr: true, - }, - { - name: "fail/ipv6-no-brackets", - constraint: "::1", - want: "", - wantErr: true, - }, - { - name: "fail/ipv6-no-brackets", - constraint: "[::1", - want: "", - wantErr: true, - }, - { - name: "fail/idna-internationalized-domain-name-lookup", - constraint: `\00local`, - want: "", - wantErr: true, - }, - { - name: "ok/wildcard", - constraint: "*.local", - want: ".local", - wantErr: false, - }, - { - name: "ok/specific-domain", - constraint: "example.local", - want: "example.local", - wantErr: false, - }, - { - name: "ok/idna-internationalized-domain-name-lookup", - constraint: `*.bücher.example.com`, - want: ".xn--bcher-kva.example.com", - wantErr: false, - }, - { - name: "ok/idna-internationalized-domain-name-lookup-deviation", - constraint: `*.faß.de`, - want: ".fass.de", // IDNA2003 vs. 2008 deviation: https://unicode.org/reports/tr46/#Deviations - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := normalizeAndValidateURIDomainConstraint(tt.constraint) - if (err != nil) != tt.wantErr { - t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr) - } - if got != tt.want { - t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want) - } - }) - } -} - func TestNew(t *testing.T) { type test struct { options []NamePolicyOption From d8776d8f7f86f3e9de55f6e9bfe4d5a2532e253a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 1 Apr 2022 15:37:48 +0200 Subject: [PATCH 28/78] Add K8sSA SSH user policy back According to the docs, the K8sSA provisioner can be configured to issue SSH user certs. --- authority/provisioner/k8sSA.go | 8 +++++++- policy/options_test.go | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index ec813b6c..b127ed13 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -56,6 +56,7 @@ type K8sSA struct { ctl *Controller x509Policy policy.X509Policy sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // GetID returns the provisioner unique identifier. The name and credential id @@ -148,6 +149,11 @@ func (p *K8sSA) Init(config Config) (err error) { return err } + // Initialize the SSH allow/deny policy engine for user certificates + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + return err + } + // Initialize the SSH allow/deny policy engine for host certificates if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err @@ -298,7 +304,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, nil), + newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), ), nil } diff --git a/policy/options_test.go b/policy/options_test.go index b7390545..74982fd8 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -135,7 +135,7 @@ func Test_normalizeAndValidateEmailConstraint(t *testing.T) { }, { name: "fail/idna-internationalized-domain", - constraint: `mail@xn--bla.local`, + constraint: "mail@xn--bla.local", want: "", wantErr: true, }, From 96f4c49b0ccc3f268b3d43c73b9adae9495f52be Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 4 Apr 2022 13:58:16 +0200 Subject: [PATCH 29/78] Improve how policy errors are returned and used --- authority/admin/api/middleware_test.go | 93 ++++++++ authority/admin/api/policy.go | 51 +++-- authority/authority.go | 3 +- authority/policy.go | 139 +++++++++--- authority/policy_test.go | 295 +++++++++++++++++++++++++ authority/provisioners.go | 14 ++ policy/engine_test.go | 2 +- policy/validate.go | 17 +- 8 files changed, 559 insertions(+), 55 deletions(-) create mode 100644 authority/policy_test.go diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index c7314e71..3dfc5823 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -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 + } + }) + } +} diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index da0e1d9c..44344271 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -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 } diff --git a/authority/authority.go b/authority/authority.go index 5caec0fb..c451aef5 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -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 { diff --git a/authority/policy.go b/authority/policy.go index bb57a7d0..a88258fe 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -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 diff --git a/authority/policy_test.go b/authority/policy_test.go new file mode 100644 index 00000000..39edf700 --- /dev/null +++ b/authority/policy_test.go @@ -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)) + } + }) + } +} diff --git a/authority/provisioners.go b/authority/provisioners.go index b47eff1d..1bea7c1b 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -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, } } } diff --git a/policy/engine_test.go b/policy/engine_test.go index 603ef6ce..38aa709a 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -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 { diff --git a/policy/validate.go b/policy/validate.go index f259515f..b85eb299 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -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 From 679e2945f20897504ed65d091a014e16543b8bce Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 4 Apr 2022 15:31:28 +0200 Subject: [PATCH 30/78] Disallow name constraint wildcard notation --- authority/admin/api/policy.go | 12 ++- authority/policy.go | 5 +- authority/policy_test.go | 16 ++-- policy/engine_test.go | 4 +- policy/options.go | 143 ++++++++++++++++++---------------- policy/options_117_test.go | 6 ++ policy/options_118_test.go | 6 ++ policy/options_test.go | 10 ++- policy/validate.go | 2 +- 9 files changed, 116 insertions(+), 88 deletions(-) diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 44344271..b47c957c 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -86,8 +86,9 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r var createdPolicy *linkedca.Policy if createdPolicy, err = par.auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil { var pe *authority.PolicyError + isPolicyError := errors.As(err, &pe) - if errors.As(err, &pe); pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure { + if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error storing authority policy")) return } @@ -126,7 +127,8 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r var updatedPolicy *linkedca.Policy if updatedPolicy, err = par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil { var pe *authority.PolicyError - if errors.As(err, &pe); pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure { + isPolicyError := errors.As(err, &pe) + if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error updating authority policy")) return } @@ -201,7 +203,8 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, 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 { + isPolicyError := errors.As(err, &pe) + if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error creating provisioner policy")) return } @@ -233,7 +236,8 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, 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 { + isPolicyError := errors.As(err, &pe) + if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error updating provisioner policy")) return } diff --git a/authority/policy.go b/authority/policy.go index a88258fe..cc785173 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -170,7 +170,7 @@ func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admi if err != nil { return &PolicyError{ Typ: ConfigurationFailure, - err: fmt.Errorf("error creating temporary policy engine: %w", err), + err: err, } } @@ -213,7 +213,8 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error { ) if allowed, err = engine.AreSANsAllowed(sans); err != nil { var policyErr *policy.NamePolicyError - if errors.As(err, &policyErr); policyErr.Reason == policy.NotAuthorizedForThisName { + isNamePolicyError := errors.As(err, &policyErr) + if isNamePolicyError && 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), diff --git a/authority/policy_test.go b/authority/policy_test.go index 39edf700..87c96a87 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -6,10 +6,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" "go.step.sm/linkedca" - "github.com/smallstep/assert" authPolicy "github.com/smallstep/certificates/authority/policy" ) @@ -34,7 +34,7 @@ func TestAuthority_checkPolicy(t *testing.T) { }, err: &PolicyError{ Typ: ConfigurationFailure, - err: errors.New("error creating temporary policy engine: cannot parse permitted domain constraint \"**.local\""), + err: errors.New("cannot parse permitted domain constraint \"**.local\": domain constraint \"**.local\" can only have wildcard as starting character"), }, } }, @@ -46,7 +46,7 @@ func TestAuthority_checkPolicy(t *testing.T) { policy: &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ - Dns: []string{".local"}, + Dns: []string{"*.local"}, }, }, }, @@ -68,7 +68,7 @@ func TestAuthority_checkPolicy(t *testing.T) { policy: &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ - Dns: []string{".local"}, + Dns: []string{"*.local"}, }, }, }, @@ -177,13 +177,13 @@ func TestAuthority_checkPolicy(t *testing.T) { if tc.err == nil { assert.Nil(t, err) } else { - assert.Type(t, &PolicyError{}, err) + assert.IsType(t, &PolicyError{}, err) pe, ok := err.(*PolicyError) - assert.Fatal(t, ok) + assert.True(t, ok) - assert.Equals(t, tc.err.Typ, pe.Typ) - assert.Equals(t, tc.err.Error(), pe.Error()) + assert.Equal(t, tc.err.Typ, pe.Typ) + assert.Equal(t, tc.err.Error(), pe.Error()) } }) } diff --git a/policy/engine_test.go b/policy/engine_test.go index 38aa709a..dd0b403f 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -1500,7 +1500,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/dns-permitted-wildcard", options: []NamePolicyOption{ AddPermittedDNSDomain("*.local"), - AddPermittedDNSDomain(".x509local"), + AddPermittedDNSDomain("*.x509local"), WithAllowLiteralWildcardNames(), }, cert: &x509.Certificate{ @@ -1665,7 +1665,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-with-port", options: []NamePolicyOption{ - AddPermittedURIDomain(".example.com"), + AddPermittedURIDomain("*.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ diff --git a/policy/options.go b/policy/options.go index fe8f470e..308d46b5 100755 --- a/policy/options.go +++ b/policy/options.go @@ -5,7 +5,6 @@ import ( "net" "strings" - "github.com/pkg/errors" "golang.org/x/net/idna" ) @@ -33,7 +32,7 @@ func WithPermittedDNSDomains(domains []string) NamePolicyOption { for i, domain := range domains { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) if err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) + return fmt.Errorf("cannot parse permitted domain constraint %q: %w", domain, err) } normalizedDomains[i] = normalizedDomain } @@ -48,7 +47,7 @@ func AddPermittedDNSDomains(domains []string) NamePolicyOption { for i, domain := range domains { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) if err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) + return fmt.Errorf("cannot parse permitted domain constraint %q: %w", domain, err) } normalizedDomains[i] = normalizedDomain } @@ -63,7 +62,7 @@ func WithExcludedDNSDomains(domains []string) NamePolicyOption { for i, domain := range domains { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) if err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) + return fmt.Errorf("cannot parse excluded domain constraint %q: %w", domain, err) } normalizedDomains[i] = normalizedDomain } @@ -78,7 +77,7 @@ func AddExcludedDNSDomains(domains []string) NamePolicyOption { for i, domain := range domains { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) if err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) + return fmt.Errorf("cannot parse excluded domain constraint %q: %w", domain, err) } normalizedDomains[i] = normalizedDomain } @@ -91,7 +90,7 @@ func WithPermittedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) if err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) + return fmt.Errorf("cannot parse permitted domain constraint %q: %w", domain, err) } e.permittedDNSDomains = []string{normalizedDomain} return nil @@ -102,7 +101,7 @@ func AddPermittedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) if err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) + return fmt.Errorf("cannot parse permitted domain constraint %q: %w", domain, err) } e.permittedDNSDomains = append(e.permittedDNSDomains, normalizedDomain) return nil @@ -113,7 +112,7 @@ func WithExcludedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) if err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) + return fmt.Errorf("cannot parse excluded domain constraint %q: %w", domain, err) } e.excludedDNSDomains = []string{normalizedDomain} return nil @@ -124,7 +123,7 @@ func AddExcludedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) if err != nil { - return errors.Errorf("cannot parse permitted domain constraint %q", domain) + return fmt.Errorf("cannot parse excluded domain constraint %q: %w", domain, err) } e.excludedDNSDomains = append(e.excludedDNSDomains, normalizedDomain) return nil @@ -151,7 +150,7 @@ func WithPermittedCIDRs(cidrs []string) NamePolicyOption { for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { - return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) + return fmt.Errorf("cannot parse permitted CIDR constraint %q", cidr) } networks[i] = nw } @@ -166,7 +165,7 @@ func AddPermittedCIDRs(cidrs []string) NamePolicyOption { for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { - return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) + return fmt.Errorf("cannot parse permitted CIDR constraint %q", cidr) } networks[i] = nw } @@ -181,7 +180,7 @@ func WithExcludedCIDRs(cidrs []string) NamePolicyOption { for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { - return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) + return fmt.Errorf("cannot parse excluded CIDR constraint %q", cidr) } networks[i] = nw } @@ -196,7 +195,7 @@ func AddExcludedCIDRs(cidrs []string) NamePolicyOption { for i, cidr := range cidrs { _, nw, err := net.ParseCIDR(cidr) if err != nil { - return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) + return fmt.Errorf("cannot parse excluded CIDR constraint %q", cidr) } networks[i] = nw } @@ -215,7 +214,7 @@ func WithPermittedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { } else if ip := net.ParseIP(ipOrCIDR); ip != nil { networks[i] = networkFor(ip) } else { - return errors.Errorf("cannot parse permitted constraint %q as IP nor CIDR", ipOrCIDR) + return fmt.Errorf("cannot parse permitted constraint %q as IP nor CIDR", ipOrCIDR) } } e.permittedIPRanges = networks @@ -233,7 +232,7 @@ func WithExcludedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { } else if ip := net.ParseIP(ipOrCIDR); ip != nil { networks[i] = networkFor(ip) } else { - return errors.Errorf("cannot parse excluded constraint %q as IP nor CIDR", ipOrCIDR) + return fmt.Errorf("cannot parse excluded constraint %q as IP nor CIDR", ipOrCIDR) } } e.excludedIPRanges = networks @@ -245,7 +244,7 @@ func WithPermittedCIDR(cidr string) NamePolicyOption { return func(e *NamePolicyEngine) error { _, nw, err := net.ParseCIDR(cidr) if err != nil { - return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) + return fmt.Errorf("cannot parse permitted CIDR constraint %q", cidr) } e.permittedIPRanges = []*net.IPNet{nw} return nil @@ -256,7 +255,7 @@ func AddPermittedCIDR(cidr string) NamePolicyOption { return func(e *NamePolicyEngine) error { _, nw, err := net.ParseCIDR(cidr) if err != nil { - return errors.Errorf("cannot parse permitted CIDR constraint %q", cidr) + return fmt.Errorf("cannot parse permitted CIDR constraint %q", cidr) } e.permittedIPRanges = append(e.permittedIPRanges, nw) return nil @@ -297,7 +296,7 @@ func WithExcludedCIDR(cidr string) NamePolicyOption { return func(e *NamePolicyEngine) error { _, nw, err := net.ParseCIDR(cidr) if err != nil { - return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) + return fmt.Errorf("cannot parse excluded CIDR constraint %q", cidr) } e.excludedIPRanges = []*net.IPNet{nw} return nil @@ -308,7 +307,7 @@ func AddExcludedCIDR(cidr string) NamePolicyOption { return func(e *NamePolicyEngine) error { _, nw, err := net.ParseCIDR(cidr) if err != nil { - return errors.Errorf("cannot parse excluded CIDR constraint %q", cidr) + return fmt.Errorf("cannot parse excluded CIDR constraint %q", cidr) } e.excludedIPRanges = append(e.excludedIPRanges, nw) return nil @@ -355,7 +354,7 @@ func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { for i, email := range emailAddresses { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) if err != nil { - return err + return fmt.Errorf("cannot parse permitted email constraint %q: %w", email, err) } normalizedEmailAddresses[i] = normalizedEmailAddress } @@ -370,7 +369,7 @@ func AddPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { for i, email := range emailAddresses { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) if err != nil { - return err + return fmt.Errorf("cannot parse permitted email constraint %q: %w", email, err) } normalizedEmailAddresses[i] = normalizedEmailAddress } @@ -385,7 +384,7 @@ func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { for i, email := range emailAddresses { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) if err != nil { - return err + return fmt.Errorf("cannot parse excluded email constraint %q: %w", email, err) } normalizedEmailAddresses[i] = normalizedEmailAddress } @@ -400,7 +399,7 @@ func AddExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { for i, email := range emailAddresses { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) if err != nil { - return err + return fmt.Errorf("cannot parse excluded email constraint %q: %w", email, err) } normalizedEmailAddresses[i] = normalizedEmailAddress } @@ -413,7 +412,7 @@ func WithPermittedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) if err != nil { - return err + return fmt.Errorf("cannot parse permitted email constraint %q: %w", emailAddress, err) } e.permittedEmailAddresses = []string{normalizedEmailAddress} return nil @@ -424,7 +423,7 @@ func AddPermittedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) if err != nil { - return err + return fmt.Errorf("cannot parse permitted email constraint %q: %w", emailAddress, err) } e.permittedEmailAddresses = append(e.permittedEmailAddresses, normalizedEmailAddress) return nil @@ -435,7 +434,7 @@ func WithExcludedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) if err != nil { - return err + return fmt.Errorf("cannot parse excluded email constraint %q: %w", emailAddress, err) } e.excludedEmailAddresses = []string{normalizedEmailAddress} return nil @@ -446,7 +445,7 @@ func AddExcludedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) if err != nil { - return err + return fmt.Errorf("cannot parse excluded email constraint %q: %w", emailAddress, err) } e.excludedEmailAddresses = append(e.excludedEmailAddresses, normalizedEmailAddress) return nil @@ -459,7 +458,7 @@ func WithPermittedURIDomains(uriDomains []string) NamePolicyOption { for i, domain := range uriDomains { normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) if err != nil { - return err + return fmt.Errorf("cannot parse permitted URI domain constraint %q: %w", domain, err) } normalizedURIDomains[i] = normalizedURIDomain } @@ -474,7 +473,7 @@ func AddPermittedURIDomains(uriDomains []string) NamePolicyOption { for i, domain := range uriDomains { normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) if err != nil { - return err + return fmt.Errorf("cannot parse permitted URI domain constraint %q: %w", domain, err) } normalizedURIDomains[i] = normalizedURIDomain } @@ -483,35 +482,35 @@ func AddPermittedURIDomains(uriDomains []string) NamePolicyOption { } } -func WithPermittedURIDomain(uriDomain string) NamePolicyOption { +func WithPermittedURIDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) if err != nil { - return err + return fmt.Errorf("cannot parse permitted URI domain constraint %q: %w", domain, err) } e.permittedURIDomains = []string{normalizedURIDomain} return nil } } -func AddPermittedURIDomain(uriDomain string) NamePolicyOption { +func AddPermittedURIDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) if err != nil { - return err + return fmt.Errorf("cannot parse permitted URI domain constraint %q: %w", domain, err) } e.permittedURIDomains = append(e.permittedURIDomains, normalizedURIDomain) return nil } } -func WithExcludedURIDomains(uriDomains []string) NamePolicyOption { +func WithExcludedURIDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - normalizedURIDomains := make([]string, len(uriDomains)) - for i, domain := range uriDomains { + normalizedURIDomains := make([]string, len(domains)) + for i, domain := range domains { normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) if err != nil { - return err + return fmt.Errorf("cannot parse excluded URI domain constraint %q: %w", domain, err) } normalizedURIDomains[i] = normalizedURIDomain } @@ -520,13 +519,13 @@ func WithExcludedURIDomains(uriDomains []string) NamePolicyOption { } } -func AddExcludedURIDomains(uriDomains []string) NamePolicyOption { +func AddExcludedURIDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { - normalizedURIDomains := make([]string, len(uriDomains)) - for i, domain := range uriDomains { + normalizedURIDomains := make([]string, len(domains)) + for i, domain := range domains { normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) if err != nil { - return err + return fmt.Errorf("cannot parse excluded URI domain constraint %q: %w", domain, err) } normalizedURIDomains[i] = normalizedURIDomain } @@ -535,22 +534,22 @@ func AddExcludedURIDomains(uriDomains []string) NamePolicyOption { } } -func WithExcludedURIDomain(uriDomain string) NamePolicyOption { +func WithExcludedURIDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) if err != nil { - return err + return fmt.Errorf("cannot parse excluded URI domain constraint %q: %w", domain, err) } e.excludedURIDomains = []string{normalizedURIDomain} return nil } } -func AddExcludedURIDomain(uriDomain string) NamePolicyOption { +func AddExcludedURIDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(uriDomain) + normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) if err != nil { - return err + return fmt.Errorf("cannot parse excluded URI domain constraint %q: %w", domain, err) } e.excludedURIDomains = append(e.excludedURIDomains, normalizedURIDomain) return nil @@ -594,26 +593,29 @@ func isIPv4(ip net.IP) bool { func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) if normalizedConstraint == "" { - return "", errors.Errorf("contraint %q can not be empty or white space string", constraint) + return "", fmt.Errorf("contraint %q can not be empty or white space string", constraint) } if strings.Contains(normalizedConstraint, "..") { - return "", errors.Errorf("domain constraint %q cannot have empty labels", constraint) + return "", fmt.Errorf("domain constraint %q cannot have empty labels", constraint) } - if normalizedConstraint[0] == '*' && normalizedConstraint[1] != '.' { - return "", errors.Errorf("wildcard character in domain constraint %q can only be used to match (full) labels", constraint) + if strings.HasPrefix(normalizedConstraint, ".") { + return "", fmt.Errorf("domain constraint %q with wildcard should start with *", constraint) } if strings.LastIndex(normalizedConstraint, "*") > 0 { - return "", errors.Errorf("domain constraint %q can only have wildcard as starting character", constraint) + return "", fmt.Errorf("domain constraint %q can only have wildcard as starting character", constraint) + } + if normalizedConstraint[0] == '*' && normalizedConstraint[1] != '.' { + return "", fmt.Errorf("wildcard character in domain constraint %q can only be used to match (full) labels", constraint) } if strings.HasPrefix(normalizedConstraint, "*.") { normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period } normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint) if err != nil { - return "", errors.Wrapf(err, "domain constraint %q can not be converted to ASCII", constraint) + return "", fmt.Errorf("domain constraint %q can not be converted to ASCII: %w", constraint, err) } if _, ok := domainToReverseLabels(normalizedConstraint); !ok { - return "", errors.Errorf("cannot parse domain constraint %q", constraint) + return "", fmt.Errorf("cannot parse domain constraint %q", constraint) } return normalizedConstraint, nil } @@ -621,7 +623,7 @@ func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) func normalizeAndValidateEmailConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) if normalizedConstraint == "" { - return "", errors.Errorf("email contraint %q can not be empty or white space string", constraint) + return "", fmt.Errorf("email contraint %q can not be empty or white space string", constraint) } if strings.Contains(normalizedConstraint, "*") { return "", fmt.Errorf("email constraint %q cannot contain asterisk wildcard", constraint) @@ -645,14 +647,14 @@ func normalizeAndValidateEmailConstraint(constraint string) (string, error) { // https://datatracker.ietf.org/doc/html/rfc5280#section-7.5 domainASCII, err := idna.Lookup.ToASCII(mailbox.domain) if err != nil { - return "", errors.Wrapf(err, "email constraint %q domain part %q cannot be converted to ASCII", constraint, mailbox.domain) + return "", fmt.Errorf("email constraint %q domain part %q cannot be converted to ASCII: %w", constraint, mailbox.domain, err) } normalizedConstraint = mailbox.local + "@" + domainASCII } else { var err error normalizedConstraint, err = idna.Lookup.ToASCII(normalizedConstraint) if err != nil { - return "", errors.Wrapf(err, "email constraint %q cannot be converted to ASCII", constraint) + return "", fmt.Errorf("email constraint %q cannot be converted to ASCII: %w", constraint, err) } } if _, ok := domainToReverseLabels(normalizedConstraint); !ok { @@ -664,35 +666,38 @@ func normalizeAndValidateEmailConstraint(constraint string) (string, error) { func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) if normalizedConstraint == "" { - return "", errors.Errorf("URI domain contraint %q cannot be empty or white space string", constraint) + return "", fmt.Errorf("URI domain contraint %q cannot be empty or white space string", constraint) } if strings.Contains(normalizedConstraint, "://") { - return "", errors.Errorf("URI domain constraint %q contains scheme (not supported yet)", constraint) + return "", fmt.Errorf("URI domain constraint %q contains scheme (not supported yet)", constraint) } if strings.Contains(normalizedConstraint, "..") { - return "", errors.Errorf("URI domain constraint %q cannot have empty labels", constraint) + return "", fmt.Errorf("URI domain constraint %q cannot have empty labels", constraint) + } + if strings.HasPrefix(normalizedConstraint, ".") { + return "", fmt.Errorf("URI domain constraint %q with wildcard should start with *", constraint) + } + if strings.LastIndex(normalizedConstraint, "*") > 0 { + return "", fmt.Errorf("URI domain constraint %q can only have wildcard as starting character", constraint) } if strings.HasPrefix(normalizedConstraint, "*.") { normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period } - if strings.Contains(normalizedConstraint, "*") { - return "", errors.Errorf("URI domain constraint %q can only have wildcard as starting character", constraint) - } // we're being strict with square brackets in domains; we don't allow them, no matter what if strings.Contains(normalizedConstraint, "[") || strings.Contains(normalizedConstraint, "]") { - return "", errors.Errorf("URI domain constraint %q contains invalid square brackets", constraint) + return "", fmt.Errorf("URI domain constraint %q contains invalid square brackets", constraint) } if _, _, err := net.SplitHostPort(normalizedConstraint); err == nil { // a successful split (likely) with host and port; we don't currently allow ports in the config - return "", errors.Errorf("URI domain constraint %q cannot contain port", constraint) + return "", fmt.Errorf("URI domain constraint %q cannot contain port", constraint) } // check if the host part of the URI domain constraint is an IP if net.ParseIP(normalizedConstraint) != nil { - return "", errors.Errorf("URI domain constraint %q cannot be an IP", constraint) + return "", fmt.Errorf("URI domain constraint %q cannot be an IP", constraint) } normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint) if err != nil { - return "", errors.Wrapf(err, "URI domain constraint %q cannot be converted to ASCII", constraint) + return "", fmt.Errorf("URI domain constraint %q cannot be converted to ASCII: %w", constraint, err) } _, ok := domainToReverseLabels(normalizedConstraint) if !ok { diff --git a/policy/options_117_test.go b/policy/options_117_test.go index bd3d287d..916eefe2 100644 --- a/policy/options_117_test.go +++ b/policy/options_117_test.go @@ -42,6 +42,12 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { want: "", wantErr: true, }, + { + name: "fail/no-asterisk", + constraint: ".example.com", + want: "", + wantErr: true, + }, { name: "fail/domain-with-port", constraint: "host.local:8443", diff --git a/policy/options_118_test.go b/policy/options_118_test.go index 059f1177..6fa2ded4 100644 --- a/policy/options_118_test.go +++ b/policy/options_118_test.go @@ -48,6 +48,12 @@ func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) { want: "", wantErr: true, }, + { + name: "fail/no-asterisk", + constraint: ".example.com", + want: "", + wantErr: true, + }, { name: "fail/ipv4", constraint: "127.0.0.1", diff --git a/policy/options_test.go b/policy/options_test.go index 74982fd8..a1c48e1f 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -46,6 +46,12 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { want: "", wantErr: true, }, + { + name: "fail/no-asterisk", + constraint: ".example.com", + want: "", + wantErr: true, + }, { name: "fail/idna-internationalized-domain-name-lookup", constraint: `\00.local`, // invalid IDNA ASCII character @@ -66,13 +72,13 @@ func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { }, { name: "ok/idna-internationalized-domain-name-punycode", - constraint: ".xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + constraint: "*.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ want: ".xn--fsq.jp", wantErr: false, }, { name: "ok/idna-internationalized-domain-name-lookup-transformed", - constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + constraint: "*.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ want: ".xn--fsq.jp", wantErr: false, }, diff --git a/policy/validate.go b/policy/validate.go index b85eb299..fd611b74 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -398,7 +398,7 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) { return mailbox, true } -// matchDomainConstraint matches a domain agains the given constraint +// matchDomainConstraint matches a domain against 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. From 7df52dbb767b49312a2ab012de97f6d42e0de461 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 7 Apr 2022 14:11:53 +0200 Subject: [PATCH 31/78] Add ACME EAB policy --- acme/account.go | 18 ++ acme/api/account.go | 7 +- acme/api/eab.go | 4 + acme/api/order.go | 4 + acme/api/order_test.go | 35 +++ acme/db.go | 12 ++ acme/db/nosql/eab.go | 4 + authority/admin/api/acme_test.go | 1 - authority/admin/api/handler.go | 15 +- authority/admin/api/middleware.go | 129 ++++++++++- authority/admin/api/middleware_test.go | 47 ++-- authority/admin/api/policy.go | 100 +++++++-- ca/adminClient.go | 284 +++++++++++++++++++++++++ ca/ca.go | 2 +- go.mod | 8 +- go.sum | 8 + 16 files changed, 622 insertions(+), 56 deletions(-) diff --git a/acme/account.go b/acme/account.go index 027d7be1..5291cb28 100644 --- a/acme/account.go +++ b/acme/account.go @@ -43,6 +43,23 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { return base64.RawURLEncoding.EncodeToString(kid), nil } +// PolicyNames contains ACME account level policy names +type PolicyNames struct { + DNSNames []string `json:"dns"` + IPRanges []string `json:"ips"` +} + +// X509Policy contains ACME account level X.509 policy +type X509Policy struct { + Allowed PolicyNames `json:"allowed"` + Denied PolicyNames `json:"denied"` +} + +// Policy is an ACME Account level policy +type Policy struct { + X509 X509Policy `json:"x509"` +} + // ExternalAccountKey is an ACME External Account Binding key. type ExternalAccountKey struct { ID string `json:"id"` @@ -52,6 +69,7 @@ type ExternalAccountKey struct { KeyBytes []byte `json:"-"` CreatedAt time.Time `json:"createdAt"` BoundAt time.Time `json:"boundAt,omitempty"` + Policy *Policy `json:"policy,omitempty"` } // AlreadyBound returns whether this EAK is already bound to diff --git a/acme/api/account.go b/acme/api/account.go index ade51aef..e4c46ca5 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "net/http" "github.com/go-chi/chi" @@ -130,12 +131,14 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } + fmt.Println("BEFORE EAK BINDING") + if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response - err := eak.BindTo(acc) - if err != nil { + if err := eak.BindTo(acc); err != nil { render.Error(w, err) return } + fmt.Println("AFTER EAK BINDING") if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key")) return diff --git a/acme/api/eab.go b/acme/api/eab.go index 1780a173..0df9d193 100644 --- a/acme/api/eab.go +++ b/acme/api/eab.go @@ -60,6 +60,10 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt) } + if len(externalAccountKey.KeyBytes) == 0 { + return nil, acme.NewError(acme.ErrorServerInternalType, "no key bytes") // TODO(hs): improve error message + } + payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) if err != nil { return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") diff --git a/acme/api/order.go b/acme/api/order.go index 7f78ca6e..a13d1148 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/json" + "fmt" "net" "net/http" "strings" @@ -110,6 +111,9 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { // TODO(hs): gather all errors, so that we can build one response with subproblems; include the nor.Validate() // error here too, like in example? + eak, err := h.db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID) + fmt.Println("EAK: ", eak, err) + for _, identifier := range nor.Identifiers { // evaluate the provisioner level policy orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value} diff --git a/acme/api/order_test.go b/acme/api/order_test.go index ccaef176..13c849a0 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -782,6 +782,11 @@ func TestHandler_NewOrder(t *testing.T) { assert.Equals(t, ch.Value, "zap.internal") return errors.New("force") }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, }, err: acme.NewErrorISE("error creating challenge: force"), } @@ -852,6 +857,11 @@ func TestHandler_NewOrder(t *testing.T) { assert.Equals(t, o.AuthorizationIDs, []string{*az1ID}) return errors.New("force") }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, }, err: acme.NewErrorISE("error creating order: force"), } @@ -949,6 +959,11 @@ func TestHandler_NewOrder(t *testing.T) { assert.Equals(t, o.AuthorizationIDs, []string{*az1ID, *az2ID}) return nil }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, }, vr: func(t *testing.T, o *acme.Order) { now := clock.Now() @@ -1042,6 +1057,11 @@ func TestHandler_NewOrder(t *testing.T) { assert.Equals(t, o.AuthorizationIDs, []string{*az1ID}) return nil }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, }, vr: func(t *testing.T, o *acme.Order) { now := clock.Now() @@ -1135,6 +1155,11 @@ func TestHandler_NewOrder(t *testing.T) { assert.Equals(t, o.AuthorizationIDs, []string{*az1ID}) return nil }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, }, vr: func(t *testing.T, o *acme.Order) { now := clock.Now() @@ -1227,6 +1252,11 @@ func TestHandler_NewOrder(t *testing.T) { assert.Equals(t, o.AuthorizationIDs, []string{*az1ID}) return nil }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, }, vr: func(t *testing.T, o *acme.Order) { testBufferDur := 5 * time.Second @@ -1320,6 +1350,11 @@ func TestHandler_NewOrder(t *testing.T) { assert.Equals(t, o.AuthorizationIDs, []string{*az1ID}) return nil }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, }, vr: func(t *testing.T, o *acme.Order) { testBufferDur := 5 * time.Second diff --git a/acme/db.go b/acme/db.go index 412276fd..b53cb397 100644 --- a/acme/db.go +++ b/acme/db.go @@ -23,6 +23,7 @@ type DB interface { GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) + GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error @@ -60,6 +61,7 @@ type MockDB struct { MockGetExternalAccountKey func(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) MockGetExternalAccountKeys func(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) + MockGetExternalAccountKeyByAccountID func(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error) MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error @@ -168,6 +170,16 @@ func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provision return m.MockRet1.(*ExternalAccountKey), m.MockError } +// GetExternalAccountKeyByAccountID mock +func (m *MockDB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error) { + if m.MockGetExternalAccountKeyByAccountID != nil { + return m.MockGetExternalAccountKeyByAccountID(ctx, provisionerID, accountID) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(*ExternalAccountKey), m.MockError +} + // DeleteExternalAccountKey mock func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { if m.MockDeleteExternalAccountKey != nil { diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go index f9a24daf..5c34c20c 100644 --- a/acme/db/nosql/eab.go +++ b/acme/db/nosql/eab.go @@ -226,6 +226,10 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) } +func (db *DB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + return nil, nil +} + func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { externalAccountKeyMutex.Lock() defer externalAccountKeyMutex.Unlock() diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 2c7bbd37..937ddfa3 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -11,7 +11,6 @@ import ( "testing" "github.com/go-chi/chi" - "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index eb0b791a..eb52ad58 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -56,7 +56,7 @@ func (h *Handler) Route(r api.Router) { } acmePolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc { - return authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(next)))) + return authnz(disabledInStandalone(h.loadProvisionerByName(h.requireEABEnabled(h.loadExternalAccountKey(next))))) } // Provisioners @@ -92,8 +92,13 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(h.policyResponder.DeleteProvisionerPolicy)) // Policy - ACME Account - r.MethodFunc("GET", "/acme/policy/{provisionerName}/{accountID}", acmePolicyMiddleware(h.policyResponder.GetACMEAccountPolicy)) - r.MethodFunc("POST", "/acme/policy/{provisionerName}/{accountID}", acmePolicyMiddleware(h.policyResponder.CreateACMEAccountPolicy)) - r.MethodFunc("PUT", "/acme/policy/{provisionerName}/{accountID}", acmePolicyMiddleware(h.policyResponder.UpdateACMEAccountPolicy)) - r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/{accountID}", acmePolicyMiddleware(h.policyResponder.DeleteACMEAccountPolicy)) + r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(h.policyResponder.GetACMEAccountPolicy)) + r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(h.policyResponder.GetACMEAccountPolicy)) + r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(h.policyResponder.CreateACMEAccountPolicy)) + r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(h.policyResponder.CreateACMEAccountPolicy)) + r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(h.policyResponder.UpdateACMEAccountPolicy)) + r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(h.policyResponder.UpdateACMEAccountPolicy)) + r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(h.policyResponder.DeleteACMEAccountPolicy)) + r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(h.policyResponder.DeleteACMEAccountPolicy)) + } diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index c30eee10..98c56f08 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -4,9 +4,11 @@ import ( "net/http" "github.com/go-chi/chi" + "google.golang.org/protobuf/types/known/timestamppb" "go.step.sm/linkedca" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin/db/nosql" @@ -81,12 +83,12 @@ func (h *Handler) loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc func (h *Handler) checkAction(next http.HandlerFunc, supportedInStandalone bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // temporarily only support the admin nosql DB - if _, ok := h.adminDB.(*nosql.DB); !ok { - render.Error(w, admin.NewError(admin.ErrorNotImplementedType, - "operation not supported")) - return - } + // // temporarily only support the admin nosql DB + // if _, ok := h.adminDB.(*nosql.DB); !ok { + // render.Error(w, admin.NewError(admin.ErrorNotImplementedType, + // "operation not supported")) + // return + // } // actions allowed in standalone mode are always supported if supportedInStandalone { @@ -106,3 +108,118 @@ func (h *Handler) checkAction(next http.HandlerFunc, supportedInStandalone bool) next(w, r) } } + +// loadExternalAccountKey is a middleware that searches for an ACME +// External Account Key by accountID, keyID or reference and stores it in the context. +func (h *Handler) loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + prov := linkedca.ProvisionerFromContext(ctx) + + reference := chi.URLParam(r, "reference") + keyID := chi.URLParam(r, "keyID") + + var ( + eak *acme.ExternalAccountKey + err error + ) + + if keyID != "" { + eak, err = h.acmeDB.GetExternalAccountKey(ctx, prov.GetId(), keyID) + } else { + eak, err = h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference) + } + + if err != nil { + // TODO: handle error; not found vs. some internal server error + render.Error(w, admin.WrapErrorISE(err, "error retrieving ACME External Account key")) + return + } + + if eak == nil { + render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key does not exist")) + return + } + + linkedEAK := eakToLinked(eak) + + ctx = linkedca.NewContextWithExternalAccountKey(ctx, linkedEAK) + + next(w, r.WithContext(ctx)) + } +} + +func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey { + + if k == nil { + return nil + } + + eak := &linkedca.EABKey{ + Id: k.ID, + HmacKey: k.KeyBytes, + Provisioner: k.ProvisionerID, + Reference: k.Reference, + Account: k.AccountID, + CreatedAt: timestamppb.New(k.CreatedAt), + BoundAt: timestamppb.New(k.BoundAt), + } + + if k.Policy != nil { + eak.Policy = &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{}, + Deny: &linkedca.X509Names{}, + }, + } + eak.Policy.X509.Allow.Dns = k.Policy.X509.Allowed.DNSNames + eak.Policy.X509.Allow.Ips = k.Policy.X509.Allowed.IPRanges + eak.Policy.X509.Deny.Dns = k.Policy.X509.Denied.DNSNames + eak.Policy.X509.Deny.Ips = k.Policy.X509.Denied.IPRanges + } + + return eak +} + +func linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey { + if k == nil { + return nil + } + + eak := &acme.ExternalAccountKey{ + ID: k.Id, + ProvisionerID: k.Provisioner, + Reference: k.Reference, + AccountID: k.Account, + KeyBytes: k.HmacKey, + CreatedAt: k.CreatedAt.AsTime(), + BoundAt: k.BoundAt.AsTime(), + } + + if k.Policy == nil { + return eak + } + + eak.Policy = &acme.Policy{} + + if k.Policy.X509 == nil { + return eak + } + + eak.Policy.X509 = acme.X509Policy{ + Allowed: acme.PolicyNames{}, + Denied: acme.PolicyNames{}, + } + + if k.Policy.X509.Allow != nil { + eak.Policy.X509.Allowed.DNSNames = k.Policy.X509.Allow.Dns + eak.Policy.X509.Allowed.IPRanges = k.Policy.X509.Allow.Ips + } + + if k.Policy.X509.Deny != nil { + eak.Policy.X509.Denied.DNSNames = k.Policy.X509.Deny.Dns + eak.Policy.X509.Denied.IPRanges = k.Policy.X509.Deny.Ips + } + + return eak +} diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index 3dfc5823..cc0f7a8d 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -368,15 +368,15 @@ func TestHandler_checkAction(t *testing.T) { 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-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, @@ -400,22 +400,21 @@ func TestHandler_checkAction(t *testing.T) { 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, - } - }, + // "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) { diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index b47c957c..959eccd5 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -6,6 +6,7 @@ import ( "go.step.sm/linkedca" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api/read" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority" @@ -31,13 +32,15 @@ type policyAdminResponderInterface interface { type PolicyAdminResponder struct { auth adminAuthority adminDB admin.DB + acmeDB acme.DB } // NewACMEAdminResponder returns a new ACMEAdminResponder -func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB) *PolicyAdminResponder { +func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB) *PolicyAdminResponder { return &PolicyAdminResponder{ auth: auth, adminDB: adminDB, + acmeDB: acmeDB, } } @@ -156,8 +159,7 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r return } - err = par.auth.RemoveAuthorityPolicy(ctx) - if err != nil { + if err := par.auth.RemoveAuthorityPolicy(ctx); err != nil { render.Error(w, admin.WrapErrorISE(err, "error deleting authority policy")) return } @@ -200,8 +202,7 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, prov.Policy = newPolicy - err := par.auth.UpdateProvisioner(ctx, prov) - if err != nil { + if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { var pe *authority.PolicyError isPolicyError := errors.As(err, &pe) if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { @@ -233,8 +234,7 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, } prov.Policy = newPolicy - err := par.auth.UpdateProvisioner(ctx, prov) - if err != nil { + if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { var pe *authority.PolicyError isPolicyError := errors.As(err, &pe) if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { @@ -263,8 +263,7 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, // remove the policy prov.Policy = nil - err := par.auth.UpdateProvisioner(ctx, prov) - if err != nil { + if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner policy")) return } @@ -273,17 +272,92 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, } func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - render.JSONStatus(w, "not implemented yet", http.StatusNotImplemented) + ctx := r.Context() + eak := linkedca.ExternalAccountKeyFromContext(ctx) + + policy := eak.GetPolicy() + if policy == nil { + render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) + return + } + + render.ProtoJSONStatus(w, policy, http.StatusOK) } func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - render.JSONStatus(w, "not implemented yet", http.StatusNotImplemented) + ctx := r.Context() + prov := linkedca.ProvisionerFromContext(ctx) + eak := linkedca.ExternalAccountKeyFromContext(ctx) + + policy := eak.GetPolicy() + if policy != nil { + adminErr := admin.NewError(admin.ErrorBadRequestType, "ACME EAK %s already has a policy", eak.Id) + adminErr.Status = http.StatusConflict + render.Error(w, adminErr) + return + } + + var newPolicy = new(linkedca.Policy) + if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { + return + } + + eak.Policy = newPolicy + + acmeEAK := linkedEAKToCertificates(eak) + if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { + render.Error(w, admin.WrapErrorISE(err, "error creating ACME EAK policy")) + return + } + + render.ProtoJSONStatus(w, newPolicy, http.StatusCreated) } func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - render.JSONStatus(w, "not implemented yet", http.StatusNotImplemented) + ctx := r.Context() + prov := linkedca.ProvisionerFromContext(ctx) + eak := linkedca.ExternalAccountKeyFromContext(ctx) + + policy := eak.GetPolicy() + if policy == nil { + render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) + return + } + + var newPolicy = new(linkedca.Policy) + if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { + return + } + + eak.Policy = newPolicy + acmeEAK := linkedEAKToCertificates(eak) + if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { + render.Error(w, admin.WrapErrorISE(err, "error updating ACME EAK policy")) + return + } + + render.ProtoJSONStatus(w, newPolicy, http.StatusOK) } func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - render.JSONStatus(w, "not implemented yet", http.StatusNotImplemented) + ctx := r.Context() + prov := linkedca.ProvisionerFromContext(ctx) + eak := linkedca.ExternalAccountKeyFromContext(ctx) + + policy := eak.GetPolicy() + if policy == nil { + render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) + return + } + + // remove the policy + eak.Policy = nil + + acmeEAK := linkedEAKToCertificates(eak) + if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { + render.Error(w, admin.WrapErrorISE(err, "error deleting ACME EAK policy")) + return + } + + render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) } diff --git a/ca/adminClient.go b/ca/adminClient.go index f972f9f8..30154662 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -808,6 +808,290 @@ retry: return nil } +func (c *AdminClient) GetProvisionerPolicy(provisionerName string) (*linkedca.Policy, error) { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("creating GET %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client GET %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) CreateProvisionerPolicy(provisionerName string, p *linkedca.Policy) (*linkedca.Policy, error) { + var retried bool + body, err := protojson.Marshal(p) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating POST %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client POST %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) UpdateProvisionerPolicy(provisionerName string, p *linkedca.Policy) (*linkedca.Policy, error) { + var retried bool + body, err := protojson.Marshal(p) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client PUT %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) RemoveProvisionerPolicy(provisionerName string) error { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody) + if err != nil { + return fmt.Errorf("creating DELETE %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("client DELETE %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return readAdminError(resp.Body) + } + return nil +} + +func (c *AdminClient) GetACMEPolicy(provisionerName, reference, keyID string) (*linkedca.Policy, error) { + var retried bool + var urlPath string + switch { + case keyID != "": + urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "key", keyID) + default: + urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference) + } + u := c.endpoint.ResolveReference(&url.URL{Path: urlPath}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("creating GET %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client GET %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) CreateACMEPolicy(provisionerName, reference, keyID string, p *linkedca.Policy) (*linkedca.Policy, error) { + var retried bool + body, err := protojson.Marshal(p) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + var urlPath string + switch { + case keyID != "": + urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "key", keyID) + default: + urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference) + } + u := c.endpoint.ResolveReference(&url.URL{Path: urlPath}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating POST %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client POST %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) UpdateACMEPolicy(provisionerName, reference, keyID string, p *linkedca.Policy) (*linkedca.Policy, error) { + var retried bool + body, err := protojson.Marshal(p) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + var urlPath string + switch { + case keyID != "": + urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "key", keyID) + default: + urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference) + } + u := c.endpoint.ResolveReference(&url.URL{Path: urlPath}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client PUT %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) RemoveACMEPolicy(provisionerName, reference, keyID string) error { + var retried bool + var urlPath string + switch { + case keyID != "": + urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "key", keyID) + default: + urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference) + } + u := c.endpoint.ResolveReference(&url.URL{Path: urlPath}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody) + if err != nil { + return fmt.Errorf("creating DELETE %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("client DELETE %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return readAdminError(resp.Body) + } + return nil +} + func readAdminError(r io.ReadCloser) error { // TODO: not all errors can be read (i.e. 404); seems to be a bigger issue defer r.Close() diff --git a/ca/ca.go b/ca/ca.go index 2c4b1aa0..eefbd280 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -208,7 +208,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { adminDB := auth.GetAdminDatabase() if adminDB != nil { acmeAdminResponder := adminAPI.NewACMEAdminResponder() - policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB) + policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB, acmeDB) adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder, policyAdminResponder) mux.Route("/admin", func(r chi.Router) { adminHandler.Route(r) diff --git a/go.mod b/go.mod index 0b5e4a8b..1d325398 100644 --- a/go.mod +++ b/go.mod @@ -38,12 +38,12 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.0 go.step.sm/crypto v0.16.1 - go.step.sm/linkedca v0.12.1-0.20220331143637-69bee7065785 + go.step.sm/linkedca v0.12.1-0.20220405095509-878e3e5f78a3 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 - golang.org/x/net v0.0.0-20220325170049-de3da57026de - golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect + golang.org/x/net v0.0.0-20220403103023-749bd193bc2b + golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect google.golang.org/api v0.70.0 - google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7 + google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de google.golang.org/grpc v1.45.0 google.golang.org/protobuf v1.28.0 gopkg.in/square/go-jose.v2 v2.6.0 diff --git a/go.sum b/go.sum index d042d982..c5a28d05 100644 --- a/go.sum +++ b/go.sum @@ -717,6 +717,8 @@ go.step.sm/linkedca v0.12.0 h1:FA18uJO5P6W2pklcezMs+w+N3dVbpKEE1LP9HLsJgg4= go.step.sm/linkedca v0.12.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= go.step.sm/linkedca v0.12.1-0.20220331143637-69bee7065785 h1:14HYoAd9P7DNpf8OkXq4OWTzEq5E6iX4hNkYu/NH4Wo= go.step.sm/linkedca v0.12.1-0.20220331143637-69bee7065785/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= +go.step.sm/linkedca v0.12.1-0.20220405095509-878e3e5f78a3 h1:CIq0rMhfcV3oDRT0h4de2GVpRQnBnLJTTVIdc0eFjUg= +go.step.sm/linkedca v0.12.1-0.20220405095509-878e3e5f78a3/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -839,6 +841,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacp golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -954,6 +958,8 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIj golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 h1:D1v9ucDTYBtbz5vNuBbAhIMAGhQhJ6Ym5ah3maMVNX4= +golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= @@ -1156,6 +1162,8 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7 h1:HOL66YCI20JvN2hVk6o2YIp9i/3RvzVUz82PqNr7fXw= google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de h1:9Ti5SG2U4cAcluryUo/sFay3TQKoxiFMfaT0pbizU7k= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= From 0bb15e16f96d7167c3d2c8ff28fc75385caa86d4 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 8 Apr 2022 16:10:26 +0200 Subject: [PATCH 32/78] Fix missing ACME provisioner option --- authority/provisioner/acme.go | 1 + authority/provisioner/acme_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 2bcaeef2..219176fd 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -139,6 +139,7 @@ func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIden // on the resulting certificate. func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { opts := []SignOption{ + p, // modifiers / withOptions newProvisionerExtensionOption(TypeACME, p.Name, ""), newForceCNOption(p.ForceCN), diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index 73342d79..33cbbc75 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -176,7 +176,7 @@ func TestACME_AuthorizeSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) && assert.NotNil(t, opts) { - assert.Len(t, 6, opts) // number of SignOptions returned + assert.Equals(t, 7, len(opts)) // number of SignOptions returned for _, o := range opts { switch v := o.(type) { case *ACME: From 256fe113f7f6612dede752c2db07d1f955046117 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 11 Apr 2022 15:25:55 +0200 Subject: [PATCH 33/78] Improve tests for ACME account policy --- acme/account.go | 21 +++ acme/api/account.go | 4 - acme/api/account_test.go | 17 +- acme/api/eab.go | 10 +- acme/api/eab_test.go | 109 +++++++++++ acme/api/order.go | 50 +++-- acme/api/order_test.go | 291 +++++++++++++++++++++++++++++- acme/api/revoke_test.go | 18 +- authority/admin/api/acme.go | 77 ++++++++ authority/admin/api/middleware.go | 76 -------- 10 files changed, 571 insertions(+), 102 deletions(-) diff --git a/acme/account.go b/acme/account.go index 5291cb28..21c37314 100644 --- a/acme/account.go +++ b/acme/account.go @@ -7,6 +7,8 @@ import ( "time" "go.step.sm/crypto/jose" + + "github.com/smallstep/certificates/authority/policy" ) // Account is a subset of the internal account type containing only those @@ -60,6 +62,25 @@ type Policy struct { X509 X509Policy `json:"x509"` } +func (p *Policy) GetAllowedNameOptions() *policy.X509NameOptions { + if p == nil { + return nil + } + return &policy.X509NameOptions{ + DNSDomains: p.X509.Allowed.DNSNames, + IPRanges: p.X509.Allowed.IPRanges, + } +} +func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions { + if p == nil { + return nil + } + return &policy.X509NameOptions{ + DNSDomains: p.X509.Denied.DNSNames, + IPRanges: p.X509.Denied.IPRanges, + } +} + // ExternalAccountKey is an ACME External Account Binding key. type ExternalAccountKey struct { ID string `json:"id"` diff --git a/acme/api/account.go b/acme/api/account.go index e4c46ca5..f6e18f90 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -2,7 +2,6 @@ package api import ( "encoding/json" - "fmt" "net/http" "github.com/go-chi/chi" @@ -131,14 +130,11 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } - fmt.Println("BEFORE EAK BINDING") - if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response if err := eak.BindTo(acc); err != nil { render.Error(w, err) return } - fmt.Println("AFTER EAK BINDING") if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key")) return diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 4c3404ec..a457655c 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -13,10 +13,12 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" + + "go.step.sm/crypto/jose" + "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/crypto/jose" ) var ( @@ -41,6 +43,19 @@ func newProv() acme.Provisioner { return p } +func newProvWithOptions(options *provisioner.Options) acme.Provisioner { + // Initialize provisioners + p := &provisioner.ACME{ + Type: "ACME", + Name: "test@acme-provisioner.com", + Options: options, + } + if err := p.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil { + fmt.Printf("%v", err) + } + return p +} + func newACMEProv(t *testing.T) *provisioner.ACME { p := newProv() a, ok := p.(*provisioner.ACME) diff --git a/acme/api/eab.go b/acme/api/eab.go index 0df9d193..6be906d4 100644 --- a/acme/api/eab.go +++ b/acme/api/eab.go @@ -56,12 +56,16 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.WrapErrorISE(err, "error retrieving external account key") } - if externalAccountKey.AlreadyBound() { - return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt) + if externalAccountKey == nil { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key") } if len(externalAccountKey.KeyBytes) == 0 { - return nil, acme.NewError(acme.ErrorServerInternalType, "no key bytes") // TODO(hs): improve error message + return nil, acme.NewError(acme.ErrorServerInternalType, "external account binding key with id '%s' does not have secret bytes", keyID) + } + + if externalAccountKey.AlreadyBound() { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt) } payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) diff --git a/acme/api/eab_test.go b/acme/api/eab_test.go index f9bce970..760c122c 100644 --- a/acme/api/eab_test.go +++ b/acme/api/eab_test.go @@ -428,6 +428,114 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { err: acme.NewErrorISE("error retrieving external account key"), } }, + "fail/db.GetExternalAccountKey-nil": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return nil, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key"), + } + }, + "fail/db.GetExternalAccountKey-no-keybytes": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + createdAt := time.Now() + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + CreatedAt: createdAt, + AccountID: "some-account-id", + KeyBytes: []byte{}, + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorServerInternalType, "external account binding key with id 'eakID' does not have secret bytes"), + } + }, "fail/db.GetExternalAccountKey-wrong-provisioner": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) @@ -522,6 +630,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { Reference: "testeak", CreatedAt: createdAt, AccountID: "some-account-id", + KeyBytes: []byte{1, 3, 3, 7}, BoundAt: boundAt, }, nil }, diff --git a/acme/api/order.go b/acme/api/order.go index a13d1148..b4f7cf27 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -17,6 +17,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" ) @@ -102,29 +103,35 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { return } - // TODO(hs): the policy evaluation below should also verify rules set in the Account (i.e. allowed/denied - // DNS and IPs). It's probably good to connect those to the EAB credentials and management? Or - // should we do it fully properly and connect them to the Account directly? The latter would allow - // 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 - // thus make sense to also set the allowed/denied names based on that info. // TODO(hs): gather all errors, so that we can build one response with subproblems; include the nor.Validate() // error here too, like in example? eak, err := h.db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID) - fmt.Println("EAK: ", eak, err) + if err != nil { + render.Error(w, acme.WrapErrorISE(err, "error retrieving external account binding key")) + return + } + + acmePolicy, err := newACMEPolicyEngine(eak) + if err != nil { + render.Error(w, acme.WrapErrorISE(err, "error creating ACME policy engine")) + return + } for _, identifier := range nor.Identifiers { + // evalue the ACME account level policy + if err = isIdentifierAllowed(acmePolicy, identifier); err != nil { + render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) + return + } // evaluate the provisioner level policy orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value} - err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier) - if err != nil { + if err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier); err != nil { render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) return } // evaluate the authority level policy - err = h.ca.AreSANsAllowed(ctx, []string{identifier.Value}) - if err != nil { + if err = h.ca.AreSANsAllowed(ctx, []string{identifier.Value}); err != nil { render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) return } @@ -180,6 +187,27 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { render.JSONStatus(w, o, http.StatusCreated) } +func isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifier) error { + if acmePolicy == nil { + return nil + } + allowed, err := acmePolicy.AreSANsAllowed([]string{identifier.Value}) + if err != nil { + return err + } + if !allowed { + return fmt.Errorf("acme identifier '%s' not allowed", identifier.Value) + } + return nil +} + +func newACMEPolicyEngine(eak *acme.ExternalAccountKey) (policy.X509Policy, error) { + if eak == nil { + return nil, nil + } + return policy.NewX509PolicyEngine(eak.Policy) +} + func (h *Handler) newAuthorization(ctx context.Context, az *acme.Authorization) error { if strings.HasPrefix(az.Identifier.Value, "*.") { az.Wildcard = true diff --git a/acme/api/order_test.go b/acme/api/order_test.go index 13c849a0..02034c16 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -16,9 +16,13 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" + + "go.step.sm/crypto/pemutil" + "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" - "go.step.sm/crypto/pemutil" + "github.com/smallstep/certificates/authority/policy" + "github.com/smallstep/certificates/authority/provisioner" ) func TestNewOrderRequest_Validate(t *testing.T) { @@ -757,6 +761,188 @@ func TestHandler_NewOrder(t *testing.T) { err: acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty"), } }, + "fail/db.GetExternalAccountKeyByAccountID-error": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + fr := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "dns", Value: "zap.internal"}, + }, + } + b, err := json.Marshal(fr) + assert.FatalError(t, err) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + return test{ + ctx: ctx, + statusCode: 500, + ca: &mockCA{}, + db: &acme.MockDB{ + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, errors.New("force") + }, + }, + err: acme.NewErrorISE("error retrieving external account binding key: force"), + } + }, + "fail/newACMEPolicyEngine-error": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + fr := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "dns", Value: "zap.internal"}, + }, + } + b, err := json.Marshal(fr) + assert.FatalError(t, err) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + return test{ + ctx: ctx, + statusCode: 500, + ca: &mockCA{}, + db: &acme.MockDB{ + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return &acme.ExternalAccountKey{ + Policy: &acme.Policy{ + X509: acme.X509Policy{ + Allowed: acme.PolicyNames{ + DNSNames: []string{"**.local"}, + }, + }, + }, + }, nil + }, + }, + err: acme.NewErrorISE("error creating ACME policy engine"), + } + }, + "fail/isIdentifierAllowed-error": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + fr := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "dns", Value: "zap.internal"}, + }, + } + b, err := json.Marshal(fr) + assert.FatalError(t, err) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + return test{ + ctx: ctx, + statusCode: 400, + ca: &mockCA{}, + db: &acme.MockDB{ + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return &acme.ExternalAccountKey{ + Policy: &acme.Policy{ + X509: acme.X509Policy{ + Allowed: acme.PolicyNames{ + DNSNames: []string{"*.local"}, + }, + }, + }, + }, nil + }, + }, + err: acme.NewError(acme.ErrorRejectedIdentifierType, "not authorized"), + } + }, + "fail/prov.AuthorizeOrderIdentifier-error": func(t *testing.T) test { + options := &provisioner.Options{ + X509: &provisioner.X509Options{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + }, + } + provWithPolicy := newProvWithOptions(options) + acc := &acme.Account{ID: "accID"} + fr := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "dns", Value: "zap.internal"}, + }, + } + b, err := json.Marshal(fr) + assert.FatalError(t, err) + ctx := context.WithValue(context.Background(), provisionerContextKey, provWithPolicy) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + return test{ + ctx: ctx, + statusCode: 400, + ca: &mockCA{}, + db: &acme.MockDB{ + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return &acme.ExternalAccountKey{ + Policy: &acme.Policy{ + X509: acme.X509Policy{ + Allowed: acme.PolicyNames{ + DNSNames: []string{"*.internal"}, + }, + }, + }, + }, nil + }, + }, + err: acme.NewError(acme.ErrorRejectedIdentifierType, "not authorized"), + } + }, + "fail/ca.AreSANsAllowed-error": func(t *testing.T) test { + options := &provisioner.Options{ + X509: &provisioner.X509Options{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.internal"}, + }, + }, + } + provWithPolicy := newProvWithOptions(options) + acc := &acme.Account{ID: "accID"} + fr := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "dns", Value: "zap.internal"}, + }, + } + b, err := json.Marshal(fr) + assert.FatalError(t, err) + ctx := context.WithValue(context.Background(), provisionerContextKey, provWithPolicy) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + return test{ + ctx: ctx, + statusCode: 400, + ca: &mockCA{ + MockAreSANsallowed: func(ctx context.Context, sans []string) error { + return errors.New("force: not authorized by authority") + }, + }, + db: &acme.MockDB{ + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return &acme.ExternalAccountKey{ + Policy: &acme.Policy{ + X509: acme.X509Policy{ + Allowed: acme.PolicyNames{ + DNSNames: []string{"*.internal"}, + }, + }, + }, + }, nil + }, + }, + err: acme.NewError(acme.ErrorRejectedIdentifierType, "not authorized"), + } + }, "fail/error-h.newAuthorization": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} fr := &NewOrderRequest{ @@ -1360,6 +1546,109 @@ func TestHandler_NewOrder(t *testing.T) { testBufferDur := 5 * time.Second orderExpiry := now.Add(defaultOrderExpiry) + assert.Equals(t, o.ID, "ordID") + assert.Equals(t, o.Status, acme.StatusPending) + assert.Equals(t, o.Identifiers, nor.Identifiers) + assert.Equals(t, o.AuthorizationURLs, []string{fmt.Sprintf("%s/acme/%s/authz/az1ID", baseURL.String(), escProvName)}) + assert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf)) + assert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf)) + assert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf)) + assert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf)) + assert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry)) + assert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry)) + }, + } + }, + "ok/default-naf-nbf-with-policy": func(t *testing.T) test { + options := &provisioner.Options{ + X509: &provisioner.X509Options{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.internal"}, + }, + }, + } + provWithPolicy := newProvWithOptions(options) + acc := &acme.Account{ID: "accID"} + nor := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "dns", Value: "zap.internal"}, + }, + } + b, err := json.Marshal(nor) + assert.FatalError(t, err) + ctx := context.WithValue(context.Background(), provisionerContextKey, provWithPolicy) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + var ( + ch1, ch2, ch3 **acme.Challenge + az1ID *string + count = 0 + ) + return test{ + ctx: ctx, + statusCode: 201, + nor: nor, + ca: &mockCA{}, + db: &acme.MockDB{ + MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { + switch count { + case 0: + ch.ID = "dns" + assert.Equals(t, ch.Type, acme.DNS01) + ch1 = &ch + case 1: + ch.ID = "http" + assert.Equals(t, ch.Type, acme.HTTP01) + ch2 = &ch + case 2: + ch.ID = "tls" + assert.Equals(t, ch.Type, acme.TLSALPN01) + ch3 = &ch + default: + assert.FatalError(t, errors.New("test logic error")) + return errors.New("force") + } + count++ + assert.Equals(t, ch.AccountID, "accID") + assert.NotEquals(t, ch.Token, "") + assert.Equals(t, ch.Status, acme.StatusPending) + assert.Equals(t, ch.Value, "zap.internal") + return nil + }, + MockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error { + az.ID = "az1ID" + az1ID = &az.ID + assert.Equals(t, az.AccountID, "accID") + assert.NotEquals(t, az.Token, "") + assert.Equals(t, az.Status, acme.StatusPending) + assert.Equals(t, az.Identifier, nor.Identifiers[0]) + assert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3}) + assert.Equals(t, az.Wildcard, false) + return nil + }, + MockCreateOrder: func(ctx context.Context, o *acme.Order) error { + o.ID = "ordID" + assert.Equals(t, o.AccountID, "accID") + assert.Equals(t, o.ProvisionerID, prov.GetID()) + assert.Equals(t, o.Status, acme.StatusPending) + assert.Equals(t, o.Identifiers, nor.Identifiers) + assert.Equals(t, o.AuthorizationIDs, []string{*az1ID}) + return nil + }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, + }, + vr: func(t *testing.T, o *acme.Order) { + now := clock.Now() + testBufferDur := 5 * time.Second + orderExpiry := now.Add(defaultOrderExpiry) + expNbf := now.Add(-defaultOrderBackdate) + expNaf := now.Add(prov.DefaultTLSCertDuration()) + assert.Equals(t, o.ID, "ordID") assert.Equals(t, o.Status, acme.StatusPending) assert.Equals(t, o.Identifiers, nor.Identifiers) diff --git a/acme/api/revoke_test.go b/acme/api/revoke_test.go index aa3dda10..9b1fd6d5 100644 --- a/acme/api/revoke_test.go +++ b/acme/api/revoke_test.go @@ -24,14 +24,16 @@ import ( "github.com/go-chi/chi" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" + "golang.org/x/crypto/ocsp" + + "go.step.sm/crypto/jose" + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/x509util" + "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/crypto/jose" - "go.step.sm/crypto/keyutil" - "go.step.sm/crypto/x509util" - "golang.org/x/crypto/ocsp" ) // v is a utility function to return the pointer to an integer @@ -274,8 +276,9 @@ func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error } type mockCA struct { - MockIsRevoked func(sn string) (bool, error) - MockRevoke func(ctx context.Context, opts *authority.RevokeOptions) error + MockIsRevoked func(sn string) (bool, error) + MockRevoke func(ctx context.Context, opts *authority.RevokeOptions) error + MockAreSANsallowed func(ctx context.Context, sans []string) error } func (m *mockCA) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { @@ -283,6 +286,9 @@ func (m *mockCA) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, } func (m *mockCA) AreSANsAllowed(ctx context.Context, sans []string) error { + if m.MockAreSANsallowed != nil { + return m.MockAreSANsallowed(ctx, sans) + } return nil } diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 0f01b009..e11ac317 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -5,7 +5,9 @@ import ( "net/http" "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority/admin" ) @@ -85,3 +87,78 @@ func (h *ACMEAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r * func (h *ACMEAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm")) } + +func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey { + + if k == nil { + return nil + } + + eak := &linkedca.EABKey{ + Id: k.ID, + HmacKey: k.KeyBytes, + Provisioner: k.ProvisionerID, + Reference: k.Reference, + Account: k.AccountID, + CreatedAt: timestamppb.New(k.CreatedAt), + BoundAt: timestamppb.New(k.BoundAt), + } + + if k.Policy != nil { + eak.Policy = &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{}, + Deny: &linkedca.X509Names{}, + }, + } + eak.Policy.X509.Allow.Dns = k.Policy.X509.Allowed.DNSNames + eak.Policy.X509.Allow.Ips = k.Policy.X509.Allowed.IPRanges + eak.Policy.X509.Deny.Dns = k.Policy.X509.Denied.DNSNames + eak.Policy.X509.Deny.Ips = k.Policy.X509.Denied.IPRanges + } + + return eak +} + +func linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey { + if k == nil { + return nil + } + + eak := &acme.ExternalAccountKey{ + ID: k.Id, + ProvisionerID: k.Provisioner, + Reference: k.Reference, + AccountID: k.Account, + KeyBytes: k.HmacKey, + CreatedAt: k.CreatedAt.AsTime(), + BoundAt: k.BoundAt.AsTime(), + } + + if k.Policy == nil { + return eak + } + + eak.Policy = &acme.Policy{} + + if k.Policy.X509 == nil { + return eak + } + + eak.Policy.X509 = acme.X509Policy{ + Allowed: acme.PolicyNames{}, + Denied: acme.PolicyNames{}, + } + + if k.Policy.X509.Allow != nil { + eak.Policy.X509.Allowed.DNSNames = k.Policy.X509.Allow.Dns + eak.Policy.X509.Allowed.IPRanges = k.Policy.X509.Allow.Ips + } + + if k.Policy.X509.Deny != nil { + eak.Policy.X509.Denied.DNSNames = k.Policy.X509.Deny.Dns + eak.Policy.X509.Denied.IPRanges = k.Policy.X509.Deny.Ips + } + + return eak +} diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 98c56f08..d9f340f3 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/go-chi/chi" - "google.golang.org/protobuf/types/known/timestamppb" "go.step.sm/linkedca" @@ -148,78 +147,3 @@ func (h *Handler) loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc next(w, r.WithContext(ctx)) } } - -func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey { - - if k == nil { - return nil - } - - eak := &linkedca.EABKey{ - Id: k.ID, - HmacKey: k.KeyBytes, - Provisioner: k.ProvisionerID, - Reference: k.Reference, - Account: k.AccountID, - CreatedAt: timestamppb.New(k.CreatedAt), - BoundAt: timestamppb.New(k.BoundAt), - } - - if k.Policy != nil { - eak.Policy = &linkedca.Policy{ - X509: &linkedca.X509Policy{ - Allow: &linkedca.X509Names{}, - Deny: &linkedca.X509Names{}, - }, - } - eak.Policy.X509.Allow.Dns = k.Policy.X509.Allowed.DNSNames - eak.Policy.X509.Allow.Ips = k.Policy.X509.Allowed.IPRanges - eak.Policy.X509.Deny.Dns = k.Policy.X509.Denied.DNSNames - eak.Policy.X509.Deny.Ips = k.Policy.X509.Denied.IPRanges - } - - return eak -} - -func linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey { - if k == nil { - return nil - } - - eak := &acme.ExternalAccountKey{ - ID: k.Id, - ProvisionerID: k.Provisioner, - Reference: k.Reference, - AccountID: k.Account, - KeyBytes: k.HmacKey, - CreatedAt: k.CreatedAt.AsTime(), - BoundAt: k.BoundAt.AsTime(), - } - - if k.Policy == nil { - return eak - } - - eak.Policy = &acme.Policy{} - - if k.Policy.X509 == nil { - return eak - } - - eak.Policy.X509 = acme.X509Policy{ - Allowed: acme.PolicyNames{}, - Denied: acme.PolicyNames{}, - } - - if k.Policy.X509.Allow != nil { - eak.Policy.X509.Allowed.DNSNames = k.Policy.X509.Allow.Dns - eak.Policy.X509.Allowed.IPRanges = k.Policy.X509.Allow.Ips - } - - if k.Policy.X509.Deny != nil { - eak.Policy.X509.Denied.DNSNames = k.Policy.X509.Deny.Dns - eak.Policy.X509.Denied.IPRanges = k.Policy.X509.Deny.Ips - } - - return eak -} From 30d5d89a13fa4bbb272801a9e82c28e271045647 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 15 Apr 2022 10:43:10 +0200 Subject: [PATCH 34/78] Improve test coverage for Policy Admin API --- api/read/read.go | 6 +- authority/admin/api/acme_test.go | 213 ++- authority/admin/api/admin_test.go | 24 +- authority/admin/api/middleware.go | 17 +- authority/admin/api/middleware_test.go | 285 +++- authority/admin/api/policy.go | 5 +- authority/admin/api/policy_test.go | 1867 ++++++++++++++++++++++++ authority/policy.go | 30 +- authority/policy_test.go | 10 +- 9 files changed, 2387 insertions(+), 70 deletions(-) create mode 100644 authority/admin/api/policy_test.go diff --git a/api/read/read.go b/api/read/read.go index f4067cb8..2f5175d9 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -35,6 +35,7 @@ func ProtoJSON(r io.Reader, m proto.Message) error { // ProtoJSONWithCheck reads JSON from the request body and stores it in the value // pointed to by m. Returns false if an error was written; true if not. +// TODO(hs): refactor this after the API flow changes are in (or before if that works) func ProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) bool { data, err := io.ReadAll(r) if err != nil { @@ -57,9 +58,12 @@ func ProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) boo if err := protojson.Unmarshal(data, m); err != nil { if errors.Is(err, proto.Error) { var wrapper = struct { - // TODO(hs): more properties in the error response? + Type string `json:"type"` + Detail string `json:"detail"` Message string `json:"message"` }{ + Type: "badRequest", + Detail: "bad request", Message: err.Error(), } errData, err := json.Marshal(wrapper) diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 937ddfa3..e44b4e9b 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -7,17 +7,19 @@ import ( "io" "net/http" "net/http/httptest" + "reflect" "strings" "testing" + "time" "github.com/go-chi/chi" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/authority/admin" + "go.step.sm/linkedca" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - - "go.step.sm/linkedca" - - "github.com/smallstep/assert" - "github.com/smallstep/certificates/authority/admin" + "google.golang.org/protobuf/types/known/timestamppb" ) func readProtoJSON(r io.ReadCloser, m proto.Message) error { @@ -341,3 +343,204 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { }) } } + +func Test_eakToLinked(t *testing.T) { + tests := []struct { + name string + k *acme.ExternalAccountKey + want *linkedca.EABKey + }{ + { + name: "no-key", + k: nil, + want: nil, + }, + { + name: "no-policy", + k: &acme.ExternalAccountKey{ + ID: "keyID", + ProvisionerID: "provID", + Reference: "ref", + AccountID: "accID", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), + BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), + Policy: nil, + }, + want: &linkedca.EABKey{ + Id: "keyID", + Provisioner: "provID", + HmacKey: []byte{1, 3, 3, 7}, + Reference: "ref", + Account: "accID", + CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)), + BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)), + Policy: nil, + }, + }, + { + name: "with-policy", + k: &acme.ExternalAccountKey{ + ID: "keyID", + ProvisionerID: "provID", + Reference: "ref", + AccountID: "accID", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), + BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), + Policy: &acme.Policy{ + X509: acme.X509Policy{ + Allowed: acme.PolicyNames{ + DNSNames: []string{"*.local"}, + IPRanges: []string{"10.0.0.0/24"}, + }, + Denied: acme.PolicyNames{ + DNSNames: []string{"badhost.local"}, + IPRanges: []string{"10.0.0.30"}, + }, + }, + }, + }, + want: &linkedca.EABKey{ + Id: "keyID", + Provisioner: "provID", + HmacKey: []byte{1, 3, 3, 7}, + Reference: "ref", + Account: "accID", + CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)), + BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)), + Policy: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + Ips: []string{"10.0.0.0/24"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"badhost.local"}, + Ips: []string{"10.0.0.30"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := eakToLinked(tt.k); !reflect.DeepEqual(got, tt.want) { + t.Errorf("eakToLinked() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_linkedEAKToCertificates(t *testing.T) { + tests := []struct { + name string + k *linkedca.EABKey + want *acme.ExternalAccountKey + }{ + { + name: "no-key", + k: nil, + want: nil, + }, + { + name: "no-policy", + k: &linkedca.EABKey{ + Id: "keyID", + Provisioner: "provID", + HmacKey: []byte{1, 3, 3, 7}, + Reference: "ref", + Account: "accID", + CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)), + BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)), + Policy: nil, + }, + want: &acme.ExternalAccountKey{ + ID: "keyID", + ProvisionerID: "provID", + Reference: "ref", + AccountID: "accID", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), + BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), + Policy: nil, + }, + }, + { + name: "no-x509-policy", + k: &linkedca.EABKey{ + Id: "keyID", + Provisioner: "provID", + HmacKey: []byte{1, 3, 3, 7}, + Reference: "ref", + Account: "accID", + CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)), + BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)), + Policy: &linkedca.Policy{}, + }, + want: &acme.ExternalAccountKey{ + ID: "keyID", + ProvisionerID: "provID", + Reference: "ref", + AccountID: "accID", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), + BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), + Policy: &acme.Policy{}, + }, + }, + { + name: "with-x509-policy", + k: &linkedca.EABKey{ + Id: "keyID", + Provisioner: "provID", + HmacKey: []byte{1, 3, 3, 7}, + Reference: "ref", + Account: "accID", + CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)), + BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)), + Policy: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + Ips: []string{"10.0.0.0/24"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"badhost.local"}, + Ips: []string{"10.0.0.30"}, + }, + }, + }, + }, + want: &acme.ExternalAccountKey{ + ID: "keyID", + ProvisionerID: "provID", + Reference: "ref", + AccountID: "accID", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), + BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), + Policy: &acme.Policy{ + X509: acme.X509Policy{ + Allowed: acme.PolicyNames{ + DNSNames: []string{"*.local"}, + IPRanges: []string{"10.0.0.0/24"}, + }, + Denied: acme.PolicyNames{ + DNSNames: []string{"badhost.local"}, + IPRanges: []string{"10.0.0.30"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := linkedEAKToCertificates(tt.k); !reflect.DeepEqual(got, tt.want) { + t.Errorf("linkedEAKToCertificates() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go index 678cf6a1..cc77ef77 100644 --- a/authority/admin/api/admin_test.go +++ b/authority/admin/api/admin_test.go @@ -41,8 +41,8 @@ type mockAdminAuthority struct { MockRemoveProvisioner func(ctx context.Context, id string) error MockGetAuthorityPolicy func(ctx context.Context) (*linkedca.Policy, error) - MockCreateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) (*linkedca.Policy, error) - MockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error + MockCreateAuthorityPolicy func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) + MockUpdateAuthorityPolicy func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) MockRemoveAuthorityPolicy func(ctx context.Context) error } @@ -138,19 +138,31 @@ func (m *mockAdminAuthority) RemoveProvisioner(ctx context.Context, id string) e } func (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { - return nil, errors.New("not implemented yet") + if m.MockGetAuthorityPolicy != nil { + return m.MockGetAuthorityPolicy(ctx) + } + return m.MockRet1.(*linkedca.Policy), m.MockErr } func (m *mockAdminAuthority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { - return nil, errors.New("not implemented yet") + if m.MockCreateAuthorityPolicy != nil { + return m.MockCreateAuthorityPolicy(ctx, adm, policy) + } + return m.MockRet1.(*linkedca.Policy), m.MockErr } func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { - return nil, errors.New("not implemented yet") + if m.MockUpdateAuthorityPolicy != nil { + return m.MockUpdateAuthorityPolicy(ctx, adm, policy) + } + return m.MockRet1.(*linkedca.Policy), m.MockErr } func (m *mockAdminAuthority) RemoveAuthorityPolicy(ctx context.Context) error { - return errors.New("not implemented yet") + if m.MockRemoveAuthorityPolicy != nil { + return m.MockRemoveAuthorityPolicy(ctx) + } + return m.MockErr } func TestCreateAdminRequest_Validate(t *testing.T) { diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index d9f340f3..45f46753 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -1,6 +1,7 @@ package api import ( + "errors" "net/http" "github.com/go-chi/chi" @@ -82,13 +83,6 @@ func (h *Handler) loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc func (h *Handler) checkAction(next http.HandlerFunc, supportedInStandalone bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // // temporarily only support the admin nosql DB - // if _, ok := h.adminDB.(*nosql.DB); !ok { - // render.Error(w, admin.NewError(admin.ErrorNotImplementedType, - // "operation not supported")) - // return - // } - // actions allowed in standalone mode are always supported if supportedInStandalone { next(w, r) @@ -130,13 +124,16 @@ func (h *Handler) loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc } if err != nil { - // TODO: handle error; not found vs. some internal server error - render.Error(w, admin.WrapErrorISE(err, "error retrieving ACME External Account key")) + if errors.Is(err, acme.ErrNotFound) { + render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found")) + return + } + render.Error(w, admin.WrapErrorISE(err, "error retrieving ACME External Account Key")) return } if eak == nil { - render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key does not exist")) + render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found")) return } diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index cc0f7a8d..5936563d 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -19,6 +19,7 @@ import ( "go.step.sm/linkedca" "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin/db/nosql" "github.com/smallstep/certificates/authority/provisioner" @@ -359,7 +360,6 @@ func TestHandler_loadProvisionerByName(t *testing.T) { } func TestHandler_checkAction(t *testing.T) { - type test struct { adminDB admin.DB next http.HandlerFunc @@ -368,15 +368,6 @@ func TestHandler_checkAction(t *testing.T) { 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, @@ -393,27 +384,23 @@ func TestHandler_checkAction(t *testing.T) { return test{ supportedInStandalone: false, adminDB: &nosql.DB{}, + statusCode: 501, + err: err, + } + }, + "standalone-no-nosql-not-supported": func(t *testing.T) test { + 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, + statusCode: 200, 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) @@ -448,3 +435,251 @@ func TestHandler_checkAction(t *testing.T) { }) } } + +func TestHandler_loadExternalAccountKey(t *testing.T) { + type test struct { + ctx context.Context + acmeDB acme.DB + next http.HandlerFunc + err *admin.Error + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/keyID-not-found-error": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + } + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("keyID", "key") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found") + err.Message = "ACME External Account Key not found" + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "key", keyID) + return nil, acme.ErrNotFound + }, + }, + err: err, + statusCode: 404, + } + }, + "fail/keyID-error": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + } + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("keyID", "key") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + err := admin.WrapErrorISE(errors.New("force"), "error retrieving ACME External Account Key") + err.Message = "error retrieving ACME External Account Key: force" + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "key", keyID) + return nil, errors.New("force") + }, + }, + err: err, + statusCode: 500, + } + }, + "fail/reference-not-found-error": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + } + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("reference", "ref") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found") + err.Message = "ACME External Account Key not found" + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "ref", reference) + return nil, acme.ErrNotFound + }, + }, + err: err, + statusCode: 404, + } + }, + "fail/reference-error": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + } + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("reference", "ref") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + err := admin.WrapErrorISE(errors.New("force"), "error retrieving ACME External Account Key") + err.Message = "error retrieving ACME External Account Key: force" + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "ref", reference) + return nil, errors.New("force") + }, + }, + err: err, + statusCode: 500, + } + }, + "fail/no-key": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + } + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("reference", "ref") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found") + err.Message = "ACME External Account Key not found" + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "ref", reference) + return nil, nil + }, + }, + err: err, + statusCode: 404, + } + }, + "ok/keyID": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + } + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("keyID", "eakID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found") + err.Message = "ACME External Account Key not found" + createdAt := time.Now().Add(-1 * time.Hour) + var boundAt time.Time + eak := &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: "provID", + CreatedAt: createdAt, + BoundAt: boundAt, + } + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "eakID", keyID) + return eak, nil + }, + }, + next: func(w http.ResponseWriter, r *http.Request) { + contextEAK := linkedca.ExternalAccountKeyFromContext(r.Context()) + assert.NotNil(t, eak) + exp := &linkedca.EABKey{ + Id: "eakID", + Provisioner: "provID", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAt), + } + assert.Equals(t, exp, contextEAK) + w.Write(nil) // mock response with status 200 + }, + err: nil, + statusCode: 200, + } + }, + "ok/reference": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + } + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("reference", "ref") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found") + err.Message = "ACME External Account Key not found" + createdAt := time.Now().Add(-1 * time.Hour) + var boundAt time.Time + eak := &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: "provID", + Reference: "ref", + CreatedAt: createdAt, + BoundAt: boundAt, + } + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "ref", reference) + return eak, nil + }, + }, + next: func(w http.ResponseWriter, r *http.Request) { + contextEAK := linkedca.ExternalAccountKeyFromContext(r.Context()) + assert.NotNil(t, eak) + exp := &linkedca.EABKey{ + Id: "eakID", + Provisioner: "provID", + Reference: "ref", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAt), + } + assert.Equals(t, exp, contextEAK) + w.Write(nil) // mock response with status 200 + }, + err: nil, + statusCode: 200, + } + }, + } + + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("GET", "/foo", nil) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.loadExternalAccountKey(tc.next)(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 + } + }) + } +} diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 959eccd5..17bc454c 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -120,8 +120,7 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r } var newPolicy = new(linkedca.Policy) - if err := read.ProtoJSON(r.Body, newPolicy); err != nil { - render.Error(w, err) + if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { return } @@ -242,7 +241,7 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, return } - render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy")) + render.Error(w, admin.WrapErrorISE(err, "error updating provisioner policy")) return } diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go new file mode 100644 index 00000000..ab09c5bd --- /dev/null +++ b/authority/admin/api/policy_test.go @@ -0,0 +1,1867 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "google.golang.org/protobuf/encoding/protojson" + + "go.step.sm/linkedca" + + "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/admin" +) + +func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { + type test struct { + auth adminAuthority + adminDB admin.DB + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { + ctx := context.Background() + err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") + err.Message = "error retrieving authority policy: force" + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorServerInternalType, "force") + }, + }, + err: err, + statusCode: 500, + } + }, + "fail/auth.GetAuthorityPolicy-not-found": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist") + err.Message = "authority policy does not exist" + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorNotFoundType, "not found") + }, + }, + err: err, + statusCode: 404, + } + }, + "ok": func(t *testing.T) test { + ctx := context.Background() + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return policy, nil + }, + }, + policy: policy, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + auth: tc.auth, + adminDB: tc.adminDB, + } + + req := httptest.NewRequest("GET", "/foo", nil) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.GetAuthorityPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { + type test struct { + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { + ctx := context.Background() + err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") + err.Message = "error retrieving authority policy: force" + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorServerInternalType, "force") + }, + }, + err: err, + statusCode: 500, + } + }, + "fail/existing-policy": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorBadRequestType, "authority already has a policy") + err.Message = "authority already has a policy" + err.Status = http.StatusConflict + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{}, nil + }, + }, + err: err, + statusCode: 409, + } + }, + "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + ctx := context.Background() + adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") + adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" + body := []byte("{?}") + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorNotFoundType, "not found") + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/CreateAuthorityPolicy-policy-admin-lockout-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + ctx := context.Background() + ctx = linkedca.NewContextWithAdmin(ctx, adm) + adminErr := admin.NewError(admin.ErrorBadRequestType, "error storing authority policy") + adminErr.Message = "error storing authority policy: admin lock out" + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorNotFoundType, "not found") + }, + MockCreateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { + return nil, &authority.PolicyError{ + Typ: authority.AdminLockOut, + Err: errors.New("admin lock out"), + } + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{ + adm, + { + Subject: "anotherAdmin", + }, + }, nil + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/CreateAuthorityPolicy-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + ctx := context.Background() + ctx = linkedca.NewContextWithAdmin(ctx, adm) + adminErr := admin.NewError(admin.ErrorServerInternalType, "error storing authority policy: force") + adminErr.Message = "error storing authority policy: force" + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorNotFoundType, "not found") + }, + MockCreateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { + return nil, &authority.PolicyError{ + Typ: authority.StoreFailure, + Err: errors.New("force"), + } + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{ + adm, + { + Subject: "anotherAdmin", + }, + }, nil + }, + }, + body: body, + err: adminErr, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + ctx := context.Background() + ctx = linkedca.NewContextWithAdmin(ctx, adm) + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorNotFoundType, "not found") + }, + MockCreateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { + return policy, nil + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{ + adm, + { + Subject: "anotherAdmin", + }, + }, nil + }, + }, + body: body, + policy: policy, + statusCode: 201, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.CreateAuthorityPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { + type test struct { + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { + ctx := context.Background() + err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") + err.Message = "error retrieving authority policy: force" + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorServerInternalType, "force") + }, + }, + err: err, + statusCode: 500, + } + }, + "fail/no-existing-policy": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist") + err.Message = "authority policy does not exist" + err.Status = http.StatusNotFound + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, nil + }, + }, + err: err, + statusCode: 404, + } + }, + "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + ctx := context.Background() + adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") + adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" + body := []byte("{?}") + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return policy, nil + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/UpdateAuthorityPolicy-policy-admin-lockout-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + ctx := context.Background() + ctx = linkedca.NewContextWithAdmin(ctx, adm) + adminErr := admin.NewError(admin.ErrorBadRequestType, "error updating authority policy: force") + adminErr.Message = "error updating authority policy: admin lock out" + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return policy, nil + }, + MockUpdateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { + return nil, &authority.PolicyError{ + Typ: authority.AdminLockOut, + Err: errors.New("admin lock out"), + } + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{ + adm, + { + Subject: "anotherAdmin", + }, + }, nil + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/UpdateAuthorityPolicy-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + ctx := context.Background() + ctx = linkedca.NewContextWithAdmin(ctx, adm) + adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating authority policy: force") + adminErr.Message = "error updating authority policy: force" + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return policy, nil + }, + MockUpdateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { + return nil, &authority.PolicyError{ + Typ: authority.StoreFailure, + Err: errors.New("force"), + } + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{ + adm, + { + Subject: "anotherAdmin", + }, + }, nil + }, + }, + body: body, + err: adminErr, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + ctx := context.Background() + ctx = linkedca.NewContextWithAdmin(ctx, adm) + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return policy, nil + }, + MockUpdateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) { + return policy, nil + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{ + adm, + { + Subject: "anotherAdmin", + }, + }, nil + }, + }, + body: body, + policy: policy, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.UpdateAuthorityPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { + type test struct { + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int + } + + var tests = map[string]func(t *testing.T) test{ + "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { + ctx := context.Background() + err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") + err.Message = "error retrieving authority policy: force" + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorServerInternalType, "force") + }, + }, + err: err, + statusCode: 500, + } + }, + "fail/no-existing-policy": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist") + err.Message = "authority policy does not exist" + err.Status = http.StatusNotFound + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, nil + }, + }, + err: err, + statusCode: 404, + } + }, + "fail/auth.RemoveAuthorityPolicy-error": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + ctx := context.Background() + err := admin.NewErrorISE("error deleting authority policy: force") + err.Message = "error deleting authority policy: force" + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return policy, nil + }, + MockRemoveAuthorityPolicy: func(ctx context.Context) error { + return errors.New("force") + }, + }, + err: err, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + ctx := context.Background() + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return policy, nil + }, + MockRemoveAuthorityPolicy: func(ctx context.Context) error { + return nil + }, + }, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.DeleteAuthorityPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + assert.FatalError(t, err) + res.Body.Close() + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + }) + } +} + +func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { + type test struct { + auth adminAuthority + adminDB admin.DB + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/prov-no-policy": func(t *testing.T) test { + prov := &linkedca.Provisioner{} + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist") + err.Message = "provisioner policy does not exist" + return test{ + ctx: ctx, + err: err, + statusCode: 404, + } + }, + "ok": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + return test{ + ctx: ctx, + policy: policy, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("GET", "/foo", nil) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.GetProvisionerPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { + type test struct { + auth adminAuthority + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/existing-policy": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewError(admin.ErrorBadRequestType, "provisioner provName already has a policy") + err.Message = "provisioner provName already has a policy" + err.Status = http.StatusConflict + return test{ + ctx: ctx, + err: err, + statusCode: 409, + } + }, + "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") + adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" + body := []byte("{?}") + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/auth.UpdateProvisioner-policy-admin-lockout-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithAdmin(context.Background(), adm) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "error creating provisioner policy") + adminErr.Message = "error creating provisioner policy: admin lock out" + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return &authority.PolicyError{ + Typ: authority.AdminLockOut, + Err: errors.New("admin lock out"), + } + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/auth.UpdateProvisioner-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithAdmin(context.Background(), adm) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + adminErr := admin.NewError(admin.ErrorServerInternalType, "error creating provisioner policy: force") + adminErr.Message = "error creating provisioner policy: force" + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return &authority.PolicyError{ + Typ: authority.StoreFailure, + Err: errors.New("force"), + } + }, + }, + body: body, + err: adminErr, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithAdmin(context.Background(), adm) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return nil + }, + }, + body: body, + policy: policy, + statusCode: 201, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + auth: tc.auth, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.CreateProvisionerPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { + type test struct { + auth adminAuthority + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/no-existing-policy": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist") + err.Message = "provisioner policy does not exist" + return test{ + ctx: ctx, + err: err, + statusCode: 404, + } + }, + "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") + adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" + body := []byte("{?}") + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/auth.UpdateProvisioner-policy-admin-lockout-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Policy: policy, + } + ctx := linkedca.NewContextWithAdmin(context.Background(), adm) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "error updating provisioner policy") + adminErr.Message = "error updating provisioner policy: admin lock out" + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return &authority.PolicyError{ + Typ: authority.AdminLockOut, + Err: errors.New("admin lock out"), + } + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/auth.UpdateProvisioner-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Policy: policy, + } + ctx := linkedca.NewContextWithAdmin(context.Background(), adm) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating provisioner policy: force") + adminErr.Message = "error updating provisioner policy: force" + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return &authority.PolicyError{ + Typ: authority.StoreFailure, + Err: errors.New("force"), + } + }, + }, + body: body, + err: adminErr, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Policy: policy, + } + ctx := linkedca.NewContextWithAdmin(context.Background(), adm) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return nil + }, + }, + body: body, + policy: policy, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + auth: tc.auth, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.UpdateProvisionerPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { + type test struct { + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int + } + + var tests = map[string]func(t *testing.T) test{ + "fail/no-existing-policy": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist") + err.Message = "provisioner policy does not exist" + return test{ + ctx: ctx, + err: err, + statusCode: 404, + } + }, + "fail/auth.UpdateProvisioner-error": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Policy: &linkedca.Policy{}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewErrorISE("error deleting provisioner policy: force") + err.Message = "error deleting provisioner policy: force" + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return errors.New("force") + }, + }, + err: err, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Policy: &linkedca.Policy{}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return nil + }, + }, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.DeleteProvisionerPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + assert.FatalError(t, err) + res.Body.Close() + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + }) + } +} + +func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { + type test struct { + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/no-policy": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + err := admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist") + err.Message = "ACME EAK policy does not exist" + return test{ + ctx: ctx, + err: err, + statusCode: 404, + } + }, + "ok": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + return test{ + ctx: ctx, + policy: policy, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("GET", "/foo", nil) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.GetACMEAccountPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { + type test struct { + acmeDB acme.DB + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/existing-policy": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + err := admin.NewError(admin.ErrorBadRequestType, "ACME EAK eakID already has a policy") + err.Message = "ACME EAK eakID already has a policy" + err.Status = http.StatusConflict + return test{ + ctx: ctx, + err: err, + statusCode: 409, + } + }, + "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") + adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" + body := []byte("{?}") + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/acmeDB.UpdateExternalAccountKey-error": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + adminErr := admin.NewError(admin.ErrorServerInternalType, "error creating ACME EAK policy") + adminErr.Message = "error creating ACME EAK policy: force" + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "eakID", eak.ID) + return errors.New("force") + }, + }, + body: body, + err: adminErr, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "eakID", eak.ID) + return nil + }, + }, + body: body, + policy: policy, + statusCode: 201, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.CreateACMEAccountPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { + type test struct { + acmeDB acme.DB + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/no-existing-policy": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + err := admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist") + err.Message = "ACME EAK policy does not exist" + return test{ + ctx: ctx, + err: err, + statusCode: 404, + } + }, + "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") + adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" + body := []byte("{?}") + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/acmeDB.UpdateExternalAccountKey-error": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Id: "provID", + } + eak := &linkedca.EABKey{ + Id: "eakID", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating ACME EAK policy: force") + adminErr.Message = "error updating ACME EAK policy: force" + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "eakID", eak.ID) + return errors.New("force") + }, + }, + body: body, + err: adminErr, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Id: "provID", + } + eak := &linkedca.EABKey{ + Id: "eakID", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + body, err := protojson.Marshal(policy) + assert.FatalError(t, err) + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "eakID", eak.ID) + return nil + }, + }, + body: body, + policy: policy, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.UpdateACMEAccountPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + p := &linkedca.Policy{} + assert.FatalError(t, readProtoJSON(res.Body, p)) + assert.Equals(t, tc.policy, p) + + }) + } +} + +func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { + type test struct { + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int + } + + var tests = map[string]func(t *testing.T) test{ + "fail/no-existing-policy": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + err := admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist") + err.Message = "ACME EAK policy does not exist" + return test{ + ctx: ctx, + err: err, + statusCode: 404, + } + }, + "fail/acmeDB.UpdateExternalAccountKey-error": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Id: "provID", + } + eak := &linkedca.EABKey{ + Id: "eakID", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + err := admin.NewErrorISE("error deleting ACME EAK policy: force") + err.Message = "error deleting ACME EAK policy: force" + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "eakID", eak.ID) + return errors.New("force") + }, + }, + err: err, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Id: "provID", + } + eak := &linkedca.EABKey{ + Id: "eakID", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + return test{ + ctx: ctx, + acmeDB: &acme.MockDB{ + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "eakID", eak.ID) + return nil + }, + }, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + par := &PolicyAdminResponder{ + acmeDB: tc.acmeDB, + } + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + + par.DeleteACMEAccountPolicy(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + ae := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equals(t, tc.err.Type, ae.Type) + assert.Equals(t, tc.err.Message, ae.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, ae.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + assert.FatalError(t, err) + res.Body.Close() + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + }) + } +} diff --git a/authority/policy.go b/authority/policy.go index cc785173..1793fb9e 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -25,11 +25,11 @@ const ( type PolicyError struct { Typ policyErrorType - err error + Err error } func (p *PolicyError) Error() string { - return p.err.Error() + return p.Err.Error() } func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { @@ -51,21 +51,21 @@ func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm if err := a.checkAuthorityPolicy(ctx, adm, p); err != nil { return nil, &PolicyError{ Typ: AdminLockOut, - err: err, + Err: err, } } if err := a.adminDB.CreateAuthorityPolicy(ctx, p); err != nil { return nil, &PolicyError{ Typ: StoreFailure, - err: err, + Err: err, } } if err := a.reloadPolicyEngines(ctx); err != nil { return nil, &PolicyError{ Typ: ReloadFailure, - err: fmt.Errorf("error reloading policy engines when creating authority policy: %w", err), + Err: fmt.Errorf("error reloading policy engines when creating authority policy: %w", err), } } @@ -83,14 +83,14 @@ func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm if err := a.adminDB.UpdateAuthorityPolicy(ctx, p); err != nil { return nil, &PolicyError{ Typ: StoreFailure, - err: err, + Err: err, } } if err := a.reloadPolicyEngines(ctx); err != nil { return nil, &PolicyError{ Typ: ReloadFailure, - err: fmt.Errorf("error reloading policy engines when updating authority policy %w", err), + Err: fmt.Errorf("error reloading policy engines when updating authority policy %w", err), } } @@ -104,14 +104,14 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { if err := a.adminDB.DeleteAuthorityPolicy(ctx); err != nil { return &PolicyError{ Typ: StoreFailure, - err: err, + Err: err, } } if err := a.reloadPolicyEngines(ctx); err != nil { return &PolicyError{ Typ: ReloadFailure, - err: fmt.Errorf("error reloading policy engines when deleting authority policy %w", err), + Err: fmt.Errorf("error reloading policy engines when deleting authority policy %w", err), } } @@ -130,7 +130,7 @@ func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *link if err != nil { return &PolicyError{ Typ: InternalFailure, - err: fmt.Errorf("error retrieving admins: %w", err), + Err: fmt.Errorf("error retrieving admins: %w", err), } } @@ -149,7 +149,7 @@ func (a *Authority) checkProvisionerPolicy(ctx context.Context, currentAdmin *li if !ok { return &PolicyError{ Typ: InternalFailure, - err: errors.New("error retrieving admins by provisioner"), + Err: errors.New("error retrieving admins by provisioner"), } } @@ -170,7 +170,7 @@ func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admi if err != nil { return &PolicyError{ Typ: ConfigurationFailure, - err: err, + Err: err, } } @@ -217,19 +217,19 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error { if isNamePolicyError && 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), + 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, + Err: err, } } if !allowed { 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), + 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), } } diff --git a/authority/policy_test.go b/authority/policy_test.go index 87c96a87..38132a7c 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -34,7 +34,7 @@ func TestAuthority_checkPolicy(t *testing.T) { }, err: &PolicyError{ Typ: ConfigurationFailure, - err: errors.New("cannot parse permitted domain constraint \"**.local\": domain constraint \"**.local\" can only have wildcard as starting character"), + Err: errors.New("cannot parse permitted domain constraint \"**.local\": domain constraint \"**.local\" can only have wildcard as starting character"), }, } }, @@ -52,7 +52,7 @@ func TestAuthority_checkPolicy(t *testing.T) { }, err: &PolicyError{ Typ: EvaluationFailure, - err: errors.New("cannot parse domain: dns \"*\" cannot be converted to ASCII"), + Err: errors.New("cannot parse domain: dns \"*\" cannot be converted to ASCII"), }, } }, @@ -74,7 +74,7 @@ func TestAuthority_checkPolicy(t *testing.T) { }, 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"), + Err: errors.New("the provided policy would lock out [step] from the CA. Please update your policy to include [step] as an allowed name"), }, } }, @@ -99,7 +99,7 @@ func TestAuthority_checkPolicy(t *testing.T) { }, err: &PolicyError{ Typ: EvaluationFailure, - err: errors.New("cannot parse domain: dns \"**\" cannot be converted to ASCII"), + Err: errors.New("cannot parse domain: dns \"**\" cannot be converted to ASCII"), }, } }, @@ -121,7 +121,7 @@ func TestAuthority_checkPolicy(t *testing.T) { }, 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"), + Err: errors.New("the provided policy would lock out [otherAdmin] from the CA. Please update your policy to include [otherAdmin] as an allowed name"), }, } }, From a9f033ece594929dea1b65cd8c61284e4036689b Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 15 Apr 2022 10:58:29 +0200 Subject: [PATCH 35/78] Fix JSON property name for ACME policy --- acme/account.go | 4 ++-- acme/api/order.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/account.go b/acme/account.go index 21c37314..51225b49 100644 --- a/acme/account.go +++ b/acme/account.go @@ -53,8 +53,8 @@ type PolicyNames struct { // X509Policy contains ACME account level X.509 policy type X509Policy struct { - Allowed PolicyNames `json:"allowed"` - Denied PolicyNames `json:"denied"` + Allowed PolicyNames `json:"allow"` + Denied PolicyNames `json:"deny"` } // Policy is an ACME Account level policy diff --git a/acme/api/order.go b/acme/api/order.go index b4f7cf27..820b642f 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -119,7 +119,7 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { } for _, identifier := range nor.Identifiers { - // evalue the ACME account level policy + // evaluate the ACME account level policy if err = isIdentifierAllowed(acmePolicy, identifier); err != nil { render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) return From 99702d36484eadd855b2aaf46e1c9945c84e985a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 18 Apr 2022 21:14:30 +0200 Subject: [PATCH 36/78] Fix case of no authority policy existing --- authority/admin/db/nosql/policy.go | 59 +- authority/admin/db/nosql/policy_test.go | 737 ++++++++++++++++++++++++ authority/policy.go | 6 +- 3 files changed, 774 insertions(+), 28 deletions(-) create mode 100644 authority/admin/db/nosql/policy_test.go diff --git a/authority/admin/db/nosql/policy.go b/authority/admin/db/nosql/policy.go index d26e44a0..b309f50c 100644 --- a/authority/admin/db/nosql/policy.go +++ b/authority/admin/db/nosql/policy.go @@ -3,12 +3,12 @@ package nosql import ( "context" "encoding/json" + "fmt" - "github.com/pkg/errors" + "go.step.sm/linkedca" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/nosql" - "go.step.sm/linkedca" ) type dbAuthorityPolicy struct { @@ -18,32 +18,29 @@ type dbAuthorityPolicy struct { } func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy { + if dbap == nil { + return nil + } return dbap.Policy } -func (dbap *dbAuthorityPolicy) clone() *dbAuthorityPolicy { - u := *dbap - return &u -} - func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) { data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID)) if nosql.IsErrNotFound(err) { - return nil, admin.NewError(admin.ErrorNotFoundType, "policy %s not found", authorityID) + return nil, admin.NewError(admin.ErrorNotFoundType, "authority policy not found") } else if err != nil { - return nil, errors.Wrapf(err, "error loading admin %s", authorityID) + return nil, fmt.Errorf("error loading authority policy: %w", err) } return data, nil } -func (db *DB) unmarshalDBAuthorityPolicy(data []byte, authorityID string) (*dbAuthorityPolicy, error) { +func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) { + if len(data) == 0 { + return nil, nil + } var dba = new(dbAuthorityPolicy) if err := json.Unmarshal(data, dba); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", authorityID) - } - if dba.AuthorityID != db.authorityID { - return nil, admin.NewError(admin.ErrorAuthorityMismatchType, - "admin %s is not owned by authority %s", dba.ID, db.authorityID) + return nil, fmt.Errorf("error unmarshaling policy bytes into dbAuthorityPolicy: %w", err) } return dba, nil } @@ -53,10 +50,17 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db if err != nil { return nil, err } - dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID) + dbap, err := db.unmarshalDBAuthorityPolicy(data) if err != nil { return nil, err } + if dbap == nil { + return nil, nil + } + if dbap.AuthorityID != authorityID { + return nil, admin.NewError(admin.ErrorAuthorityMismatchType, + "authority policy is not owned by authority %s", authorityID) + } return dbap, nil } @@ -68,12 +72,11 @@ func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy Policy: policy, } - old, err := db.getDBAuthorityPolicy(ctx, db.authorityID) - if err != nil { - return err + if err := db.save(ctx, dbap.ID, dbap, nil, "authority_policy", authorityPoliciesTable); err != nil { + return admin.WrapErrorISE(err, "error creating authority policy") } - return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable) + return nil } func (db *DB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { @@ -97,16 +100,22 @@ func (db *DB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy Policy: policy, } - return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable) + if err := db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable); err != nil { + return admin.WrapErrorISE(err, "error updating authority policy") + } + + return nil } func (db *DB) DeleteAuthorityPolicy(ctx context.Context) error { - dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID) + old, err := db.getDBAuthorityPolicy(ctx, db.authorityID) if err != nil { return err } - old := dbap.clone() - dbap.Policy = nil - return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable) + if err := db.save(ctx, old.ID, nil, old, "authority_policy", authorityPoliciesTable); err != nil { + return admin.WrapErrorISE(err, "error deleting authority policy") + } + + return nil } diff --git a/authority/admin/db/nosql/policy_test.go b/authority/admin/db/nosql/policy_test.go new file mode 100644 index 00000000..09bcd070 --- /dev/null +++ b/authority/admin/db/nosql/policy_test.go @@ -0,0 +1,737 @@ +package nosql + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "go.step.sm/linkedca" + + "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/db" + "github.com/smallstep/nosql" + nosqldb "github.com/smallstep/nosql/database" +) + +func TestDB_getDBAuthorityPolicyBytes(t *testing.T) { + authID := "authID" + type test struct { + ctx context.Context + authorityID string + db nosql.DB + err error + adminErr *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return nil, nosqldb.ErrNotFound + }, + }, + adminErr: admin.NewError(admin.ErrorNotFoundType, "authority policy not found"), + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading authority policy: force"), + } + }, + "ok": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return []byte("foo"), nil + }, + }, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if b, err := d.getDBAuthorityPolicyBytes(tc.ctx, tc.authorityID); err != nil { + switch k := err.(type) { + case *admin.Error: + if assert.NotNil(t, tc.adminErr) { + assert.Equals(t, k.Type, tc.adminErr.Type) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + assert.Equals(t, k.Status, tc.adminErr.Status) + assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error()) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, string(b), "foo") + } + }) + } +} + +func TestDB_getDBAuthorityPolicy(t *testing.T) { + authID := "authID" + type test struct { + ctx context.Context + authorityID string + db nosql.DB + err error + adminErr *admin.Error + dbap *dbAuthorityPolicy + } + var tests = map[string]func(t *testing.T) test{ + "fail/not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return nil, nosqldb.ErrNotFound + }, + }, + adminErr: admin.NewError(admin.ErrorNotFoundType, "authority policy not found"), + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return []byte("foo"), nil + }, + }, + err: errors.New("error unmarshaling policy bytes into dbAuthorityPolicy"), + } + }, + "fail/authorityID-error": func(t *testing.T) test { + dbp := &dbAuthorityPolicy{ + ID: "ID", + AuthorityID: "diffAuthID", + Policy: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + }, + } + b, err := json.Marshal(dbp) + assert.FatalError(t, err) + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return b, nil + }, + }, + adminErr: admin.NewError(admin.ErrorAuthorityMismatchType, + "authority policy is not owned by authority authID"), + } + }, + "ok/empty-bytes": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return []byte{}, nil + }, + }, + } + }, + "ok": func(t *testing.T) test { + dbap := &dbAuthorityPolicy{ + ID: "ID", + AuthorityID: authID, + Policy: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + }, + } + b, err := json.Marshal(dbap) + assert.FatalError(t, err) + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return b, nil + }, + }, + dbap: dbap, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if dbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID); err != nil { + switch k := err.(type) { + case *admin.Error: + if assert.NotNil(t, tc.adminErr) { + assert.Equals(t, k.Type, tc.adminErr.Type) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + assert.Equals(t, k.Status, tc.adminErr.Status) + assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error()) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) && tc.dbap == nil { + assert.Nil(t, dbp) + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, dbp.ID, "ID") + assert.Equals(t, dbp.AuthorityID, tc.dbap.AuthorityID) + assert.Equals(t, dbp.Policy, tc.dbap.Policy) + } + }) + } +} + +func TestDB_CreateAuthorityPolicy(t *testing.T) { + authID := "authID" + type test struct { + ctx context.Context + authorityID string + policy *linkedca.Policy + db nosql.DB + err error + adminErr *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/save-error": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + return test{ + ctx: context.Background(), + authorityID: authID, + policy: policy, + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + var _dbap = new(dbAuthorityPolicy) + assert.FatalError(t, json.Unmarshal(nu, _dbap)) + + assert.Equals(t, _dbap.ID, authID) + assert.Equals(t, _dbap.AuthorityID, authID) + assert.Equals(t, _dbap.Policy, policy) + + return nil, false, errors.New("force") + }, + }, + adminErr: admin.NewErrorISE("error creating authority policy: error saving authority authority_policy: force"), + } + }, + "ok": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + return test{ + ctx: context.Background(), + authorityID: authID, + policy: policy, + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, old, nil) + + var _dbap = new(dbAuthorityPolicy) + assert.FatalError(t, json.Unmarshal(nu, _dbap)) + + assert.Equals(t, _dbap.ID, authID) + assert.Equals(t, _dbap.AuthorityID, authID) + assert.Equals(t, _dbap.Policy, policy) + + return nil, true, nil + }, + }, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db, authorityID: tc.authorityID} + if err := d.CreateAuthorityPolicy(tc.ctx, tc.policy); err != nil { + switch k := err.(type) { + case *admin.Error: + if assert.NotNil(t, tc.adminErr) { + assert.Equals(t, k.Type, tc.adminErr.Type) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + assert.Equals(t, k.Status, tc.adminErr.Status) + assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error()) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } + }) + } +} + +func TestDB_GetAuthorityPolicy(t *testing.T) { + authID := "authID" + type test struct { + ctx context.Context + authorityID string + policy *linkedca.Policy + db nosql.DB + err error + adminErr *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return nil, nosqldb.ErrNotFound + }, + }, + adminErr: admin.NewError(admin.ErrorNotFoundType, "authority policy not found"), + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading authority policy: force"), + } + }, + "ok": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + return test{ + ctx: context.Background(), + authorityID: authID, + policy: policy, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + dbap := &dbAuthorityPolicy{ + ID: authID, + AuthorityID: authID, + Policy: policy, + } + + b, err := json.Marshal(dbap) + assert.FatalError(t, err) + + return b, nil + }, + }, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db, authorityID: tc.authorityID} + got, err := d.GetAuthorityPolicy(tc.ctx) + if err != nil { + switch k := err.(type) { + case *admin.Error: + if assert.NotNil(t, tc.adminErr) { + assert.Equals(t, k.Type, tc.adminErr.Type) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + assert.Equals(t, k.Status, tc.adminErr.Status) + assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error()) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + return + } + + assert.NotNil(t, got) + assert.Equals(t, tc.policy, got) + }) + } +} + +func TestDB_UpdateAuthorityPolicy(t *testing.T) { + authID := "authID" + type test struct { + ctx context.Context + authorityID string + policy *linkedca.Policy + db nosql.DB + err error + adminErr *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return nil, nosqldb.ErrNotFound + }, + }, + adminErr: admin.NewError(admin.ErrorNotFoundType, "authority policy not found"), + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading authority policy: force"), + } + }, + "fail/save-error": func(t *testing.T) test { + oldPolicy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.localhost"}, + }, + }, + } + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + return test{ + ctx: context.Background(), + authorityID: authID, + policy: policy, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + dbap := &dbAuthorityPolicy{ + ID: authID, + AuthorityID: authID, + Policy: oldPolicy, + } + + b, err := json.Marshal(dbap) + assert.FatalError(t, err) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + var _dbap = new(dbAuthorityPolicy) + assert.FatalError(t, json.Unmarshal(nu, _dbap)) + + assert.Equals(t, _dbap.ID, authID) + assert.Equals(t, _dbap.AuthorityID, authID) + assert.Equals(t, _dbap.Policy, policy) + + return nil, false, errors.New("force") + }, + }, + adminErr: admin.NewErrorISE("error updating authority policy: error saving authority authority_policy: force"), + } + }, + "ok": func(t *testing.T) test { + oldPolicy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.localhost"}, + }, + }, + } + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + return test{ + ctx: context.Background(), + authorityID: authID, + policy: policy, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + dbap := &dbAuthorityPolicy{ + ID: authID, + AuthorityID: authID, + Policy: oldPolicy, + } + + b, err := json.Marshal(dbap) + assert.FatalError(t, err) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + var _dbap = new(dbAuthorityPolicy) + assert.FatalError(t, json.Unmarshal(nu, _dbap)) + + assert.Equals(t, _dbap.ID, authID) + assert.Equals(t, _dbap.AuthorityID, authID) + assert.Equals(t, _dbap.Policy, policy) + + return nil, true, nil + }, + }, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db, authorityID: tc.authorityID} + if err := d.UpdateAuthorityPolicy(tc.ctx, tc.policy); err != nil { + switch k := err.(type) { + case *admin.Error: + if assert.NotNil(t, tc.adminErr) { + assert.Equals(t, k.Type, tc.adminErr.Type) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + assert.Equals(t, k.Status, tc.adminErr.Status) + assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error()) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + return + } + }) + } +} + +func TestDB_DeleteAuthorityPolicy(t *testing.T) { + authID := "authID" + type test struct { + ctx context.Context + authorityID string + db nosql.DB + err error + adminErr *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + return nil, nosqldb.ErrNotFound + }, + }, + adminErr: admin.NewError(admin.ErrorNotFoundType, "authority policy not found"), + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading authority policy: force"), + } + }, + "fail/save-error": func(t *testing.T) test { + oldPolicy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.localhost"}, + }, + }, + } + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + dbap := &dbAuthorityPolicy{ + ID: authID, + AuthorityID: authID, + Policy: oldPolicy, + } + + b, err := json.Marshal(dbap) + assert.FatalError(t, err) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + assert.Equals(t, nil, nu) + + return nil, false, errors.New("force") + }, + }, + adminErr: admin.NewErrorISE("error deleting authority policy: error saving authority authority_policy: force"), + } + }, + "ok": func(t *testing.T) test { + oldPolicy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.localhost"}, + }, + }, + } + return test{ + ctx: context.Background(), + authorityID: authID, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + + dbap := &dbAuthorityPolicy{ + ID: authID, + AuthorityID: authID, + Policy: oldPolicy, + } + + b, err := json.Marshal(dbap) + assert.FatalError(t, err) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, authorityPoliciesTable) + assert.Equals(t, string(key), authID) + assert.Equals(t, nil, nu) + + return nil, true, nil + }, + }, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db, authorityID: tc.authorityID} + if err := d.DeleteAuthorityPolicy(tc.ctx); err != nil { + switch k := err.(type) { + case *admin.Error: + if assert.NotNil(t, tc.adminErr) { + assert.Equals(t, k.Type, tc.adminErr.Type) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + assert.Equals(t, k.Status, tc.adminErr.Status) + assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error()) + assert.Equals(t, k.Detail, tc.adminErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + return + } + }) + } +} diff --git a/authority/policy.go b/authority/policy.go index 1793fb9e..b7d5e4ec 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -69,7 +69,7 @@ func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm } } - return p, nil // TODO: return the newly stored policy + return p, nil } func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) { @@ -94,7 +94,7 @@ func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm } } - return p, nil // TODO: return the updated stored policy + return p, nil } func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { @@ -111,7 +111,7 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { if err := a.reloadPolicyEngines(ctx); err != nil { return &PolicyError{ Typ: ReloadFailure, - Err: fmt.Errorf("error reloading policy engines when deleting authority policy %w", err), + Err: fmt.Errorf("error reloading policy engines when deleting authority policy: %w", err), } } From 8d15a027a77203700ffc8947a1a0cc1baaa02d69 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 18 Apr 2022 21:47:13 +0200 Subject: [PATCH 37/78] Fix if-else linting issue --- authority/admin/db/nosql/policy_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/authority/admin/db/nosql/policy_test.go b/authority/admin/db/nosql/policy_test.go index 09bcd070..39be7e13 100644 --- a/authority/admin/db/nosql/policy_test.go +++ b/authority/admin/db/nosql/policy_test.go @@ -205,7 +205,9 @@ func TestDB_getDBAuthorityPolicy(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if dbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID); err != nil { + dbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID) + switch { + case err != nil: switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -220,9 +222,9 @@ func TestDB_getDBAuthorityPolicy(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) && tc.dbap == nil { + case assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) && tc.dbap == nil: assert.Nil(t, dbp) - } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + case assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr): assert.Equals(t, dbp.ID, "ID") assert.Equals(t, dbp.AuthorityID, tc.dbap.AuthorityID) assert.Equals(t, dbp.Policy, tc.dbap.Policy) From 82e0033428c3b27dbeaa146a23e201a0567ad1e1 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 18 Apr 2022 21:47:31 +0200 Subject: [PATCH 38/78] Remove Adder options --- policy/engine.go | 3 + policy/engine_test.go | 207 +++++++-------- policy/options.go | 247 ------------------ policy/options_test.go | 572 ----------------------------------------- 4 files changed, 107 insertions(+), 922 deletions(-) diff --git a/policy/engine.go b/policy/engine.go index afaa2416..fe86ed5c 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -258,6 +258,9 @@ func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, // in the SSH certificate. We're exluding URIs, because they can be confusing // when used in a SSH user certificate. principals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals) + if len(ips) > 0 { + err = fmt.Errorf("IP principals %v not expected in SSH user certificate ", ips) + } if len(uris) > 0 { err = fmt.Errorf("URL principals %v not expected in SSH user certificate ", uris) } diff --git a/policy/engine_test.go b/policy/engine_test.go index dd0b403f..25e69af3 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -637,7 +637,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -648,7 +648,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-wildcard-literal-x509", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.x509local"), + WithPermittedDNSDomain("*.x509local"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -661,7 +661,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-single-host", options: []NamePolicyOption{ - AddPermittedDNSDomain("host.local"), + WithPermittedDNSDomain("host.local"), }, cert: &x509.Certificate{ DNSNames: []string{"differenthost.local"}, @@ -672,7 +672,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-no-label", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"local"}, @@ -683,7 +683,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-empty-label", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"www..local"}, @@ -694,7 +694,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-dot-domain", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -707,7 +707,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-wildcard-multiple-subdomains", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -720,7 +720,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-wildcard-literal", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -733,7 +733,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.豆.jp"), + WithPermittedDNSDomain("*.豆.jp"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -746,7 +746,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/ipv4-permitted", options: []NamePolicyOption{ - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -764,7 +764,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/ipv6-permitted", options: []NamePolicyOption{ - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), @@ -782,7 +782,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-wildcard", options: []NamePolicyOption{ - AddPermittedEmailAddress("@example.com"), + WithPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -795,7 +795,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-wildcard-x509", options: []NamePolicyOption{ - AddPermittedEmailAddress("example.com"), + WithPermittedEmailAddress("example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -808,7 +808,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-specific-mailbox", options: []NamePolicyOption{ - AddPermittedEmailAddress("test@local.com"), + WithPermittedEmailAddress("test@local.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -821,7 +821,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-wildcard-subdomain", options: []NamePolicyOption{ - AddPermittedEmailAddress("@example.com"), + WithPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -834,7 +834,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - AddPermittedEmailAddress("@例.jp"), + WithPermittedEmailAddress("@例.jp"), }, cert: &x509.Certificate{ EmailAddresses: []string{"bücher@例.jp"}, @@ -845,7 +845,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-idna-internationalized-domain-rfc822", options: []NamePolicyOption{ - AddPermittedEmailAddress("@例.jp"), + WithPermittedEmailAddress("@例.jp"), }, cert: &x509.Certificate{ EmailAddresses: []string{"bücher@例.jp" + string(byte(0))}, @@ -856,7 +856,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-idna-internationalized-domain-ascii", options: []NamePolicyOption{ - AddPermittedEmailAddress("@例.jp"), + WithPermittedEmailAddress("@例.jp"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@xn---bla.jp"}, @@ -867,7 +867,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-domain-wildcard", options: []NamePolicyOption{ - AddPermittedURIDomain("*.local"), + WithPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -883,7 +883,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted", options: []NamePolicyOption{ - AddPermittedURIDomain("test.local"), + WithPermittedURIDomain("test.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -899,7 +899,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld options: []NamePolicyOption{ - AddPermittedURIDomain("*.local"), + WithPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -915,7 +915,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - AddPermittedURIDomain("*.bücher.example.com"), + WithPermittedURIDomain("*.bücher.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -932,7 +932,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-excluded", options: []NamePolicyOption{ - AddExcludedDNSDomain("*.example.com"), + WithExcludedDNSDomain("*.example.com"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -943,7 +943,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-excluded-single-host", options: []NamePolicyOption{ - AddExcludedDNSDomain("host.example.com"), + WithExcludedDNSDomain("host.example.com"), }, cert: &x509.Certificate{ DNSNames: []string{"host.example.com"}, @@ -954,7 +954,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/ipv4-excluded", options: []NamePolicyOption{ - AddExcludedIPRanges( + WithExcludedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -972,7 +972,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/ipv6-excluded", options: []NamePolicyOption{ - AddExcludedIPRanges( + WithExcludedIPRanges( []*net.IPNet{ { IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), @@ -990,7 +990,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-excluded", options: []NamePolicyOption{ - AddExcludedEmailAddress("@example.com"), + WithExcludedEmailAddress("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, @@ -1001,7 +1001,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-excluded", options: []NamePolicyOption{ - AddExcludedURIDomain("*.example.com"), + WithExcludedURIDomain("*.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1017,7 +1017,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-excluded-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld options: []NamePolicyOption{ - AddExcludedURIDomain("*.local"), + WithExcludedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1035,7 +1035,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-dns-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1049,7 +1049,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-dns-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedDNSDomain("*.local"), + WithExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1063,7 +1063,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-ipv4-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -1084,7 +1084,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-ipv4-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedIPRanges( + WithExcludedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -1105,7 +1105,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-ipv6-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), @@ -1126,7 +1126,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-ipv6-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedIPRanges( + WithExcludedIPRanges( []*net.IPNet{ { IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), @@ -1147,7 +1147,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-email-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedEmailAddress("@example.local"), + WithPermittedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1161,7 +1161,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-email-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedEmailAddress("@example.local"), + WithExcludedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1175,7 +1175,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-uri-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedURIDomain("*.example.com"), + WithPermittedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1189,7 +1189,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-uri-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedURIDomain("*.example.com"), + WithExcludedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1203,7 +1203,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-with-ip-name", // when only DNS is permitted, IPs are not allowed. options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, @@ -1214,7 +1214,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-with-mail", // when only DNS is permitted, mails are not allowed. options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@smallstep.com"}, @@ -1225,7 +1225,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-with-uri", // when only DNS is permitted, URIs are not allowed. options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1241,7 +1241,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/ip-permitted-with-dns-name", // when only IP is permitted, DNS names are not allowed. options: []NamePolicyOption{ - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -1259,7 +1259,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/ip-permitted-with-mail", // when only IP is permitted, mails are not allowed. options: []NamePolicyOption{ - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -1277,7 +1277,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/ip-permitted-with-uri", // when only IP is permitted, URIs are not allowed. options: []NamePolicyOption{ - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -1300,7 +1300,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-with-dns-name", // when only mail is permitted, DNS names are not allowed. options: []NamePolicyOption{ - AddPermittedEmailAddress("@example.com"), + WithPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -1311,7 +1311,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-with-ip", // when only mail is permitted, IPs are not allowed. options: []NamePolicyOption{ - AddPermittedEmailAddress("@example.com"), + WithPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{ @@ -1324,7 +1324,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-with-uri", // when only mail is permitted, URIs are not allowed. options: []NamePolicyOption{ - AddPermittedEmailAddress("@example.com"), + WithPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1340,7 +1340,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-with-dns-name", // when only URI is permitted, DNS names are not allowed. options: []NamePolicyOption{ - AddPermittedURIDomain("*.local"), + WithPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"host.local"}, @@ -1351,7 +1351,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, IPs are not allowed. options: []NamePolicyOption{ - AddPermittedURIDomain("*.local"), + WithPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{ @@ -1364,7 +1364,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, mails are not allowed. options: []NamePolicyOption{ - AddPermittedURIDomain("*.local"), + WithPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@smallstep.com"}, @@ -1488,7 +1488,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"example.local"}, @@ -1499,8 +1499,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted-wildcard", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), - AddPermittedDNSDomain("*.x509local"), + WithPermittedDNSDomains([]string{"*.local", "*.x509local"}), WithAllowLiteralWildcardNames(), }, cert: &x509.Certificate{ @@ -1515,8 +1514,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted-wildcard-literal", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), - AddPermittedDNSDomain("*.x509local"), + WithPermittedDNSDomains([]string{"*.local", "*.x509local"}), WithAllowLiteralWildcardNames(), }, cert: &x509.Certificate{ @@ -1531,9 +1529,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted-combined", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.local"), - AddPermittedDNSDomain("*.x509local"), - AddPermittedDNSDomain("host.example.com"), + WithPermittedDNSDomains([]string{"*.local", "*.x509local", "host.example.com"}), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -1548,7 +1544,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - AddPermittedDNSDomain("*.例.jp"), + WithPermittedDNSDomain("*.例.jp"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -1561,7 +1557,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ipv4-permitted", options: []NamePolicyOption{ - AddPermittedCIDR("127.0.0.1/24"), + WithPermittedCIDR("127.0.0.1/24"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, @@ -1572,7 +1568,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ipv6-permitted", options: []NamePolicyOption{ - AddPermittedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), + WithPermittedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, @@ -1583,7 +1579,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-permitted-wildcard", options: []NamePolicyOption{ - AddPermittedEmailAddress("@example.com"), + WithPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -1596,7 +1592,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-permitted-plain-domain", options: []NamePolicyOption{ - AddPermittedEmailAddress("example.com"), + WithPermittedEmailAddress("example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -1609,7 +1605,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-permitted-specific-mailbox", options: []NamePolicyOption{ - AddPermittedEmailAddress("test@local.com"), + WithPermittedEmailAddress("test@local.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -1622,7 +1618,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - AddPermittedEmailAddress("@例.jp"), + WithPermittedEmailAddress("@例.jp"), }, cert: &x509.Certificate{ EmailAddresses: []string{}, @@ -1633,7 +1629,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-domain-wildcard", options: []NamePolicyOption{ - AddPermittedURIDomain("*.local"), + WithPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1649,7 +1645,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-specific-uri", options: []NamePolicyOption{ - AddPermittedURIDomain("test.local"), + WithPermittedURIDomain("test.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1665,7 +1661,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-with-port", options: []NamePolicyOption{ - AddPermittedURIDomain("*.example.com"), + WithPermittedURIDomain("*.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1681,7 +1677,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - AddPermittedURIDomain("*.bücher.example.com"), + WithPermittedURIDomain("*.bücher.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1697,7 +1693,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - AddPermittedURIDomain("bücher.example.com"), + WithPermittedURIDomain("bücher.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1725,7 +1721,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ipv4-excluded", options: []NamePolicyOption{ - AddExcludedIPRanges( + WithExcludedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -1743,7 +1739,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ipv6-excluded", options: []NamePolicyOption{ - AddExcludedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), + WithExcludedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, @@ -1794,7 +1790,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-empty", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1809,7 +1805,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-dns-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedDNSDomain("*.local"), + WithPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1823,7 +1819,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-dns-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedDNSDomain("*.notlocal"), + WithExcludedDNSDomain("*.notlocal"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1837,7 +1833,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-ipv4-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), @@ -1858,7 +1854,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-ipv4-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedIPRanges( + WithExcludedIPRanges( []*net.IPNet{ { IP: net.ParseIP("128.0.0.1"), @@ -1879,7 +1875,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-ipv6-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedIPRanges( + WithPermittedIPRanges( []*net.IPNet{ { IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), @@ -1900,7 +1896,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-ipv6-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedIPRanges( + WithExcludedIPRanges( []*net.IPNet{ { IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), @@ -1921,7 +1917,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-email-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedEmailAddress("@example.local"), + WithPermittedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1935,7 +1931,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-email-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedEmailAddress("@example.notlocal"), + WithExcludedEmailAddress("@example.notlocal"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1949,7 +1945,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-uri-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddPermittedURIDomain("*.example.com"), + WithPermittedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1963,7 +1959,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-uri-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedURIDomain("*.smallstep.com"), + WithExcludedURIDomain("*.smallstep.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1977,7 +1973,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-excluded-with-ip-name", // when only DNS is exluded, we allow anything else options: []NamePolicyOption{ - AddExcludedDNSDomain("*.local"), + WithExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, @@ -1988,7 +1984,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else options: []NamePolicyOption{ - AddExcludedDNSDomain("*.local"), + WithExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, @@ -1999,7 +1995,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else options: []NamePolicyOption{ - AddExcludedDNSDomain("*.local"), + WithExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -2125,7 +2121,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/dns-excluded-with-subject-ip-name", // when only DNS is exluded, we allow anything else options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - AddExcludedDNSDomain("*.local"), + WithExcludedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -2750,6 +2746,18 @@ func Test_splitSSHPrincipals(t *testing.T) { wantErr: true, } }, + "fail/user-ip": func(t *testing.T) test { + r := emptyResult() + r.wantIps = []net.IP{net.ParseIP("127.0.0.1")} + return test{ + cert: &ssh.Certificate{ + CertType: ssh.UserCert, + ValidPrincipals: []string{"127.0.0.1"}, + }, + r: r, + wantErr: true, + } + }, "fail/user-uri": func(t *testing.T) test { r := emptyResult() return test{ @@ -2780,7 +2788,8 @@ func Test_splitSSHPrincipals(t *testing.T) { CertType: ssh.HostCert, ValidPrincipals: []string{"host.example.com"}, }, - r: r, + r: r, + wantErr: false, } }, "ok/host-ip": func(t *testing.T) test { @@ -2791,7 +2800,8 @@ func Test_splitSSHPrincipals(t *testing.T) { CertType: ssh.HostCert, ValidPrincipals: []string{"127.0.0.1"}, }, - r: r, + r: r, + wantErr: false, } }, "ok/host-email": func(t *testing.T) test { @@ -2814,7 +2824,8 @@ func Test_splitSSHPrincipals(t *testing.T) { CertType: ssh.UserCert, ValidPrincipals: []string{"localhost"}, }, - r: r, + r: r, + wantErr: false, } }, "ok/user-username-with-period": func(t *testing.T) test { @@ -2825,17 +2836,6 @@ func Test_splitSSHPrincipals(t *testing.T) { CertType: ssh.UserCert, ValidPrincipals: []string{"x.joe"}, }, - r: r, - } - }, - "ok/user-ip": func(t *testing.T) test { - r := emptyResult() - r.wantIps = []net.IP{net.ParseIP("127.0.0.1")} - return test{ - cert: &ssh.Certificate{ - CertType: ssh.UserCert, - ValidPrincipals: []string{"127.0.0.1"}, - }, r: r, wantErr: false, } @@ -2848,7 +2848,8 @@ func Test_splitSSHPrincipals(t *testing.T) { CertType: ssh.UserCert, ValidPrincipals: []string{"ops@work"}, }, - r: r, + r: r, + wantErr: false, } }, } diff --git a/policy/options.go b/policy/options.go index 308d46b5..e01e082e 100755 --- a/policy/options.go +++ b/policy/options.go @@ -41,21 +41,6 @@ func WithPermittedDNSDomains(domains []string) NamePolicyOption { } } -func AddPermittedDNSDomains(domains []string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedDomains := make([]string, len(domains)) - for i, domain := range domains { - normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse permitted domain constraint %q: %w", domain, err) - } - normalizedDomains[i] = normalizedDomain - } - e.permittedDNSDomains = append(e.permittedDNSDomains, normalizedDomains...) - return nil - } -} - func WithExcludedDNSDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomains := make([]string, len(domains)) @@ -71,21 +56,6 @@ func WithExcludedDNSDomains(domains []string) NamePolicyOption { } } -func AddExcludedDNSDomains(domains []string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedDomains := make([]string, len(domains)) - for i, domain := range domains { - normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse excluded domain constraint %q: %w", domain, err) - } - normalizedDomains[i] = normalizedDomain - } - e.excludedDNSDomains = append(e.excludedDNSDomains, normalizedDomains...) - return nil - } -} - func WithPermittedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) @@ -97,17 +67,6 @@ func WithPermittedDNSDomain(domain string) NamePolicyOption { } } -func AddPermittedDNSDomain(domain string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse permitted domain constraint %q: %w", domain, err) - } - e.permittedDNSDomains = append(e.permittedDNSDomains, normalizedDomain) - return nil - } -} - func WithExcludedDNSDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) @@ -119,17 +78,6 @@ func WithExcludedDNSDomain(domain string) NamePolicyOption { } } -func AddExcludedDNSDomain(domain string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse excluded domain constraint %q: %w", domain, err) - } - e.excludedDNSDomains = append(e.excludedDNSDomains, normalizedDomain) - return nil - } -} - func WithPermittedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { return func(e *NamePolicyEngine) error { e.permittedIPRanges = ipRanges @@ -137,13 +85,6 @@ func WithPermittedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { } } -func AddPermittedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { - return func(e *NamePolicyEngine) error { - e.permittedIPRanges = append(e.permittedIPRanges, ipRanges...) - return nil - } -} - func WithPermittedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { networks := make([]*net.IPNet, len(cidrs)) @@ -159,21 +100,6 @@ func WithPermittedCIDRs(cidrs []string) NamePolicyOption { } } -func AddPermittedCIDRs(cidrs []string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - networks := make([]*net.IPNet, len(cidrs)) - for i, cidr := range cidrs { - _, nw, err := net.ParseCIDR(cidr) - if err != nil { - return fmt.Errorf("cannot parse permitted CIDR constraint %q", cidr) - } - networks[i] = nw - } - e.permittedIPRanges = append(e.permittedIPRanges, networks...) - return nil - } -} - func WithExcludedCIDRs(cidrs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { networks := make([]*net.IPNet, len(cidrs)) @@ -189,21 +115,6 @@ func WithExcludedCIDRs(cidrs []string) NamePolicyOption { } } -func AddExcludedCIDRs(cidrs []string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - networks := make([]*net.IPNet, len(cidrs)) - for i, cidr := range cidrs { - _, nw, err := net.ParseCIDR(cidr) - if err != nil { - return fmt.Errorf("cannot parse excluded CIDR constraint %q", cidr) - } - networks[i] = nw - } - e.excludedIPRanges = append(e.excludedIPRanges, networks...) - return nil - } -} - func WithPermittedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { return func(e *NamePolicyEngine) error { networks := make([]*net.IPNet, len(ipsOrCIDRs)) @@ -251,17 +162,6 @@ func WithPermittedCIDR(cidr string) NamePolicyOption { } } -func AddPermittedCIDR(cidr string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - _, nw, err := net.ParseCIDR(cidr) - if err != nil { - return fmt.Errorf("cannot parse permitted CIDR constraint %q", cidr) - } - e.permittedIPRanges = append(e.permittedIPRanges, nw) - return nil - } -} - func WithPermittedIP(ip net.IP) NamePolicyOption { return func(e *NamePolicyEngine) error { nw := networkFor(ip) @@ -270,14 +170,6 @@ func WithPermittedIP(ip net.IP) NamePolicyOption { } } -func AddPermittedIP(ip net.IP) NamePolicyOption { - return func(e *NamePolicyEngine) error { - nw := networkFor(ip) - e.permittedIPRanges = append(e.permittedIPRanges, nw) - return nil - } -} - func WithExcludedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { return func(e *NamePolicyEngine) error { e.excludedIPRanges = ipRanges @@ -285,13 +177,6 @@ func WithExcludedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { } } -func AddExcludedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { - return func(e *NamePolicyEngine) error { - e.excludedIPRanges = append(e.excludedIPRanges, ipRanges...) - return nil - } -} - func WithExcludedCIDR(cidr string) NamePolicyOption { return func(e *NamePolicyEngine) error { _, nw, err := net.ParseCIDR(cidr) @@ -303,17 +188,6 @@ func WithExcludedCIDR(cidr string) NamePolicyOption { } } -func AddExcludedCIDR(cidr string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - _, nw, err := net.ParseCIDR(cidr) - if err != nil { - return fmt.Errorf("cannot parse excluded CIDR constraint %q", cidr) - } - e.excludedIPRanges = append(e.excludedIPRanges, nw) - return nil - } -} - func WithExcludedIP(ip net.IP) NamePolicyOption { return func(e *NamePolicyEngine) error { var mask net.IPMask @@ -331,23 +205,6 @@ func WithExcludedIP(ip net.IP) NamePolicyOption { } } -func AddExcludedIP(ip net.IP) NamePolicyOption { - return func(e *NamePolicyEngine) error { - var mask net.IPMask - if !isIPv4(ip) { - mask = net.CIDRMask(128, 128) - } else { - mask = net.CIDRMask(32, 32) - } - nw := &net.IPNet{ - IP: ip, - Mask: mask, - } - e.excludedIPRanges = append(e.excludedIPRanges, nw) - return nil - } -} - func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddresses := make([]string, len(emailAddresses)) @@ -363,21 +220,6 @@ func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { } } -func AddPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedEmailAddresses := make([]string, len(emailAddresses)) - for i, email := range emailAddresses { - normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) - if err != nil { - return fmt.Errorf("cannot parse permitted email constraint %q: %w", email, err) - } - normalizedEmailAddresses[i] = normalizedEmailAddress - } - e.permittedEmailAddresses = append(e.permittedEmailAddresses, normalizedEmailAddresses...) - return nil - } -} - func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddresses := make([]string, len(emailAddresses)) @@ -393,21 +235,6 @@ func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { } } -func AddExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedEmailAddresses := make([]string, len(emailAddresses)) - for i, email := range emailAddresses { - normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email) - if err != nil { - return fmt.Errorf("cannot parse excluded email constraint %q: %w", email, err) - } - normalizedEmailAddresses[i] = normalizedEmailAddress - } - e.excludedEmailAddresses = append(e.excludedEmailAddresses, normalizedEmailAddresses...) - return nil - } -} - func WithPermittedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) @@ -419,17 +246,6 @@ func WithPermittedEmailAddress(emailAddress string) NamePolicyOption { } } -func AddPermittedEmailAddress(emailAddress string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) - if err != nil { - return fmt.Errorf("cannot parse permitted email constraint %q: %w", emailAddress, err) - } - e.permittedEmailAddresses = append(e.permittedEmailAddresses, normalizedEmailAddress) - return nil - } -} - func WithExcludedEmailAddress(emailAddress string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) @@ -441,17 +257,6 @@ func WithExcludedEmailAddress(emailAddress string) NamePolicyOption { } } -func AddExcludedEmailAddress(emailAddress string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) - if err != nil { - return fmt.Errorf("cannot parse excluded email constraint %q: %w", emailAddress, err) - } - e.excludedEmailAddresses = append(e.excludedEmailAddresses, normalizedEmailAddress) - return nil - } -} - func WithPermittedURIDomains(uriDomains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedURIDomains := make([]string, len(uriDomains)) @@ -467,21 +272,6 @@ func WithPermittedURIDomains(uriDomains []string) NamePolicyOption { } } -func AddPermittedURIDomains(uriDomains []string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedURIDomains := make([]string, len(uriDomains)) - for i, domain := range uriDomains { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse permitted URI domain constraint %q: %w", domain, err) - } - normalizedURIDomains[i] = normalizedURIDomain - } - e.permittedURIDomains = append(e.permittedURIDomains, normalizedURIDomains...) - return nil - } -} - func WithPermittedURIDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) @@ -493,17 +283,6 @@ func WithPermittedURIDomain(domain string) NamePolicyOption { } } -func AddPermittedURIDomain(domain string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse permitted URI domain constraint %q: %w", domain, err) - } - e.permittedURIDomains = append(e.permittedURIDomains, normalizedURIDomain) - return nil - } -} - func WithExcludedURIDomains(domains []string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedURIDomains := make([]string, len(domains)) @@ -519,21 +298,6 @@ func WithExcludedURIDomains(domains []string) NamePolicyOption { } } -func AddExcludedURIDomains(domains []string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedURIDomains := make([]string, len(domains)) - for i, domain := range domains { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse excluded URI domain constraint %q: %w", domain, err) - } - normalizedURIDomains[i] = normalizedURIDomain - } - e.excludedURIDomains = append(e.excludedURIDomains, normalizedURIDomains...) - return nil - } -} - func WithExcludedURIDomain(domain string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) @@ -545,17 +309,6 @@ func WithExcludedURIDomain(domain string) NamePolicyOption { } } -func AddExcludedURIDomain(domain string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse excluded URI domain constraint %q: %w", domain, err) - } - e.excludedURIDomains = append(e.excludedURIDomains, normalizedURIDomain) - return nil - } -} - func WithPermittedPrincipals(principals []string) NamePolicyOption { return func(g *NamePolicyEngine) error { // TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do. diff --git a/policy/options_test.go b/policy/options_test.go index a1c48e1f..78df3b7b 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -206,15 +206,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-permitted-dns-domains": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddPermittedDNSDomains([]string{"**.local"}), - }, - want: nil, - wantErr: true, - } - }, "fail/with-excluded-dns-domains": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -224,15 +215,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-excluded-dns-domains": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddExcludedDNSDomains([]string{"**.local"}), - }, - want: nil, - wantErr: true, - } - }, "fail/with-permitted-dns-domain": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -242,15 +224,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-permitted-dns-domain": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddPermittedDNSDomain("**.local"), - }, - want: nil, - wantErr: true, - } - }, "fail/with-excluded-dns-domain": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -260,15 +233,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-excluded-dns-domain": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddExcludedDNSDomain("**.local"), - }, - want: nil, - wantErr: true, - } - }, "fail/with-permitted-cidrs": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -278,15 +242,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-permitted-cidrs": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddPermittedCIDRs([]string{"127.0.0.1//24"}), - }, - want: nil, - wantErr: true, - } - }, "fail/with-excluded-cidrs": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -296,15 +251,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-excluded-cidrs": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddExcludedCIDRs([]string{"127.0.0.1//24"}), - }, - want: nil, - wantErr: true, - } - }, "fail/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -350,15 +296,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-permitted-cidr": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddPermittedCIDR("127.0.0.1//24"), - }, - want: nil, - wantErr: true, - } - }, "fail/with-excluded-cidr": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -368,15 +305,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-excluded-cidr": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddExcludedCIDR("127.0.0.1//24"), - }, - want: nil, - wantErr: true, - } - }, "fail/with-permitted-emails": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -386,15 +314,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-permitted-emails": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddPermittedEmailAddresses([]string{"*.local"}), - }, - want: nil, - wantErr: true, - } - }, "fail/with-excluded-emails": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -404,15 +323,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-excluded-emails": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddExcludedEmailAddresses([]string{"*.local"}), - }, - want: nil, - wantErr: true, - } - }, "fail/with-permitted-email": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -422,15 +332,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-permitted-email": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddPermittedEmailAddress("*.local"), - }, - want: nil, - wantErr: true, - } - }, "fail/with-excluded-email": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -440,15 +341,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-excluded-email": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddExcludedEmailAddress("*.local"), - }, - want: nil, - wantErr: true, - } - }, "fail/with-permitted-uris": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -458,15 +350,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-permitted-uris": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddPermittedURIDomains([]string{"**.local"}), - }, - want: nil, - wantErr: true, - } - }, "fail/with-excluded-uris": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -476,15 +359,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-excluded-uris": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddExcludedURIDomains([]string{"**.local"}), - }, - want: nil, - wantErr: true, - } - }, "fail/with-permitted-uri": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -494,15 +368,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-permitted-uri": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddPermittedURIDomain("**.local"), - }, - want: nil, - wantErr: true, - } - }, "fail/with-excluded-uri": func(t *testing.T) test { return test{ options: []NamePolicyOption{ @@ -512,15 +377,6 @@ func TestNew(t *testing.T) { wantErr: true, } }, - "fail/add-excluded-uri": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - AddExcludedURIDomain("**.local"), - }, - want: nil, - wantErr: true, - } - }, "ok/default": func(t *testing.T) test { return test{ options: []NamePolicyOption{}, @@ -567,22 +423,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-dns-wildcard-domains": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedDNSDomains([]string{"*.local"}), - AddPermittedDNSDomains([]string{"*.example.com", "*.local"}), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedDNSDomains: []string{".local", ".example.com"}, - numberOfDNSDomainConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-dns-domains": func(t *testing.T) test { options := []NamePolicyOption{ WithExcludedDNSDomains([]string{"*.local", "*.example.com"}), @@ -598,22 +438,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-dns-domains": func(t *testing.T) test { - options := []NamePolicyOption{ - WithExcludedDNSDomains([]string{"*.local"}), - AddExcludedDNSDomains([]string{"*.local", "*.example.com"}), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedDNSDomains: []string{".local", ".example.com"}, - numberOfDNSDomainConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-dns-wildcard-domain": func(t *testing.T) test { options := []NamePolicyOption{ WithPermittedDNSDomain("*.example.com"), @@ -629,22 +453,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-dns-wildcard-domain": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedDNSDomain("*.example.com"), - AddPermittedDNSDomain("*.local"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedDNSDomains: []string{".example.com", ".local"}, - numberOfDNSDomainConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-dns-domain": func(t *testing.T) test { options := []NamePolicyOption{ WithPermittedDNSDomain("www.example.com"), @@ -660,22 +468,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-dns-domain": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedDNSDomain("www.example.com"), - AddPermittedDNSDomain("host.local"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedDNSDomains: []string{"www.example.com", "host.local"}, - numberOfDNSDomainConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-ip-ranges": func(t *testing.T) test { _, nw1, err := net.ParseCIDR("127.0.0.1/24") assert.FatalError(t, err) @@ -701,36 +493,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-ip-ranges": func(t *testing.T) test { - _, nw1, err := net.ParseCIDR("127.0.0.1/24") - assert.FatalError(t, err) - _, nw2, err := net.ParseCIDR("192.168.0.1/24") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithPermittedIPRanges( - []*net.IPNet{ - nw1, - }, - ), - AddPermittedIPRanges( - []*net.IPNet{ - nw1, nw2, - }, - ), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-ip-ranges": func(t *testing.T) test { _, nw1, err := net.ParseCIDR("127.0.0.1/24") assert.FatalError(t, err) @@ -756,36 +518,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-ip-ranges": func(t *testing.T) test { - _, nw1, err := net.ParseCIDR("127.0.0.1/24") - assert.FatalError(t, err) - _, nw2, err := net.ParseCIDR("192.168.0.1/24") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithExcludedIPRanges( - []*net.IPNet{ - nw1, - }, - ), - AddExcludedIPRanges( - []*net.IPNet{ - nw1, nw2, - }, - ), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-cidrs": func(t *testing.T) test { _, nw1, err := net.ParseCIDR("127.0.0.1/24") assert.FatalError(t, err) @@ -807,28 +539,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-cidrs": func(t *testing.T) test { - _, nw1, err := net.ParseCIDR("127.0.0.1/24") - assert.FatalError(t, err) - _, nw2, err := net.ParseCIDR("192.168.0.1/24") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithPermittedCIDRs([]string{"127.0.0.1/24"}), - AddPermittedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-cidrs": func(t *testing.T) test { _, nw1, err := net.ParseCIDR("127.0.0.1/24") assert.FatalError(t, err) @@ -850,28 +560,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-cidrs": func(t *testing.T) test { - _, nw1, err := net.ParseCIDR("127.0.0.1/24") - assert.FatalError(t, err) - _, nw2, err := net.ParseCIDR("192.168.0.1/24") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithExcludedCIDRs([]string{"127.0.0.1/24"}), - AddExcludedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test { _, nw1, err := net.ParseCIDR("127.0.0.1/24") assert.FatalError(t, err) @@ -933,28 +621,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-cidr": func(t *testing.T) test { - _, nw1, err := net.ParseCIDR("127.0.0.1/24") - assert.FatalError(t, err) - _, nw2, err := net.ParseCIDR("192.168.0.1/24") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithPermittedCIDR("127.0.0.1/24"), - AddPermittedCIDR("192.168.0.1/24"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-cidr": func(t *testing.T) test { _, nw1, err := net.ParseCIDR("127.0.0.1/24") assert.FatalError(t, err) @@ -974,28 +640,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-cidr": func(t *testing.T) test { - _, nw1, err := net.ParseCIDR("127.0.0.1/24") - assert.FatalError(t, err) - _, nw2, err := net.ParseCIDR("192.168.0.1/24") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithExcludedCIDR("127.0.0.1/24"), - AddExcludedCIDR("192.168.0.1/24"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-ipv4": func(t *testing.T) test { ip1, nw1, err := net.ParseCIDR("127.0.0.15/32") assert.FatalError(t, err) @@ -1015,28 +659,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-ipv4": func(t *testing.T) test { - ip1, nw1, err := net.ParseCIDR("127.0.0.45/32") - assert.FatalError(t, err) - ip2, nw2, err := net.ParseCIDR("192.168.0.55/32") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithPermittedIP(ip1), - AddPermittedIP(ip2), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-ipv4": func(t *testing.T) test { ip1, nw1, err := net.ParseCIDR("127.0.0.15/32") assert.FatalError(t, err) @@ -1056,28 +678,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-ipv4": func(t *testing.T) test { - ip1, nw1, err := net.ParseCIDR("127.0.0.45/32") - assert.FatalError(t, err) - ip2, nw2, err := net.ParseCIDR("192.168.0.55/32") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithExcludedIP(ip1), - AddExcludedIP(ip2), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-ipv6": func(t *testing.T) test { ip1, nw1, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") assert.FatalError(t, err) @@ -1097,28 +697,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-ipv6": func(t *testing.T) test { - ip1, nw1, err := net.ParseCIDR("127.0.0.10/32") - assert.FatalError(t, err) - ip2, nw2, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithPermittedIP(ip1), - AddPermittedIP(ip2), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-ipv6": func(t *testing.T) test { ip1, nw1, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") assert.FatalError(t, err) @@ -1138,28 +716,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-ipv6": func(t *testing.T) test { - ip1, nw1, err := net.ParseCIDR("127.0.0.10/32") - assert.FatalError(t, err) - ip2, nw2, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithExcludedIP(ip1), - AddExcludedIP(ip2), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedIPRanges: []*net.IPNet{ - nw1, nw2, - }, - numberOfIPRangeConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-emails": func(t *testing.T) test { options := []NamePolicyOption{ WithPermittedEmailAddresses([]string{"mail@local", "@example.com"}), @@ -1175,22 +731,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-emails": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedEmailAddresses([]string{"mail@local"}), - AddPermittedEmailAddresses([]string{"@example.com", "mail@local"}), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedEmailAddresses: []string{"mail@local", "example.com"}, - numberOfEmailAddressConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-emails": func(t *testing.T) test { options := []NamePolicyOption{ WithExcludedEmailAddresses([]string{"mail@local", "@example.com"}), @@ -1206,22 +746,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-emails": func(t *testing.T) test { - options := []NamePolicyOption{ - WithExcludedEmailAddresses([]string{"mail@local"}), - AddExcludedEmailAddresses([]string{"@example.com", "mail@local"}), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedEmailAddresses: []string{"mail@local", "example.com"}, - numberOfEmailAddressConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-email": func(t *testing.T) test { options := []NamePolicyOption{ WithPermittedEmailAddress("mail@local"), @@ -1237,22 +761,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-email": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedEmailAddress("mail@local"), - AddPermittedEmailAddress("@example.com"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedEmailAddresses: []string{"mail@local", "example.com"}, - numberOfEmailAddressConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-email": func(t *testing.T) test { options := []NamePolicyOption{ WithExcludedEmailAddress("mail@local"), @@ -1268,22 +776,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-email": func(t *testing.T) test { - options := []NamePolicyOption{ - WithExcludedEmailAddress("mail@local"), - AddExcludedEmailAddress("@example.com"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedEmailAddresses: []string{"mail@local", "example.com"}, - numberOfEmailAddressConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-uris": func(t *testing.T) test { options := []NamePolicyOption{ WithPermittedURIDomains([]string{"host.local", "*.example.com"}), @@ -1299,22 +791,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-uris": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedURIDomains([]string{"host.local"}), - AddPermittedURIDomains([]string{"*.example.com", "host.local"}), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedURIDomains: []string{"host.local", ".example.com"}, - numberOfURIDomainConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-uris": func(t *testing.T) test { options := []NamePolicyOption{ WithExcludedURIDomains([]string{"host.local", "*.example.com"}), @@ -1330,22 +806,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-uris": func(t *testing.T) test { - options := []NamePolicyOption{ - WithExcludedURIDomains([]string{"host.local"}), - AddExcludedURIDomains([]string{"*.example.com", "host.local"}), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedURIDomains: []string{"host.local", ".example.com"}, - numberOfURIDomainConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-uri": func(t *testing.T) test { options := []NamePolicyOption{ WithPermittedURIDomain("host.local"), @@ -1376,22 +836,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-permitted-uri": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedURIDomain("host.local"), - AddPermittedURIDomain("*.example.com"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedURIDomains: []string{"host.local", ".example.com"}, - numberOfURIDomainConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-excluded-uri": func(t *testing.T) test { options := []NamePolicyOption{ WithExcludedURIDomain("host.local"), @@ -1407,22 +851,6 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/add-excluded-uri": func(t *testing.T) test { - options := []NamePolicyOption{ - WithExcludedURIDomain("host.local"), - AddExcludedURIDomain("*.example.com"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedURIDomains: []string{"host.local", ".example.com"}, - numberOfURIDomainConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, "ok/with-permitted-principals": func(t *testing.T) test { options := []NamePolicyOption{ WithPermittedPrincipals([]string{"root", "ops"}), From ff8cb19b78db97674a130044fb46c6a864953587 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 18 Apr 2022 21:59:06 +0200 Subject: [PATCH 39/78] Fix usage of URL in generateAdminToken --- ca/adminClient.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ca/adminClient.go b/ca/adminClient.go index ea40d1c4..1c98e662 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -684,7 +684,7 @@ retry: func (c *AdminClient) GetAuthorityPolicy() (*linkedca.Policy, error) { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -719,7 +719,7 @@ func (c *AdminClient) CreateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Polic return nil, fmt.Errorf("error marshaling request: %w", err) } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -754,7 +754,7 @@ func (c *AdminClient) UpdateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Polic return nil, fmt.Errorf("error marshaling request: %w", err) } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -785,7 +785,7 @@ retry: func (c *AdminClient) RemoveAuthorityPolicy() error { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return fmt.Errorf("error generating admin token: %w", err) } @@ -812,7 +812,7 @@ retry: func (c *AdminClient) GetProvisionerPolicy(provisionerName string) (*linkedca.Policy, error) { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -847,7 +847,7 @@ func (c *AdminClient) CreateProvisionerPolicy(provisionerName string, p *linkedc return nil, fmt.Errorf("error marshaling request: %w", err) } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -882,7 +882,7 @@ func (c *AdminClient) UpdateProvisionerPolicy(provisionerName string, p *linkedc return nil, fmt.Errorf("error marshaling request: %w", err) } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -913,7 +913,7 @@ retry: func (c *AdminClient) RemoveProvisionerPolicy(provisionerName string) error { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioner", provisionerName, "policy")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return fmt.Errorf("error generating admin token: %w", err) } @@ -947,7 +947,7 @@ func (c *AdminClient) GetACMEPolicy(provisionerName, reference, keyID string) (* urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference) } u := c.endpoint.ResolveReference(&url.URL{Path: urlPath}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -989,7 +989,7 @@ func (c *AdminClient) CreateACMEPolicy(provisionerName, reference, keyID string, urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference) } u := c.endpoint.ResolveReference(&url.URL{Path: urlPath}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -1031,7 +1031,7 @@ func (c *AdminClient) UpdateACMEPolicy(provisionerName, reference, keyID string, urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference) } u := c.endpoint.ResolveReference(&url.URL{Path: urlPath}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) } @@ -1069,7 +1069,7 @@ func (c *AdminClient) RemoveACMEPolicy(provisionerName, reference, keyID string) urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference) } u := c.endpoint.ResolveReference(&url.URL{Path: urlPath}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return fmt.Errorf("error generating admin token: %w", err) } From 2ca5c0170f78ca330e58f2b0af1670a0e49cca5b Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 18 Apr 2022 22:39:47 +0200 Subject: [PATCH 40/78] Fix flaky test behavior for protobuf messages --- authority/admin/api/policy_test.go | 67 +++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index ab09c5bd..5717e73a 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/http/httptest" + "strings" "testing" "google.golang.org/protobuf/encoding/protojson" @@ -342,10 +343,19 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, ae.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + } else { + assert.Equals(t, tc.err.Message, ae.Message) + } + return } @@ -583,10 +593,19 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, ae.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + } else { + assert.Equals(t, tc.err.Message, ae.Message) + } + return } @@ -994,10 +1013,19 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, ae.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + } else { + assert.Equals(t, tc.err.Message, ae.Message) + } + return } @@ -1185,10 +1213,19 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, ae.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + } else { + assert.Equals(t, tc.err.Message, ae.Message) + } + return } @@ -1549,10 +1586,19 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, ae.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + } else { + assert.Equals(t, tc.err.Message, ae.Message) + } + return } @@ -1715,10 +1761,19 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, ae.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + } else { + assert.Equals(t, tc.err.Message, ae.Message) + } + return } From def9438ad62e2c71975e2ac1a4f96b6f5ec663a5 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 18 Apr 2022 23:38:13 +0200 Subject: [PATCH 41/78] Improve handling of bad JSON protobuf bodies --- api/read/read.go | 88 +++++++++++-------------- authority/admin/api/policy.go | 18 +++-- authority/admin/api/policy_test.go | 12 ++-- authority/admin/api/provisioner_test.go | 49 +++++++++----- 4 files changed, 90 insertions(+), 77 deletions(-) diff --git a/api/read/read.go b/api/read/read.go index 2f5175d9..7482c272 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -10,7 +10,6 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/errs" ) @@ -24,62 +23,55 @@ func JSON(r io.Reader, v interface{}) error { } // ProtoJSON reads JSON from the request body and stores it in the value -// pointed by v. +// pointed to by v. func ProtoJSON(r io.Reader, m proto.Message) error { data, err := io.ReadAll(r) if err != nil { return errs.BadRequestErr(err, "error reading request body") } - return protojson.Unmarshal(data, m) -} - -// ProtoJSONWithCheck reads JSON from the request body and stores it in the value -// pointed to by m. Returns false if an error was written; true if not. -// TODO(hs): refactor this after the API flow changes are in (or before if that works) -func ProtoJSONWithCheck(w http.ResponseWriter, r io.Reader, m proto.Message) bool { - data, err := io.ReadAll(r) - if err != nil { - var wrapper = struct { - Status int `json:"code"` - Message string `json:"message"` - }{ - Status: http.StatusBadRequest, - Message: err.Error(), - } - errData, err := json.Marshal(wrapper) - if err != nil { - panic(err) - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - w.Write(errData) - return false - } if err := protojson.Unmarshal(data, m); err != nil { if errors.Is(err, proto.Error) { - var wrapper = struct { - Type string `json:"type"` - Detail string `json:"detail"` - Message string `json:"message"` - }{ - Type: "badRequest", - Detail: "bad request", - Message: err.Error(), - } - errData, err := json.Marshal(wrapper) - if err != nil { - panic(err) - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - w.Write(errData) - return false + return newBadProtoJSONError(err) } + } + return err +} - // fallback to the default error writer - render.Error(w, err) - return false +// BadProtoJSONError is an error type that is used when a proto +// message cannot be unmarshaled. Usually this is caused by an error +// in the request body. +type BadProtoJSONError struct { + err error + Type string `json:"type"` + Detail string `json:"detail"` + Message string `json:"message"` +} + +// newBadProtoJSONError returns a new instance of BadProtoJSONError +// This error type is always caused by an error in the request body. +func newBadProtoJSONError(err error) *BadProtoJSONError { + return &BadProtoJSONError{ + err: err, + Type: "badRequest", + Detail: "bad request", + Message: err.Error(), + } +} + +// Error implements the error interface +func (e *BadProtoJSONError) Error() string { + return e.err.Error() +} + +// Render implements render.RenderableError for BadProtoError +func (e *BadProtoJSONError) Render(w http.ResponseWriter) { + + errData, err := json.Marshal(e) + if err != nil { + panic(err) } - return true + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + w.Write(errData) } diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 17bc454c..b7c7855f 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -80,7 +80,8 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r } var newPolicy = new(linkedca.Policy) - if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { + if err := read.ProtoJSON(r.Body, newPolicy); err != nil { + render.Error(w, err) return } @@ -120,7 +121,8 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r } var newPolicy = new(linkedca.Policy) - if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { + if err := read.ProtoJSON(r.Body, newPolicy); err != nil { + render.Error(w, err) return } @@ -195,7 +197,8 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, } var newPolicy = new(linkedca.Policy) - if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { + if err := read.ProtoJSON(r.Body, newPolicy); err != nil { + render.Error(w, err) return } @@ -228,7 +231,8 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, } var newPolicy = new(linkedca.Policy) - if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { + if err := read.ProtoJSON(r.Body, newPolicy); err != nil { + render.Error(w, err) return } @@ -297,7 +301,8 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, } var newPolicy = new(linkedca.Policy) - if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { + if err := read.ProtoJSON(r.Body, newPolicy); err != nil { + render.Error(w, err) return } @@ -324,7 +329,8 @@ func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, } var newPolicy = new(linkedca.Policy) - if !read.ProtoJSONWithCheck(w, r.Body, newPolicy) { + if err := read.ProtoJSON(r.Body, newPolicy); err != nil { + render.Error(w, err) return } diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index 5717e73a..cc4f64fb 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -167,7 +167,7 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { statusCode: 409, } }, - "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + "fail/read.ProtoJSON": func(t *testing.T) test { ctx := context.Background() adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" @@ -410,7 +410,7 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { statusCode: 404, } }, - "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + "fail/read.ProtoJSON": func(t *testing.T) test { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ @@ -871,7 +871,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { statusCode: 409, } }, - "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + "fail/read.ProtoJSON": func(t *testing.T) test { prov := &linkedca.Provisioner{ Name: "provName", } @@ -1060,7 +1060,7 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { statusCode: 404, } }, - "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + "fail/read.ProtoJSON": func(t *testing.T) test { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ @@ -1472,7 +1472,7 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { statusCode: 409, } }, - "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + "fail/read.ProtoJSON": func(t *testing.T) test { prov := &linkedca.Provisioner{ Name: "provName", } @@ -1637,7 +1637,7 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { statusCode: 404, } }, - "fail/read.ProtoJSONWithCheck": func(t *testing.T) test { + "fail/read.ProtoJSON": func(t *testing.T) test { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ diff --git a/authority/admin/api/provisioner_test.go b/authority/admin/api/provisioner_test.go index 6d5024f2..de7c3646 100644 --- a/authority/admin/api/provisioner_test.go +++ b/authority/admin/api/provisioner_test.go @@ -8,18 +8,21 @@ import ( "io" "net/http" "net/http/httptest" + "strings" "testing" "time" "github.com/go-chi/chi" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" + + "go.step.sm/linkedca" + "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/linkedca" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/types/known/timestamppb" ) func TestHandler_GetProvisioner(t *testing.T) { @@ -335,12 +338,12 @@ func TestHandler_CreateProvisioner(t *testing.T) { return test{ ctx: context.Background(), body: body, - statusCode: 500, - err: &admin.Error{ // TODO(hs): this probably needs a better error - Type: "", - Status: 500, - Detail: "", - Message: "", + statusCode: 400, + err: &admin.Error{ + Type: "badRequest", + Status: 400, + Detail: "bad request", + Message: "proto: syntax error (line 1:2): invalid value !", }, } }, @@ -423,9 +426,15 @@ func TestHandler_CreateProvisioner(t *testing.T) { assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) assert.Equals(t, tc.err.Type, adminErr.Type) - assert.Equals(t, tc.err.Message, adminErr.Message) assert.Equals(t, tc.err.Detail, adminErr.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + } else { + assert.Equals(t, tc.err.Message, adminErr.Message) + } + return } @@ -616,12 +625,12 @@ func TestHandler_UpdateProvisioner(t *testing.T) { return test{ ctx: context.Background(), body: body, - statusCode: 500, - err: &admin.Error{ // TODO(hs): this probably needs a better error - Type: "", - Status: 500, - Detail: "", - Message: "", + statusCode: 400, + err: &admin.Error{ + Type: "badRequest", + Status: 400, + Detail: "bad request", + Message: "proto: syntax error (line 1:2): invalid value !", }, } }, @@ -1074,9 +1083,15 @@ func TestHandler_UpdateProvisioner(t *testing.T) { assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) assert.Equals(t, tc.err.Type, adminErr.Type) - assert.Equals(t, tc.err.Message, adminErr.Message) assert.Equals(t, tc.err.Detail, adminErr.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + } else { + assert.Equals(t, tc.err.Message, adminErr.Message) + } + return } From 7f9034d22aff899bc433e5ceb00b7f25fc441567 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 10:24:52 +0200 Subject: [PATCH 42/78] Add additional policy options --- acme/account.go | 14 +++++++ authority/admin/api/policy.go | 18 +++++++++ authority/policy/options.go | 66 ++++++++++++++++++++++---------- authority/policy/policy.go | 9 ++++- authority/provisioner/options.go | 25 ++++++++++++ go.mod | 3 +- 6 files changed, 112 insertions(+), 23 deletions(-) diff --git a/acme/account.go b/acme/account.go index 51225b49..18c0d646 100644 --- a/acme/account.go +++ b/acme/account.go @@ -81,6 +81,20 @@ func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions { } } +// IsWildcardLiteralAllowed returns true by default for +// ACME account policies, as authorization is performed on DNS +// level. +func (p *Policy) IsWildcardLiteralAllowed() bool { + return true +} + +// ShouldVerifySubjectCommonName returns true by default +// for ACME account policies, as this is embedded in the +// protocol. +func (p *Policy) ShouldVerifySubjectCommonName() bool { + return true +} + // ExternalAccountKey is an ACME External Account Binding key. type ExternalAccountKey struct { ID string `json:"id"` diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index b7c7855f..d294060c 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -2,9 +2,11 @@ package api import ( "errors" + "fmt" "net/http" "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/wrapperspb" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api/read" @@ -85,6 +87,10 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r return } + fmt.Println("before: ", newPolicy) + applyDefaults(newPolicy) + fmt.Println("after: ", newPolicy) + adm := linkedca.AdminFromContext(ctx) var createdPolicy *linkedca.Policy @@ -202,6 +208,8 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, return } + applyDefaults(newPolicy) + prov.Policy = newPolicy if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { @@ -366,3 +374,13 @@ func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) } + +func applyDefaults(p *linkedca.Policy) { + if p.GetX509() == nil { + return + } + if p.GetX509().VerifySubjectCommonName == nil { + p.X509.VerifySubjectCommonName = &wrapperspb.BoolValue{Value: true} + } + return +} diff --git a/authority/policy/options.go b/authority/policy/options.go index e1c33104..c3b30c0a 100644 --- a/authority/policy/options.go +++ b/authority/policy/options.go @@ -30,6 +30,8 @@ func (o *Options) GetSSHOptions() *SSHPolicyOptions { type X509PolicyOptionsInterface interface { GetAllowedNameOptions() *X509NameOptions GetDeniedNameOptions() *X509NameOptions + IsWildcardLiteralAllowed() bool + ShouldVerifySubjectCommonName() bool } // X509PolicyOptions is a container for x509 allowed and denied @@ -39,6 +41,13 @@ type X509PolicyOptions struct { AllowedNames *X509NameOptions `json:"allow,omitempty"` // DeniedNames contains the x509 denied names DeniedNames *X509NameOptions `json:"deny,omitempty"` + // AllowWildcardLiteral indicates if literal wildcard names + // such as *.example.com and @example.com are allowed. Defaults + // to false. + AllowWildcardLiteral *bool `json:"allow_wildcard_literal,omitempty"` + // VerifySubjectCommonName indicates if the Subject Common Name + // is verified in addition to the SANs. Defaults to true. + VerifySubjectCommonName *bool `json:"verify_subject_common_name,omitempty"` } // X509NameOptions models the X509 name policy configuration. @@ -58,6 +67,43 @@ func (o *X509NameOptions) HasNames() bool { len(o.URIDomains) > 0 } +// GetDeniedNameOptions returns the x509 denied name policy configuration +func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { + if o == nil { + return nil + } + return o.DeniedNames +} + +// GetAllowedUserNameOptions returns the SSH allowed user name policy +// configuration. +func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.AllowedNames +} + +func (o *X509PolicyOptions) IsWildcardLiteralAllowed() bool { + if o == nil { + return true + } + return o.AllowWildcardLiteral != nil && *o.AllowWildcardLiteral +} + +func (o *X509PolicyOptions) ShouldVerifySubjectCommonName() bool { + if o == nil { + return false + } + if o.VerifySubjectCommonName == nil { + return true + } + return *o.VerifySubjectCommonName +} + // SSHPolicyOptionsInterface is an interface for providers of // SSH user and host name policy configuration. type SSHPolicyOptionsInterface interface { @@ -84,26 +130,6 @@ func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions { return o.AllowedNames } -// GetDeniedNameOptions returns the x509 denied name policy configuration -func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { - if o == nil { - return nil - } - return o.DeniedNames -} - -// GetAllowedUserNameOptions returns the SSH allowed user name policy -// configuration. -func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - if o.User == nil { - return nil - } - return o.User.AllowedNames -} - // GetDeniedUserNameOptions returns the SSH denied user name policy // configuration. func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions { diff --git a/authority/policy/policy.go b/authority/policy/policy.go index 403ac0b7..f1142ea7 100644 --- a/authority/policy/policy.go +++ b/authority/policy/policy.go @@ -50,8 +50,13 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, return nil, nil } - // enable x509 Subject Common Name validation by default - options = append(options, policy.WithSubjectCommonNameVerification()) + if policyOptions.ShouldVerifySubjectCommonName() { + options = append(options, policy.WithSubjectCommonNameVerification()) + } + + if policyOptions.IsWildcardLiteralAllowed() { + options = append(options, policy.WithAllowLiteralWildcardNames()) + } return policy.New(options...) } diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 0975a4c2..12e371a6 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -65,6 +65,14 @@ type X509Options struct { // DeniedNames contains the SANs the provisioner is not authorized to sign DeniedNames *policy.X509NameOptions `json:"-"` + + // AllowWildcardLiteral indicates if literal wildcard names + // such as *.example.com and @example.com are allowed. Defaults + // to false. + AllowWildcardLiteral *bool `json:"-"` + // VerifySubjectCommonName indicates if the Subject Common Name + // is verified in addition to the SANs. Defaults to true. + VerifySubjectCommonName *bool `json:"-"` } // HasTemplate returns true if a template is defined in the provisioner options. @@ -90,6 +98,23 @@ func (o *X509Options) GetDeniedNameOptions() *policy.X509NameOptions { return o.DeniedNames } +func (o *X509Options) IsWildcardLiteralAllowed() bool { + if o == nil { + return true + } + return o.AllowWildcardLiteral != nil && *o.AllowWildcardLiteral +} + +func (o *X509Options) ShouldVerifySubjectCommonName() bool { + if o == nil { + return false + } + if o.VerifySubjectCommonName == nil { + return true + } + return *o.VerifySubjectCommonName +} + // TemplateOptions generates a CertificateOptions with the template and data // defined in the ProvisionerOptions, the provisioner generated data, and the // user data provided in the request. If no template has been provided, diff --git a/go.mod b/go.mod index c8b8c66c..68e384ca 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/go-kit/kit v0.10.0 // indirect github.com/go-piv/piv-go v1.7.0 github.com/golang/mock v1.6.0 + github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.7 github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.1.1 @@ -52,4 +53,4 @@ require ( // replace github.com/smallstep/nosql => ../nosql // replace go.step.sm/crypto => ../crypto // replace go.step.sm/cli-utils => ../cli-utils -// replace go.step.sm/linkedca => ../linkedca +replace go.step.sm/linkedca => ../linkedca From 6532c933030e33e46c28ddc5018d17f3d3926720 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 12:07:57 +0200 Subject: [PATCH 43/78] Improve read.ProtoJSON bad protobuf body error handling --- api/read/read.go | 61 +++++++++++++++++++----------------------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/api/read/read.go b/api/read/read.go index 7482c272..6cfc90ee 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -10,6 +10,7 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/errs" ) @@ -29,49 +30,35 @@ func ProtoJSON(r io.Reader, m proto.Message) error { if err != nil { return errs.BadRequestErr(err, "error reading request body") } - if err := protojson.Unmarshal(data, m); err != nil { - if errors.Is(err, proto.Error) { - return newBadProtoJSONError(err) - } + + switch err := protojson.Unmarshal(data, m); { + case errors.Is(err, proto.Error): + return badProtoJSONError(err.Error()) + default: + return err } - return err } -// BadProtoJSONError is an error type that is used when a proto -// message cannot be unmarshaled. Usually this is caused by an error -// in the request body. -type BadProtoJSONError struct { - err error - Type string `json:"type"` - Detail string `json:"detail"` - Message string `json:"message"` +// badProtoJSONError is an error type that is returned by ProtoJSON +// when a proto message cannot be unmarshaled. Usually this is caused +// by an error in the request body. +type badProtoJSONError string + +// Error implements error for badProtoJSONError +func (e badProtoJSONError) Error() string { + return string(e) } -// newBadProtoJSONError returns a new instance of BadProtoJSONError -// This error type is always caused by an error in the request body. -func newBadProtoJSONError(err error) *BadProtoJSONError { - return &BadProtoJSONError{ - err: err, +// Render implements render.RenderableError for badProtoJSONError +func (e badProtoJSONError) Render(w http.ResponseWriter) { + v := struct { + Type string `json:"type"` + Detail string `json:"detail"` + Message string `json:"message"` + }{ Type: "badRequest", Detail: "bad request", - Message: err.Error(), + Message: e.Error(), } -} - -// Error implements the error interface -func (e *BadProtoJSONError) Error() string { - return e.err.Error() -} - -// Render implements render.RenderableError for BadProtoError -func (e *BadProtoJSONError) Render(w http.ResponseWriter) { - - errData, err := json.Marshal(e) - if err != nil { - panic(err) - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - w.Write(errData) + render.JSONStatus(w, v, http.StatusBadRequest) } From f2f9cb899edcf9056f12cd483bf301d45c1f26e4 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 12:09:45 +0200 Subject: [PATCH 44/78] Add conditional defaults to policy protobuf request bodies --- authority/admin/api/policy.go | 14 +++--- authority/admin/api/policy_test.go | 81 ++++++++++++++++++++++++++++++ go.mod | 2 +- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index d294060c..b84c18c5 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -2,7 +2,6 @@ package api import ( "errors" - "fmt" "net/http" "go.step.sm/linkedca" @@ -87,9 +86,7 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r return } - fmt.Println("before: ", newPolicy) - applyDefaults(newPolicy) - fmt.Println("after: ", newPolicy) + applyConditionalDefaults(newPolicy) adm := linkedca.AdminFromContext(ctx) @@ -107,7 +104,7 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r return } - render.JSONStatus(w, createdPolicy, http.StatusCreated) + render.ProtoJSONStatus(w, createdPolicy, http.StatusCreated) } // UpdateAuthorityPolicy handles the PUT /admin/authority/policy request @@ -208,7 +205,7 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, return } - applyDefaults(newPolicy) + applyConditionalDefaults(newPolicy) prov.Policy = newPolicy @@ -375,12 +372,13 @@ func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) } -func applyDefaults(p *linkedca.Policy) { +// applyConditionalDefaults applies default settings in case they're not provided +// in the request body. +func applyConditionalDefaults(p *linkedca.Policy) { if p.GetX509() == nil { return } if p.GetX509().VerifySubjectCommonName == nil { p.X509.VerifySubjectCommonName = &wrapperspb.BoolValue{Value: true} } - return } diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index cc4f64fb..41fe05ae 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -12,6 +12,7 @@ import ( "testing" "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/wrapperspb" "go.step.sm/linkedca" @@ -1920,3 +1921,83 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { }) } } + +func Test_applyConditionalDefaults(t *testing.T) { + tests := []struct { + name string + policy *linkedca.Policy + expected *linkedca.Policy + }{ + { + name: "no-x509", + policy: &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{}, + }, + expected: &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{}, + }, + }, + { + name: "with-x509-verify-subject-common-name", + policy: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + }, + }, + expected: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + }, + }, + }, + { + name: "without-x509-verify-subject-common-name", + policy: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: false}, + }, + }, + expected: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: false}, + }, + }, + }, + { + name: "no-x509-verify-subject-common-name", + policy: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + }, + expected: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + applyConditionalDefaults(tt.policy) + assert.Equals(t, tt.expected, tt.policy) + }) + } +} diff --git a/go.mod b/go.mod index ed1d26bf..104538a3 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/go-kit/kit v0.10.0 // indirect github.com/go-piv/piv-go v1.7.0 github.com/golang/mock v1.6.0 - github.com/golang/protobuf v1.5.2 + github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.7 github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.1.1 From 9a21208f22b3f2c655fd8285418ecdadfdee6857 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 13:21:37 +0200 Subject: [PATCH 45/78] Add deduplication of policy configuration values --- authority/admin/api/policy.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index b84c18c5..d04147b3 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -88,6 +88,8 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r applyConditionalDefaults(newPolicy) + newPolicy.Deduplicate() + adm := linkedca.AdminFromContext(ctx) var createdPolicy *linkedca.Policy @@ -129,6 +131,8 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r return } + newPolicy.Deduplicate() + adm := linkedca.AdminFromContext(ctx) var updatedPolicy *linkedca.Policy @@ -207,6 +211,8 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, applyConditionalDefaults(newPolicy) + newPolicy.Deduplicate() + prov.Policy = newPolicy if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { @@ -241,6 +247,8 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, return } + newPolicy.Deduplicate() + prov.Policy = newPolicy if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { var pe *authority.PolicyError @@ -311,6 +319,8 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, return } + newPolicy.Deduplicate() + eak.Policy = newPolicy acmeEAK := linkedEAKToCertificates(eak) @@ -339,6 +349,8 @@ func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, return } + newPolicy.Deduplicate() + eak.Policy = newPolicy acmeEAK := linkedEAKToCertificates(eak) if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { From 72bbe533763be336153d5be970e5f0cec6a70d19 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 14:41:36 +0200 Subject: [PATCH 46/78] Add additional policy options --- authority/admin/api/policy.go | 2 +- authority/admin/api/policy_test.go | 2 + authority/policy.go | 135 ++++++++++++++------------ authority/policy/options_test.go | 93 ++++++++++++++++++ authority/policy_test.go | 35 ++++++- authority/provisioner/options_test.go | 88 +++++++++++++++++ 6 files changed, 291 insertions(+), 64 deletions(-) create mode 100644 authority/policy/options_test.go diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index d04147b3..fc6ab1d9 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -390,7 +390,7 @@ func applyConditionalDefaults(p *linkedca.Policy) { if p.GetX509() == nil { return } - if p.GetX509().VerifySubjectCommonName == nil { + if p.GetX509().GetVerifySubjectCommonName() == nil { p.X509.VerifySubjectCommonName = &wrapperspb.BoolValue{Value: true} } } diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index 41fe05ae..e3cc6b65 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -286,6 +286,7 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, }, } body, err := protojson.Marshal(policy) @@ -971,6 +972,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, }, } body, err := protojson.Marshal(policy) diff --git a/authority/policy.go b/authority/policy.go index b7d5e4ec..e626eaed 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -243,97 +243,108 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { return nil } - // prepare full policy struct - opts := &authPolicy.Options{ - X509: &authPolicy.X509PolicyOptions{ - AllowedNames: &authPolicy.X509NameOptions{}, - DeniedNames: &authPolicy.X509NameOptions{}, - }, - SSH: &authPolicy.SSHPolicyOptions{ - Host: &authPolicy.SSHHostCertificateOptions{ - AllowedNames: &authPolicy.SSHNameOptions{}, - DeniedNames: &authPolicy.SSHNameOptions{}, - }, - User: &authPolicy.SSHUserCertificateOptions{ - AllowedNames: &authPolicy.SSHNameOptions{}, - DeniedNames: &authPolicy.SSHNameOptions{}, - }, - }, + // return early if x509 nor SSH is set + if p.GetX509() == nil && p.GetSsh() == nil { + return nil } + opts := &authPolicy.Options{} + // fill x509 policy configuration - if p.X509 != nil { - if p.X509.Allow != nil { - if p.X509.Allow.Dns != nil { - opts.X509.AllowedNames.DNSDomains = p.X509.Allow.Dns + if p.GetX509() != nil { + opts.X509 = &authPolicy.X509PolicyOptions{} + if p.GetX509().GetAllow() != nil { + opts.X509.AllowedNames = &authPolicy.X509NameOptions{} + allow := p.GetX509().GetAllow() + if allow.Dns != nil { + opts.X509.AllowedNames.DNSDomains = allow.Dns } - if p.X509.Allow.Ips != nil { - opts.X509.AllowedNames.IPRanges = p.X509.Allow.Ips + if allow.Ips != nil { + opts.X509.AllowedNames.IPRanges = allow.Ips } - if p.X509.Allow.Emails != nil { - opts.X509.AllowedNames.EmailAddresses = p.X509.Allow.Emails + if allow.Emails != nil { + opts.X509.AllowedNames.EmailAddresses = allow.Emails } - if p.X509.Allow.Uris != nil { - opts.X509.AllowedNames.URIDomains = p.X509.Allow.Uris + if allow.Uris != nil { + opts.X509.AllowedNames.URIDomains = allow.Uris } } - if p.X509.Deny != nil { - if p.X509.Deny.Dns != nil { - opts.X509.DeniedNames.DNSDomains = p.X509.Deny.Dns + if p.GetX509().GetDeny() != nil { + opts.X509.DeniedNames = &authPolicy.X509NameOptions{} + deny := p.GetX509().GetDeny() + if deny.Dns != nil { + opts.X509.DeniedNames.DNSDomains = deny.Dns } - if p.X509.Deny.Ips != nil { - opts.X509.DeniedNames.IPRanges = p.X509.Deny.Ips + if deny.Ips != nil { + opts.X509.DeniedNames.IPRanges = deny.Ips } - if p.X509.Deny.Emails != nil { - opts.X509.DeniedNames.EmailAddresses = p.X509.Deny.Emails + if deny.Emails != nil { + opts.X509.DeniedNames.EmailAddresses = deny.Emails } - if p.X509.Deny.Uris != nil { - opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris + if deny.Uris != nil { + opts.X509.DeniedNames.URIDomains = deny.Uris } } + if p.GetX509().GetAllowWildcardLiteral() != nil { + opts.X509.AllowWildcardLiteral = &p.GetX509().GetAllowWildcardLiteral().Value + } + if p.GetX509().GetVerifySubjectCommonName() != nil { + opts.X509.VerifySubjectCommonName = &p.GetX509().VerifySubjectCommonName.Value + } } // fill ssh policy configuration - if p.Ssh != nil { - if p.Ssh.Host != nil { - if p.Ssh.Host.Allow != nil { - if p.Ssh.Host.Allow.Dns != nil { - opts.SSH.Host.AllowedNames.DNSDomains = p.Ssh.Host.Allow.Dns + if p.GetSsh() != nil { + opts.SSH = &authPolicy.SSHPolicyOptions{} + if p.GetSsh().GetHost() != nil { + opts.SSH.Host = &authPolicy.SSHHostCertificateOptions{} + if p.GetSsh().GetHost().GetAllow() != nil { + opts.SSH.Host.AllowedNames = &authPolicy.SSHNameOptions{} + allow := p.GetSsh().GetHost().GetAllow() + if allow.Dns != nil { + opts.SSH.Host.AllowedNames.DNSDomains = allow.Dns } - if p.Ssh.Host.Allow.Ips != nil { - opts.SSH.Host.AllowedNames.IPRanges = p.Ssh.Host.Allow.Ips + if allow.Ips != nil { + opts.SSH.Host.AllowedNames.IPRanges = allow.Ips } - if p.Ssh.Host.Allow.Principals != nil { - opts.SSH.Host.AllowedNames.Principals = p.Ssh.Host.Allow.Principals + if allow.Principals != nil { + opts.SSH.Host.AllowedNames.Principals = allow.Principals } } - if p.Ssh.Host.Deny != nil { - if p.Ssh.Host.Deny.Dns != nil { - opts.SSH.Host.DeniedNames.DNSDomains = p.Ssh.Host.Deny.Dns + if p.GetSsh().GetHost().GetDeny() != nil { + opts.SSH.Host.DeniedNames = &authPolicy.SSHNameOptions{} + deny := p.GetSsh().GetHost().GetDeny() + if deny.Dns != nil { + opts.SSH.Host.DeniedNames.DNSDomains = deny.Dns } - if p.Ssh.Host.Deny.Ips != nil { - opts.SSH.Host.DeniedNames.IPRanges = p.Ssh.Host.Deny.Ips + if deny.Ips != nil { + opts.SSH.Host.DeniedNames.IPRanges = deny.Ips } - if p.Ssh.Host.Deny.Principals != nil { - opts.SSH.Host.DeniedNames.Principals = p.Ssh.Host.Deny.Principals + if deny.Principals != nil { + opts.SSH.Host.DeniedNames.Principals = deny.Principals } } } - if p.Ssh.User != nil { - if p.Ssh.User.Allow != nil { - if p.Ssh.User.Allow.Emails != nil { - opts.SSH.User.AllowedNames.EmailAddresses = p.Ssh.User.Allow.Emails + if p.GetSsh().GetUser() != nil { + opts.SSH.User = &authPolicy.SSHUserCertificateOptions{} + if p.GetSsh().GetUser().GetAllow() != nil { + opts.SSH.User.AllowedNames = &authPolicy.SSHNameOptions{} + allow := p.GetSsh().GetUser().GetAllow() + if allow.Emails != nil { + opts.SSH.User.AllowedNames.EmailAddresses = allow.Emails } - if p.Ssh.User.Allow.Principals != nil { - opts.SSH.User.AllowedNames.Principals = p.Ssh.User.Allow.Principals + if allow.Principals != nil { + opts.SSH.User.AllowedNames.Principals = allow.Principals } } - if p.Ssh.User.Deny != nil { - if p.Ssh.User.Deny.Emails != nil { - opts.SSH.User.DeniedNames.EmailAddresses = p.Ssh.User.Deny.Emails + if p.GetSsh().GetUser().GetDeny() != nil { + opts.SSH.User.DeniedNames = &authPolicy.SSHNameOptions{} + deny := p.GetSsh().GetUser().GetDeny() + if deny.Emails != nil { + opts.SSH.User.DeniedNames.EmailAddresses = deny.Emails } - if p.Ssh.User.Deny.Principals != nil { - opts.SSH.User.DeniedNames.Principals = p.Ssh.User.Deny.Principals + if deny.Principals != nil { + opts.SSH.User.DeniedNames.Principals = deny.Principals } } } diff --git a/authority/policy/options_test.go b/authority/policy/options_test.go new file mode 100644 index 00000000..49330f08 --- /dev/null +++ b/authority/policy/options_test.go @@ -0,0 +1,93 @@ +package policy + +import ( + "testing" +) + +func TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) { + trueValue := true + falseValue := false + tests := []struct { + name string + options *X509PolicyOptions + want bool + }{ + { + name: "nil-options", + options: nil, + want: true, + }, + { + name: "nil", + options: &X509PolicyOptions{ + AllowWildcardLiteral: nil, + }, + want: false, + }, + { + name: "set-true", + options: &X509PolicyOptions{ + AllowWildcardLiteral: &trueValue, + }, + want: true, + }, + { + name: "set-false", + options: &X509PolicyOptions{ + AllowWildcardLiteral: &falseValue, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.options.IsWildcardLiteralAllowed(); got != tt.want { + t.Errorf("X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestX509PolicyOptions_ShouldVerifySubjectCommonName(t *testing.T) { + trueValue := true + falseValue := false + tests := []struct { + name string + options *X509PolicyOptions + want bool + }{ + { + name: "nil-options", + options: nil, + want: false, + }, + { + name: "nil", + options: &X509PolicyOptions{ + VerifySubjectCommonName: nil, + }, + want: true, + }, + { + name: "set-true", + options: &X509PolicyOptions{ + VerifySubjectCommonName: &trueValue, + }, + want: true, + }, + { + name: "set-false", + options: &X509PolicyOptions{ + VerifySubjectCommonName: &falseValue, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.options.ShouldVerifySubjectCommonName(); got != tt.want { + t.Errorf("X509PolicyOptions.ShouldVerifySubjectCommonName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/policy_test.go b/authority/policy_test.go index 38132a7c..24fe5840 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/wrapperspb" "go.step.sm/linkedca" @@ -190,16 +191,44 @@ func TestAuthority_checkPolicy(t *testing.T) { } func Test_policyToCertificates(t *testing.T) { + trueValue := true + falseValue := false tests := []struct { name string policy *linkedca.Policy want *authPolicy.Options }{ { - name: "no-policy", + name: "nil", policy: nil, want: nil, }, + { + name: "no-policy", + policy: &linkedca.Policy{}, + want: nil, + }, + { + name: "partial-policy", + policy: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + AllowWildcardLiteral: &wrapperspb.BoolValue{Value: false}, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + }, + }, + want: &authPolicy.Options{ + X509: &authPolicy.X509PolicyOptions{ + AllowedNames: &authPolicy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + AllowWildcardLiteral: &falseValue, + VerifySubjectCommonName: &trueValue, + }, + }, + }, { name: "full-policy", policy: &linkedca.Policy{ @@ -216,6 +245,8 @@ func Test_policyToCertificates(t *testing.T) { Emails: []string{"badhost.example.com"}, Uris: []string{"https://badhost.local"}, }, + AllowWildcardLiteral: &wrapperspb.BoolValue{Value: true}, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, }, Ssh: &linkedca.SSHPolicy{ Host: &linkedca.SSHHostPolicy{ @@ -256,6 +287,8 @@ func Test_policyToCertificates(t *testing.T) { EmailAddresses: []string{"badhost.example.com"}, URIDomains: []string{"https://badhost.local"}, }, + AllowWildcardLiteral: &trueValue, + VerifySubjectCommonName: &trueValue, }, SSH: &authPolicy.SSHPolicyOptions{ Host: &authPolicy.SSHHostCertificateOptions{ diff --git a/authority/provisioner/options_test.go b/authority/provisioner/options_test.go index 8f411aca..32bea92b 100644 --- a/authority/provisioner/options_test.go +++ b/authority/provisioner/options_test.go @@ -287,3 +287,91 @@ func Test_unsafeParseSigned(t *testing.T) { }) } } + +func TestX509Options_IsWildcardLiteralAllowed(t *testing.T) { + trueValue := true + falseValue := false + tests := []struct { + name string + options *X509Options + want bool + }{ + { + name: "nil-options", + options: nil, + want: true, + }, + { + name: "nil", + options: &X509Options{ + AllowWildcardLiteral: nil, + }, + want: false, + }, + { + name: "set-true", + options: &X509Options{ + AllowWildcardLiteral: &trueValue, + }, + want: true, + }, + { + name: "set-false", + options: &X509Options{ + AllowWildcardLiteral: &falseValue, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.options.IsWildcardLiteralAllowed(); got != tt.want { + t.Errorf("X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestX509Options_ShouldVerifySubjectCommonName(t *testing.T) { + trueValue := true + falseValue := false + tests := []struct { + name string + options *X509Options + want bool + }{ + { + name: "nil-options", + options: nil, + want: false, + }, + { + name: "nil", + options: &X509Options{ + VerifySubjectCommonName: nil, + }, + want: true, + }, + { + name: "set-true", + options: &X509Options{ + VerifySubjectCommonName: &trueValue, + }, + want: true, + }, + { + name: "set-false", + options: &X509Options{ + VerifySubjectCommonName: &falseValue, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.options.ShouldVerifySubjectCommonName(); got != tt.want { + t.Errorf("X509PolicyOptions.ShouldVerifySubjectCommonName() = %v, want %v", got, tt.want) + } + }) + } +} From 3eecc4f7bbcf04f24393fd355d2c5ef1d90fe61a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 17:10:13 +0200 Subject: [PATCH 47/78] Improve test coverage for reloadPolicyEngines --- authority/admin/db.go | 5 + authority/authority.go | 57 ---- authority/policy.go | 67 +++++ authority/policy_test.go | 581 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 637 insertions(+), 73 deletions(-) diff --git a/authority/admin/db.go b/authority/admin/db.go index 75ac1368..0c0e7767 100644 --- a/authority/admin/db.go +++ b/authority/admin/db.go @@ -190,12 +190,15 @@ func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error { return m.MockError } +// CreateAuthorityPolicy mock func (m *MockDB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { if m.MockCreateAuthorityPolicy != nil { return m.MockCreateAuthorityPolicy(ctx, policy) } return m.MockError } + +// GetAuthorityPolicy mock func (m *MockDB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { if m.MockGetAuthorityPolicy != nil { return m.MockGetAuthorityPolicy(ctx) @@ -203,6 +206,7 @@ func (m *MockDB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, erro return m.MockRet1.(*linkedca.Policy), m.MockError } +// UpdateAuthorityPolicy mock func (m *MockDB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { if m.MockUpdateAuthorityPolicy != nil { return m.MockUpdateAuthorityPolicy(ctx, policy) @@ -210,6 +214,7 @@ func (m *MockDB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Pol return m.MockError } +// DeleteAuthorityPolicy mock func (m *MockDB) DeleteAuthorityPolicy(ctx context.Context) error { if m.MockDeleteAuthorityPolicy != nil { return m.MockDeleteAuthorityPolicy(ctx) diff --git a/authority/authority.go b/authority/authority.go index 49557de1..84864159 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -6,7 +6,6 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" - "fmt" "log" "strings" "sync" @@ -222,62 +221,6 @@ func (a *Authority) reloadAdminResources(ctx context.Context) error { return nil } -// reloadPolicyEngines reloads x509 and SSH policy engines using -// configuration stored in the DB or from the configuration file. -func (a *Authority) reloadPolicyEngines(ctx context.Context) error { - var ( - err error - policyOptions *policy.Options - ) - // if admin API is enabled, the CA is running in linked mode - if a.config.AuthorityConfig.EnableAdmin { - - // temporarily disable policy loading when LinkedCA is in use - if _, ok := a.adminDB.(*linkedCaClient); ok { - return nil - } - - // temporarily only support the admin nosql DB - if _, ok := a.adminDB.(*adminDBNosql.DB); !ok { - return nil - } - - linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) - if err != nil { - return fmt.Errorf("error getting policy to (re)load policy engines: %w", err) - } - policyOptions = policyToCertificates(linkedPolicy) - } else { - policyOptions = a.config.AuthorityConfig.Policy - } - - // if no new or updated policy option is set, clear policy engines that (may have) - // been configured before and return early - if policyOptions == nil { - a.x509Policy = nil - a.sshHostPolicy = nil - a.sshUserPolicy = nil - return nil - } - - // Initialize the x509 allow/deny policy engine - if a.x509Policy, err = policy.NewX509PolicyEngine(policyOptions.GetX509Options()); err != nil { - return err - } - - // // Initialize the SSH allow/deny policy engine for host certificates - if a.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(policyOptions.GetSSHOptions()); err != nil { - return err - } - - // // Initialize the SSH allow/deny policy engine for user certificates - if a.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(policyOptions.GetSSHOptions()); err != nil { - return err - } - - return nil -} - // init performs validation and initializes the fields of an Authority struct. func (a *Authority) init() error { // Check if handler has already been validated/initialized. diff --git a/authority/policy.go b/authority/policy.go index e626eaed..96307586 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -206,6 +206,73 @@ func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admi return nil } +// reloadPolicyEngines reloads x509 and SSH policy engines using +// configuration stored in the DB or from the configuration file. +func (a *Authority) reloadPolicyEngines(ctx context.Context) error { + var ( + err error + policyOptions *authPolicy.Options + ) + // if admin API is enabled, the CA is running in linked mode + if a.config.AuthorityConfig.EnableAdmin { + + // temporarily disable policy loading when LinkedCA is in use + if _, ok := a.adminDB.(*linkedCaClient); ok { + return nil + } + + // // temporarily only support the admin nosql DB + // if _, ok := a.adminDB.(*adminDBNosql.DB); !ok { + // return nil + // } + + linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) + if err != nil { + return fmt.Errorf("error getting policy to (re)load policy engines: %w", err) + } + policyOptions = policyToCertificates(linkedPolicy) + } else { + policyOptions = a.config.AuthorityConfig.Policy + } + + // if no new or updated policy option is set, clear policy engines that (may have) + // been configured before and return early + if policyOptions == nil { + a.x509Policy = nil + a.sshHostPolicy = nil + a.sshUserPolicy = nil + return nil + } + + var ( + x509Policy authPolicy.X509Policy + sshHostPolicy authPolicy.HostPolicy + sshUserPolicy authPolicy.UserPolicy + ) + + // initialize the x509 allow/deny policy engine + if x509Policy, err = authPolicy.NewX509PolicyEngine(policyOptions.GetX509Options()); err != nil { + return err + } + + // initialize the SSH allow/deny policy engine for host certificates + if sshHostPolicy, err = authPolicy.NewSSHHostPolicyEngine(policyOptions.GetSSHOptions()); err != nil { + return err + } + + // initialize the SSH allow/deny policy engine for user certificates + if sshUserPolicy, err = authPolicy.NewSSHUserPolicyEngine(policyOptions.GetSSHOptions()); err != nil { + return err + } + + // set all policy engines; all or nothing + a.x509Policy = x509Policy + a.sshHostPolicy = sshHostPolicy + a.sshUserPolicy = sshUserPolicy + + return nil +} + func isAllowed(engine authPolicy.X509Policy, sans []string) error { var ( allowed bool diff --git a/authority/policy_test.go b/authority/policy_test.go index 24fe5840..514a7a51 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -11,7 +11,9 @@ import ( "go.step.sm/linkedca" - authPolicy "github.com/smallstep/certificates/authority/policy" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/policy" ) func TestAuthority_checkPolicy(t *testing.T) { @@ -196,7 +198,7 @@ func Test_policyToCertificates(t *testing.T) { tests := []struct { name string policy *linkedca.Policy - want *authPolicy.Options + want *policy.Options }{ { name: "nil", @@ -219,9 +221,9 @@ func Test_policyToCertificates(t *testing.T) { VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, }, }, - want: &authPolicy.Options{ - X509: &authPolicy.X509PolicyOptions{ - AllowedNames: &authPolicy.X509NameOptions{ + want: &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.local"}, }, AllowWildcardLiteral: &falseValue, @@ -273,15 +275,15 @@ func Test_policyToCertificates(t *testing.T) { }, }, }, - want: &authPolicy.Options{ - X509: &authPolicy.X509PolicyOptions{ - AllowedNames: &authPolicy.X509NameOptions{ + want: &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"step"}, IPRanges: []string{"127.0.0.1/24"}, EmailAddresses: []string{"*.example.com"}, URIDomains: []string{"https://*.local"}, }, - DeniedNames: &authPolicy.X509NameOptions{ + DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"bad"}, IPRanges: []string{"127.0.0.30"}, EmailAddresses: []string{"badhost.example.com"}, @@ -290,25 +292,25 @@ func Test_policyToCertificates(t *testing.T) { AllowWildcardLiteral: &trueValue, VerifySubjectCommonName: &trueValue, }, - SSH: &authPolicy.SSHPolicyOptions{ - Host: &authPolicy.SSHHostCertificateOptions{ - AllowedNames: &authPolicy.SSHNameOptions{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ DNSDomains: []string{"*.localhost"}, IPRanges: []string{"127.0.0.1/24"}, Principals: []string{"user"}, }, - DeniedNames: &authPolicy.SSHNameOptions{ + DeniedNames: &policy.SSHNameOptions{ DNSDomains: []string{"badhost.localhost"}, IPRanges: []string{"127.0.0.40"}, Principals: []string{"root"}, }, }, - User: &authPolicy.SSHUserCertificateOptions{ - AllowedNames: &authPolicy.SSHNameOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ EmailAddresses: []string{"@work"}, Principals: []string{"user"}, }, - DeniedNames: &authPolicy.SSHNameOptions{ + DeniedNames: &policy.SSHNameOptions{ EmailAddresses: []string{"root@work"}, Principals: []string{"root"}, }, @@ -326,3 +328,550 @@ func Test_policyToCertificates(t *testing.T) { }) } } + +func TestAuthority_reloadPolicyEngines(t *testing.T) { + type exp struct { + x509Policy bool + sshUserPolicy bool + sshHostPolicy bool + } + trueValue := true + tests := []struct { + name string + config *config.Config + adminDB admin.DB + ctx context.Context + expected *exp + wantErr bool + }{ + { + name: "fail/standalone-x509-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"**.local"}, + }, + }, + }, + }, + }, + ctx: context.Background(), + wantErr: true, + }, + { + name: "fail/standalone-ssh-host-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"**.local"}, + }, + }, + }, + }, + }, + }, + ctx: context.Background(), + wantErr: true, + }, + { + name: "fail/standalone-ssh-user-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + EmailAddresses: []string{"**example.com"}, + }, + }, + }, + }, + }, + }, + ctx: context.Background(), + wantErr: true, + }, + { + name: "fail/adminDB.GetAuthorityPolicy-error", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, errors.New("force") + }, + }, + ctx: context.Background(), + wantErr: true, + }, + { + name: "fail/admin-x509-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"**.local"}, + }, + }, + }, nil + }, + }, + ctx: context.Background(), + wantErr: true, + }, + { + name: "fail/admin-ssh-host-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"**.local"}, + }, + }, + }, + }, nil + }, + }, + ctx: context.Background(), + wantErr: true, + }, + { + name: "fail/admin-ssh-user-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@@example.com"}, + }, + }, + }, + }, nil + }, + }, + ctx: context.Background(), + wantErr: true, + }, + { + name: "ok/linkedca-unsupported", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &linkedCaClient{}, + ctx: context.Background(), + wantErr: false, + }, + { + name: "ok/standalone-no-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: nil, + }, + }, + ctx: context.Background(), + wantErr: false, + expected: nil, + }, + { + name: "ok/standalone-x509-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.X509NameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + AllowWildcardLiteral: &trueValue, + VerifySubjectCommonName: &trueValue, + }, + }, + }, + }, + ctx: context.Background(), + wantErr: false, + expected: &exp{ + // expect only the X.509 policy to exist + x509Policy: true, + sshHostPolicy: false, + sshUserPolicy: false, + }, + }, + { + name: "ok/standalone-ssh-host-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + }, + }, + }, + }, + }, + ctx: context.Background(), + wantErr: false, + expected: &exp{ + // expect only the SSH host policy to exist + x509Policy: false, + sshHostPolicy: true, + sshUserPolicy: false, + }, + }, + { + name: "ok/standalone-ssh-user-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"*"}, + }, + DeniedNames: &policy.SSHNameOptions{ + Principals: []string{"root"}, + }, + }, + }, + }, + }, + }, + ctx: context.Background(), + wantErr: false, + expected: &exp{ + // expect only the SSH user policy to exist + x509Policy: false, + sshHostPolicy: false, + sshUserPolicy: true, + }, + }, + { + name: "ok/standalone-ssh-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"*"}, + }, + DeniedNames: &policy.SSHNameOptions{ + Principals: []string{"root"}, + }, + }, + }, + }, + }, + }, + ctx: context.Background(), + wantErr: false, + expected: &exp{ + // expect only the SSH policy engines to exist + x509Policy: false, + sshHostPolicy: true, + sshUserPolicy: true, + }, + }, + { + name: "ok/standalone-full-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: false, + Policy: &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.X509NameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + AllowWildcardLiteral: &trueValue, + VerifySubjectCommonName: &trueValue, + }, + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"*"}, + }, + DeniedNames: &policy.SSHNameOptions{ + Principals: []string{"root"}, + }, + }, + }, + }, + }, + }, + ctx: context.Background(), + wantErr: false, + expected: &exp{ + // expect all three policy engines to exist + x509Policy: true, + sshHostPolicy: true, + sshUserPolicy: true, + }, + }, + { + name: "ok/admin-x509-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + }, nil + }, + }, + ctx: context.Background(), + wantErr: false, + expected: &exp{ + x509Policy: true, + sshHostPolicy: false, + sshUserPolicy: false, + }, + }, + { + name: "ok/admin-ssh-host-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.local"}, + }, + }, + }, + }, nil + }, + }, + ctx: context.Background(), + wantErr: false, + expected: &exp{ + x509Policy: false, + sshHostPolicy: true, + sshUserPolicy: false, + }, + }, + { + name: "ok/admin-ssh-user-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + }, + }, + }, + }, nil + }, + }, + ctx: context.Background(), + wantErr: false, + expected: &exp{ + x509Policy: false, + sshHostPolicy: false, + sshUserPolicy: true, + }, + }, + { + name: "ok/admin-full-policy", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + ctx: context.Background(), + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"badhost.local"}, + }, + AllowWildcardLiteral: &wrapperspb.BoolValue{Value: true}, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + }, + Ssh: &linkedca.SSHPolicy{ + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.local"}, + }, + }, + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + }, + }, + }, + }, nil + }, + }, + wantErr: false, + expected: &exp{ + // expect all three policy engines to exist + x509Policy: true, + sshHostPolicy: true, + sshUserPolicy: true, + }, + }, + { + // both DB and JSON config; DB config is taken if Admin API is enabled + name: "ok/admin-over-standalone", + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + Policy: &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"*"}, + }, + DeniedNames: &policy.SSHNameOptions{ + Principals: []string{"root"}, + }, + }, + }, + }, + }, + }, + ctx: context.Background(), + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"badhost.local"}, + }, + AllowWildcardLiteral: &wrapperspb.BoolValue{Value: true}, + VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + }, + }, nil + }, + }, + wantErr: false, + expected: &exp{ + // expect all three policy engines to exist + x509Policy: true, + sshHostPolicy: false, + sshUserPolicy: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + config: tt.config, + adminDB: tt.adminDB, + } + if err := a.reloadPolicyEngines(tt.ctx); (err != nil) != tt.wantErr { + t.Errorf("Authority.reloadPolicyEngines() error = %v, wantErr %v", err, tt.wantErr) + } + + // if expected value is set, check existence of the policy engines + // Check that they're always nil if the expected value is not set, + // which happens on errors. + if tt.expected != nil { + assert.Equal(t, tt.expected.x509Policy, a.x509Policy != nil) + assert.Equal(t, tt.expected.sshHostPolicy, a.sshHostPolicy != nil) + assert.Equal(t, tt.expected.sshUserPolicy, a.sshUserPolicy != nil) + } else { + assert.Nil(t, a.x509Policy) + assert.Nil(t, a.sshHostPolicy) + assert.Nil(t, a.sshUserPolicy) + } + }) + } +} From a2cfbe3d546404932ca15bd8b7db981932a9a179 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 21 Apr 2022 12:14:03 +0200 Subject: [PATCH 48/78] Fix (part of) PR comments --- api/read/read.go | 2 +- authority/admin/api/policy.go | 9 +- authority/admin/api/policy_test.go | 9 +- authority/admin/errors.go | 13 +- authority/policy.go | 34 +-- authority/policy/options.go | 44 ++- authority/policy/policy.go | 32 +-- authority/provisioners.go | 70 ++--- policy/engine_test.go | 418 +++++++++++++---------------- policy/options.go | 143 ++-------- policy/options_test.go | 371 +++---------------------- 11 files changed, 346 insertions(+), 799 deletions(-) diff --git a/api/read/read.go b/api/read/read.go index 6cfc90ee..2f7348bd 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -24,7 +24,7 @@ func JSON(r io.Reader, v interface{}) error { } // ProtoJSON reads JSON from the request body and stores it in the value -// pointed to by v. +// pointed to by m. func ProtoJSON(r io.Reader, m proto.Message) error { data, err := io.ReadAll(r) if err != nil { diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index fc6ab1d9..34b7bf96 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -74,8 +74,7 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r } if policy != nil { - adminErr := admin.NewError(admin.ErrorBadRequestType, "authority already has a policy") - adminErr.Status = http.StatusConflict + adminErr := admin.NewError(admin.ErrorConflictType, "authority already has a policy") render.Error(w, adminErr) return } @@ -197,8 +196,7 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, policy := prov.GetPolicy() if policy != nil { - adminErr := admin.NewError(admin.ErrorBadRequestType, "provisioner %s already has a policy", prov.Name) - adminErr.Status = http.StatusConflict + adminErr := admin.NewError(admin.ErrorConflictType, "provisioner %s already has a policy", prov.Name) render.Error(w, adminErr) return } @@ -307,8 +305,7 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, policy := eak.GetPolicy() if policy != nil { - adminErr := admin.NewError(admin.ErrorBadRequestType, "ACME EAK %s already has a policy", eak.Id) - adminErr.Status = http.StatusConflict + adminErr := admin.NewError(admin.ErrorConflictType, "ACME EAK %s already has a policy", eak.Id) render.Error(w, adminErr) return } diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index e3cc6b65..72a462a4 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -154,9 +154,8 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { }, "fail/existing-policy": func(t *testing.T) test { ctx := context.Background() - err := admin.NewError(admin.ErrorBadRequestType, "authority already has a policy") + err := admin.NewError(admin.ErrorConflictType, "authority already has a policy") err.Message = "authority already has a policy" - err.Status = http.StatusConflict return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -864,9 +863,8 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { Policy: policy, } ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) - err := admin.NewError(admin.ErrorBadRequestType, "provisioner provName already has a policy") + err := admin.NewError(admin.ErrorConflictType, "provisioner provName already has a policy") err.Message = "provisioner provName already has a policy" - err.Status = http.StatusConflict return test{ ctx: ctx, err: err, @@ -1466,9 +1464,8 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { } ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) - err := admin.NewError(admin.ErrorBadRequestType, "ACME EAK eakID already has a policy") + err := admin.NewError(admin.ErrorConflictType, "ACME EAK eakID already has a policy") err.Message = "ACME EAK eakID already has a policy" - err.Status = http.StatusConflict return test{ ctx: ctx, err: err, diff --git a/authority/admin/errors.go b/authority/admin/errors.go index baa32dd9..2cf0c0e5 100644 --- a/authority/admin/errors.go +++ b/authority/admin/errors.go @@ -24,10 +24,12 @@ const ( ErrorBadRequestType // ErrorNotImplementedType not implemented. ErrorNotImplementedType - // ErrorUnauthorizedType internal server error. + // ErrorUnauthorizedType unauthorized. ErrorUnauthorizedType // ErrorServerInternalType internal server error. ErrorServerInternalType + // ErrorConflictType conflict. + ErrorConflictType ) // String returns the string representation of the admin problem type, @@ -48,6 +50,8 @@ func (ap ProblemType) String() string { return "unauthorized" case ErrorServerInternalType: return "internalServerError" + case ErrorConflictType: + return "conflict" default: return fmt.Sprintf("unsupported error type '%d'", int(ap)) } @@ -64,7 +68,7 @@ var ( errorServerInternalMetadata = errorMetadata{ typ: ErrorServerInternalType.String(), details: "the server experienced an internal error", - status: 500, + status: http.StatusInternalServerError, } errorMap = map[ProblemType]errorMetadata{ ErrorNotFoundType: { @@ -98,6 +102,11 @@ var ( status: http.StatusUnauthorized, }, ErrorServerInternalType: errorServerInternalMetadata, + ErrorConflictType: { + typ: ErrorConflictType.String(), + details: "conflict", + status: http.StatusConflict, + }, } ) diff --git a/authority/policy.go b/authority/policy.go index 96307586..dd24ecf7 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -318,11 +318,10 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { opts := &authPolicy.Options{} // fill x509 policy configuration - if p.GetX509() != nil { + if x509 := p.GetX509(); x509 != nil { opts.X509 = &authPolicy.X509PolicyOptions{} - if p.GetX509().GetAllow() != nil { + if allow := x509.GetAllow(); allow != nil { opts.X509.AllowedNames = &authPolicy.X509NameOptions{} - allow := p.GetX509().GetAllow() if allow.Dns != nil { opts.X509.AllowedNames.DNSDomains = allow.Dns } @@ -336,9 +335,8 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { opts.X509.AllowedNames.URIDomains = allow.Uris } } - if p.GetX509().GetDeny() != nil { + if deny := x509.GetDeny(); deny != nil { opts.X509.DeniedNames = &authPolicy.X509NameOptions{} - deny := p.GetX509().GetDeny() if deny.Dns != nil { opts.X509.DeniedNames.DNSDomains = deny.Dns } @@ -352,22 +350,21 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { opts.X509.DeniedNames.URIDomains = deny.Uris } } - if p.GetX509().GetAllowWildcardLiteral() != nil { - opts.X509.AllowWildcardLiteral = &p.GetX509().GetAllowWildcardLiteral().Value + if v := x509.GetAllowWildcardLiteral(); v != nil { + opts.X509.AllowWildcardLiteral = &v.Value } - if p.GetX509().GetVerifySubjectCommonName() != nil { - opts.X509.VerifySubjectCommonName = &p.GetX509().VerifySubjectCommonName.Value + if v := x509.GetVerifySubjectCommonName(); v != nil { + opts.X509.VerifySubjectCommonName = &v.Value } } // fill ssh policy configuration - if p.GetSsh() != nil { + if ssh := p.GetSsh(); ssh != nil { opts.SSH = &authPolicy.SSHPolicyOptions{} - if p.GetSsh().GetHost() != nil { + if host := ssh.GetHost(); host != nil { opts.SSH.Host = &authPolicy.SSHHostCertificateOptions{} - if p.GetSsh().GetHost().GetAllow() != nil { + if allow := host.GetAllow(); allow != nil { opts.SSH.Host.AllowedNames = &authPolicy.SSHNameOptions{} - allow := p.GetSsh().GetHost().GetAllow() if allow.Dns != nil { opts.SSH.Host.AllowedNames.DNSDomains = allow.Dns } @@ -378,9 +375,8 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { opts.SSH.Host.AllowedNames.Principals = allow.Principals } } - if p.GetSsh().GetHost().GetDeny() != nil { + if deny := host.GetDeny(); deny != nil { opts.SSH.Host.DeniedNames = &authPolicy.SSHNameOptions{} - deny := p.GetSsh().GetHost().GetDeny() if deny.Dns != nil { opts.SSH.Host.DeniedNames.DNSDomains = deny.Dns } @@ -392,11 +388,10 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { } } } - if p.GetSsh().GetUser() != nil { + if user := ssh.GetUser(); user != nil { opts.SSH.User = &authPolicy.SSHUserCertificateOptions{} - if p.GetSsh().GetUser().GetAllow() != nil { + if allow := user.GetAllow(); allow != nil { opts.SSH.User.AllowedNames = &authPolicy.SSHNameOptions{} - allow := p.GetSsh().GetUser().GetAllow() if allow.Emails != nil { opts.SSH.User.AllowedNames.EmailAddresses = allow.Emails } @@ -404,9 +399,8 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { opts.SSH.User.AllowedNames.Principals = allow.Principals } } - if p.GetSsh().GetUser().GetDeny() != nil { + if deny := user.GetDeny(); deny != nil { opts.SSH.User.DeniedNames = &authPolicy.SSHNameOptions{} - deny := p.GetSsh().GetUser().GetDeny() if deny.Emails != nil { opts.SSH.User.DeniedNames.EmailAddresses = deny.Emails } diff --git a/authority/policy/options.go b/authority/policy/options.go index c3b30c0a..68efe45a 100644 --- a/authority/policy/options.go +++ b/authority/policy/options.go @@ -67,6 +67,14 @@ func (o *X509NameOptions) HasNames() bool { len(o.URIDomains) > 0 } +// GetAllowedNameOptions returns x509 allowed name policy configuration +func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions { + if o == nil { + return nil + } + return o.AllowedNames +} + // GetDeniedNameOptions returns the x509 denied name policy configuration func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { if o == nil { @@ -75,18 +83,6 @@ func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { return o.DeniedNames } -// GetAllowedUserNameOptions returns the SSH allowed user name policy -// configuration. -func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - if o.User == nil { - return nil - } - return o.User.AllowedNames -} - func (o *X509PolicyOptions) IsWildcardLiteralAllowed() bool { if o == nil { return true @@ -122,21 +118,19 @@ type SSHPolicyOptions struct { Host *SSHHostCertificateOptions `json:"host,omitempty"` } -// GetAllowedNameOptions returns x509 allowed name policy configuration -func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions { - if o == nil { +// GetAllowedUserNameOptions returns the SSH allowed user name policy +// configuration. +func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { + if o == nil || o.User == nil { return nil } - return o.AllowedNames + return o.User.AllowedNames } // GetDeniedUserNameOptions returns the SSH denied user name policy // configuration. func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - if o.User == nil { + if o == nil || o.User == nil { return nil } return o.User.DeniedNames @@ -145,10 +139,7 @@ func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions { // GetAllowedHostNameOptions returns the SSH allowed host name policy // configuration. func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - if o.Host == nil { + if o == nil || o.Host == nil { return nil } return o.Host.AllowedNames @@ -157,10 +148,7 @@ func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions { // GetDeniedHostNameOptions returns the SSH denied host name policy // configuration. func (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - if o.Host == nil { + if o == nil || o.Host == nil { return nil } return o.Host.DeniedNames diff --git a/authority/policy/policy.go b/authority/policy/policy.go index f1142ea7..564fca24 100644 --- a/authority/policy/policy.go +++ b/authority/policy/policy.go @@ -28,20 +28,20 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, allowed := policyOptions.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { options = append(options, - policy.WithPermittedDNSDomains(allowed.DNSDomains), - policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), - policy.WithPermittedEmailAddresses(allowed.EmailAddresses), - policy.WithPermittedURIDomains(allowed.URIDomains), + policy.WithPermittedDNSDomains(allowed.DNSDomains...), + policy.WithPermittedIPsOrCIDRs(allowed.IPRanges...), + policy.WithPermittedEmailAddresses(allowed.EmailAddresses...), + policy.WithPermittedURIDomains(allowed.URIDomains...), ) } denied := policyOptions.GetDeniedNameOptions() if denied != nil && denied.HasNames() { options = append(options, - policy.WithExcludedDNSDomains(denied.DNSDomains), - policy.WithExcludedIPsOrCIDRs(denied.IPRanges), - policy.WithExcludedEmailAddresses(denied.EmailAddresses), - policy.WithExcludedURIDomains(denied.URIDomains), + policy.WithExcludedDNSDomains(denied.DNSDomains...), + policy.WithExcludedIPsOrCIDRs(denied.IPRanges...), + policy.WithExcludedEmailAddresses(denied.EmailAddresses...), + policy.WithExcludedURIDomains(denied.URIDomains...), ) } @@ -114,19 +114,19 @@ func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEn if allowed != nil && allowed.HasNames() { options = append(options, - policy.WithPermittedDNSDomains(allowed.DNSDomains), - policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), - policy.WithPermittedEmailAddresses(allowed.EmailAddresses), - policy.WithPermittedPrincipals(allowed.Principals), + policy.WithPermittedDNSDomains(allowed.DNSDomains...), + policy.WithPermittedIPsOrCIDRs(allowed.IPRanges...), + policy.WithPermittedEmailAddresses(allowed.EmailAddresses...), + policy.WithPermittedPrincipals(allowed.Principals...), ) } if denied != nil && denied.HasNames() { options = append(options, - policy.WithExcludedDNSDomains(denied.DNSDomains), - policy.WithExcludedIPsOrCIDRs(denied.IPRanges), - policy.WithExcludedEmailAddresses(denied.EmailAddresses), - policy.WithExcludedPrincipals(denied.Principals), + policy.WithExcludedDNSDomains(denied.DNSDomains...), + policy.WithExcludedIPsOrCIDRs(denied.IPRanges...), + policy.WithExcludedEmailAddresses(denied.EmailAddresses...), + policy.WithExcludedPrincipals(denied.Principals...), ) } diff --git a/authority/provisioners.go b/authority/provisioners.go index 990d892f..26aff4d8 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -10,17 +10,19 @@ import ( "os" "github.com/pkg/errors" + "gopkg.in/square/go-jose.v2/jwt" + + "go.step.sm/cli-utils/step" + "go.step.sm/cli-utils/ui" + "go.step.sm/crypto/jose" + "go.step.sm/linkedca" + "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" - "go.step.sm/cli-utils/step" - "go.step.sm/cli-utils/ui" - "go.step.sm/crypto/jose" - "go.step.sm/linkedca" - "gopkg.in/square/go-jose.v2/jwt" ) // GetEncryptedKey returns the JWE key corresponding to the given kid argument. @@ -440,55 +442,55 @@ func optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options { ops.SSH.Template = string(p.SshTemplate.Template) ops.SSH.TemplateData = p.SshTemplate.Data } - if p.Policy != nil { - if p.Policy.X509 != nil { - if p.Policy.X509.Allow != nil { + if pol := p.GetPolicy(); pol != nil { + if x := pol.GetX509(); x != nil { + if allow := x.GetAllow(); allow != nil { ops.X509.AllowedNames = &policy.X509NameOptions{ - DNSDomains: p.Policy.X509.Allow.Dns, - IPRanges: p.Policy.X509.Allow.Ips, - EmailAddresses: p.Policy.X509.Allow.Emails, - URIDomains: p.Policy.X509.Allow.Uris, + DNSDomains: allow.Dns, + IPRanges: allow.Ips, + EmailAddresses: allow.Emails, + URIDomains: allow.Uris, } } - if p.Policy.X509.Deny != nil { + if deny := x.GetDeny(); deny != nil { ops.X509.DeniedNames = &policy.X509NameOptions{ - DNSDomains: p.Policy.X509.Deny.Dns, - IPRanges: p.Policy.X509.Deny.Ips, - EmailAddresses: p.Policy.X509.Deny.Emails, - URIDomains: p.Policy.X509.Deny.Uris, + DNSDomains: deny.Dns, + IPRanges: deny.Ips, + EmailAddresses: deny.Emails, + URIDomains: deny.Uris, } } } - if p.Policy.Ssh != nil { - if p.Policy.Ssh.Host != nil { + if ssh := pol.GetSsh(); ssh != nil { + if host := ssh.GetHost(); host != nil { ops.SSH.Host = &policy.SSHHostCertificateOptions{} - if p.Policy.Ssh.Host.Allow != nil { + if allow := host.GetAllow(); allow != nil { 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, + DNSDomains: allow.Dns, + IPRanges: allow.Ips, + Principals: allow.Principals, } } - if p.Policy.Ssh.Host.Deny != nil { + if deny := host.GetDeny(); 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, + DNSDomains: deny.Dns, + IPRanges: deny.Ips, + Principals: deny.Principals, } } } - if p.Policy.Ssh.User != nil { + if user := ssh.GetUser(); user != nil { ops.SSH.User = &policy.SSHUserCertificateOptions{} - if p.Policy.Ssh.User.Allow != nil { + if allow := user.GetAllow(); allow != nil { ops.SSH.User.AllowedNames = &policy.SSHNameOptions{ - EmailAddresses: p.Policy.Ssh.User.Allow.Emails, - Principals: p.Policy.Ssh.User.Allow.Principals, + EmailAddresses: allow.Emails, + Principals: allow.Principals, } } - if p.Policy.Ssh.User.Deny != nil { + if deny := user.GetDeny(); deny != nil { ops.SSH.User.DeniedNames = &policy.SSHNameOptions{ - EmailAddresses: p.Policy.Ssh.User.Deny.Emails, - Principals: p.Policy.Ssh.User.Deny.Principals, + EmailAddresses: deny.Emails, + Principals: deny.Principals, } } } diff --git a/policy/engine_test.go b/policy/engine_test.go index 25e69af3..cce4ad34 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -637,7 +637,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -648,7 +648,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-wildcard-literal-x509", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.x509local"), + WithPermittedDNSDomains("*.x509local"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -661,7 +661,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-single-host", options: []NamePolicyOption{ - WithPermittedDNSDomain("host.local"), + WithPermittedDNSDomains("host.local"), }, cert: &x509.Certificate{ DNSNames: []string{"differenthost.local"}, @@ -672,7 +672,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-no-label", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"local"}, @@ -683,7 +683,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-empty-label", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"www..local"}, @@ -694,7 +694,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-dot-domain", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -707,7 +707,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-wildcard-multiple-subdomains", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -720,7 +720,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-wildcard-literal", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -733,7 +733,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.豆.jp"), + WithPermittedDNSDomains("*.豆.jp"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -747,11 +747,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/ipv4-permitted", options: []NamePolicyOption{ WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -765,11 +763,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/ipv6-permitted", options: []NamePolicyOption{ WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, + &net.IPNet{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), }, ), }, @@ -782,7 +778,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-wildcard", options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -795,7 +791,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-wildcard-x509", options: []NamePolicyOption{ - WithPermittedEmailAddress("example.com"), + WithPermittedEmailAddresses("example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -808,7 +804,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-specific-mailbox", options: []NamePolicyOption{ - WithPermittedEmailAddress("test@local.com"), + WithPermittedEmailAddresses("test@local.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -821,7 +817,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-wildcard-subdomain", options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -834,7 +830,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - WithPermittedEmailAddress("@例.jp"), + WithPermittedEmailAddresses("@例.jp"), }, cert: &x509.Certificate{ EmailAddresses: []string{"bücher@例.jp"}, @@ -845,7 +841,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-idna-internationalized-domain-rfc822", options: []NamePolicyOption{ - WithPermittedEmailAddress("@例.jp"), + WithPermittedEmailAddresses("@例.jp"), }, cert: &x509.Certificate{ EmailAddresses: []string{"bücher@例.jp" + string(byte(0))}, @@ -856,7 +852,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-idna-internationalized-domain-ascii", options: []NamePolicyOption{ - WithPermittedEmailAddress("@例.jp"), + WithPermittedEmailAddresses("@例.jp"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@xn---bla.jp"}, @@ -867,7 +863,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-domain-wildcard", options: []NamePolicyOption{ - WithPermittedURIDomain("*.local"), + WithPermittedURIDomains("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -883,7 +879,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted", options: []NamePolicyOption{ - WithPermittedURIDomain("test.local"), + WithPermittedURIDomains("test.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -899,7 +895,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld options: []NamePolicyOption{ - WithPermittedURIDomain("*.local"), + WithPermittedURIDomains("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -915,7 +911,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - WithPermittedURIDomain("*.bücher.example.com"), + WithPermittedURIDomains("*.bücher.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -932,7 +928,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-excluded", options: []NamePolicyOption{ - WithExcludedDNSDomain("*.example.com"), + WithExcludedDNSDomains("*.example.com"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -943,7 +939,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-excluded-single-host", options: []NamePolicyOption{ - WithExcludedDNSDomain("host.example.com"), + WithExcludedDNSDomains("host.example.com"), }, cert: &x509.Certificate{ DNSNames: []string{"host.example.com"}, @@ -955,11 +951,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/ipv4-excluded", options: []NamePolicyOption{ WithExcludedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -973,11 +967,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/ipv6-excluded", options: []NamePolicyOption{ WithExcludedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, + &net.IPNet{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), }, ), }, @@ -990,7 +982,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-excluded", options: []NamePolicyOption{ - WithExcludedEmailAddress("@example.com"), + WithExcludedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, @@ -1001,7 +993,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-excluded", options: []NamePolicyOption{ - WithExcludedURIDomain("*.example.com"), + WithExcludedURIDomains("*.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1017,7 +1009,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-excluded-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld options: []NamePolicyOption{ - WithExcludedURIDomain("*.local"), + WithExcludedURIDomains("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1035,7 +1027,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-dns-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1049,7 +1041,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-dns-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithExcludedDNSDomain("*.local"), + WithExcludedDNSDomains("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1064,11 +1056,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -1085,11 +1075,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithExcludedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -1106,11 +1094,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, + &net.IPNet{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), }, ), }, @@ -1127,11 +1113,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithExcludedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, + &net.IPNet{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), }, ), }, @@ -1147,7 +1131,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-email-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedEmailAddress("@example.local"), + WithPermittedEmailAddresses("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1161,7 +1145,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-email-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithExcludedEmailAddress("@example.local"), + WithExcludedEmailAddresses("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1175,7 +1159,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-uri-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedURIDomain("*.example.com"), + WithPermittedURIDomains("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1189,7 +1173,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/subject-uri-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithExcludedURIDomain("*.example.com"), + WithExcludedURIDomains("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1203,7 +1187,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-with-ip-name", // when only DNS is permitted, IPs are not allowed. options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, @@ -1214,7 +1198,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-with-mail", // when only DNS is permitted, mails are not allowed. options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@smallstep.com"}, @@ -1225,7 +1209,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/dns-permitted-with-uri", // when only DNS is permitted, URIs are not allowed. options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1242,11 +1226,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/ip-permitted-with-dns-name", // when only IP is permitted, DNS names are not allowed. options: []NamePolicyOption{ WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -1260,11 +1242,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/ip-permitted-with-mail", // when only IP is permitted, mails are not allowed. options: []NamePolicyOption{ WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -1278,11 +1258,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/ip-permitted-with-uri", // when only IP is permitted, URIs are not allowed. options: []NamePolicyOption{ WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -1300,7 +1278,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-with-dns-name", // when only mail is permitted, DNS names are not allowed. options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, @@ -1311,7 +1289,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-with-ip", // when only mail is permitted, IPs are not allowed. options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{ @@ -1324,7 +1302,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/mail-permitted-with-uri", // when only mail is permitted, URIs are not allowed. options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1340,7 +1318,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-with-dns-name", // when only URI is permitted, DNS names are not allowed. options: []NamePolicyOption{ - WithPermittedURIDomain("*.local"), + WithPermittedURIDomains("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"host.local"}, @@ -1351,7 +1329,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, IPs are not allowed. options: []NamePolicyOption{ - WithPermittedURIDomain("*.local"), + WithPermittedURIDomains("*.local"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{ @@ -1364,7 +1342,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, mails are not allowed. options: []NamePolicyOption{ - WithPermittedURIDomain("*.local"), + WithPermittedURIDomains("*.local"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@smallstep.com"}, @@ -1377,14 +1355,14 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "fail/combined-simple-all-badhost.local", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedDNSDomain("*.local"), - WithPermittedCIDR("127.0.0.1/24"), - WithPermittedEmailAddress("@example.local"), - WithPermittedURIDomain("*.example.local"), - WithExcludedDNSDomain("badhost.local"), - WithExcludedCIDR("127.0.0.128/25"), - WithExcludedEmailAddress("badmail@example.local"), - WithExcludedURIDomain("badwww.example.local"), + WithPermittedDNSDomains("*.local"), + WithPermittedCIDRs("127.0.0.1/24"), + WithPermittedEmailAddresses("@example.local"), + WithPermittedURIDomains("*.example.local"), + WithExcludedDNSDomains("badhost.local"), + WithExcludedCIDRs("127.0.0.128/25"), + WithExcludedEmailAddresses("badmail@example.local"), + WithExcludedURIDomains("badwww.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1488,7 +1466,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ DNSNames: []string{"example.local"}, @@ -1499,7 +1477,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted-wildcard", options: []NamePolicyOption{ - WithPermittedDNSDomains([]string{"*.local", "*.x509local"}), + WithPermittedDNSDomains("*.local", "*.x509local"), WithAllowLiteralWildcardNames(), }, cert: &x509.Certificate{ @@ -1514,7 +1492,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted-wildcard-literal", options: []NamePolicyOption{ - WithPermittedDNSDomains([]string{"*.local", "*.x509local"}), + WithPermittedDNSDomains("*.local", "*.x509local"), WithAllowLiteralWildcardNames(), }, cert: &x509.Certificate{ @@ -1529,7 +1507,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted-combined", options: []NamePolicyOption{ - WithPermittedDNSDomains([]string{"*.local", "*.x509local", "host.example.com"}), + WithPermittedDNSDomains("*.local", "*.x509local", "host.example.com"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -1544,7 +1522,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.例.jp"), + WithPermittedDNSDomains("*.例.jp"), }, cert: &x509.Certificate{ DNSNames: []string{ @@ -1557,7 +1535,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ipv4-permitted", options: []NamePolicyOption{ - WithPermittedCIDR("127.0.0.1/24"), + WithPermittedCIDRs("127.0.0.1/24"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, @@ -1568,7 +1546,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ipv6-permitted", options: []NamePolicyOption{ - WithPermittedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), + WithPermittedCIDRs("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, @@ -1579,7 +1557,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-permitted-wildcard", options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -1592,7 +1570,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-permitted-plain-domain", options: []NamePolicyOption{ - WithPermittedEmailAddress("example.com"), + WithPermittedEmailAddresses("example.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -1605,7 +1583,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-permitted-specific-mailbox", options: []NamePolicyOption{ - WithPermittedEmailAddress("test@local.com"), + WithPermittedEmailAddresses("test@local.com"), }, cert: &x509.Certificate{ EmailAddresses: []string{ @@ -1618,7 +1596,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - WithPermittedEmailAddress("@例.jp"), + WithPermittedEmailAddresses("@例.jp"), }, cert: &x509.Certificate{ EmailAddresses: []string{}, @@ -1629,7 +1607,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-domain-wildcard", options: []NamePolicyOption{ - WithPermittedURIDomain("*.local"), + WithPermittedURIDomains("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1645,7 +1623,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-specific-uri", options: []NamePolicyOption{ - WithPermittedURIDomain("test.local"), + WithPermittedURIDomains("test.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1661,7 +1639,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-with-port", options: []NamePolicyOption{ - WithPermittedURIDomain("*.example.com"), + WithPermittedURIDomains("*.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1677,7 +1655,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - WithPermittedURIDomain("*.bücher.example.com"), + WithPermittedURIDomains("*.bücher.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1693,7 +1671,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-permitted-idna-internationalized-domain", options: []NamePolicyOption{ - WithPermittedURIDomain("bücher.example.com"), + WithPermittedURIDomains("bücher.example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1710,7 +1688,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-excluded", options: []NamePolicyOption{ - WithExcludedDNSDomain("*.notlocal"), + WithExcludedDNSDomains("*.notlocal"), }, cert: &x509.Certificate{ DNSNames: []string{"example.local"}, @@ -1722,11 +1700,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/ipv4-excluded", options: []NamePolicyOption{ WithExcludedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -1739,7 +1715,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ipv6-excluded", options: []NamePolicyOption{ - WithExcludedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), + WithExcludedCIDRs("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, @@ -1750,7 +1726,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-excluded", options: []NamePolicyOption{ - WithExcludedEmailAddress("@notlocal"), + WithExcludedEmailAddresses("@notlocal"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@local"}, @@ -1761,7 +1737,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-excluded-with-subdomain", options: []NamePolicyOption{ - WithExcludedEmailAddress("@local"), + WithExcludedEmailAddresses("@local"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.local"}, @@ -1772,7 +1748,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-excluded", options: []NamePolicyOption{ - WithExcludedURIDomain("*.google.com"), + WithExcludedURIDomains("*.google.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -1790,7 +1766,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-empty", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1805,7 +1781,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-dns-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1819,7 +1795,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-dns-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithExcludedDNSDomain("*.notlocal"), + WithExcludedDNSDomains("*.notlocal"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1834,11 +1810,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("127.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -1855,11 +1829,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithExcludedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("128.0.0.1"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + &net.IPNet{ + IP: net.ParseIP("128.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), }, ), }, @@ -1876,11 +1848,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithPermittedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, + &net.IPNet{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), }, ), }, @@ -1897,11 +1867,9 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithExcludedIPRanges( - []*net.IPNet{ - { - IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), - Mask: net.CIDRMask(120, 128), - }, + &net.IPNet{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Mask: net.CIDRMask(120, 128), }, ), }, @@ -1917,7 +1885,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-email-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedEmailAddress("@example.local"), + WithPermittedEmailAddresses("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1931,7 +1899,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-email-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithExcludedEmailAddress("@example.notlocal"), + WithExcludedEmailAddresses("@example.notlocal"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1945,7 +1913,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-uri-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedURIDomain("*.example.com"), + WithPermittedURIDomains("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1959,7 +1927,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/subject-uri-excluded", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithExcludedURIDomain("*.smallstep.com"), + WithExcludedURIDomains("*.smallstep.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -1973,7 +1941,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-excluded-with-ip-name", // when only DNS is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedDNSDomain("*.local"), + WithExcludedDNSDomains("*.local"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, @@ -1984,7 +1952,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedDNSDomain("*.local"), + WithExcludedDNSDomains("*.local"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, @@ -1995,7 +1963,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedDNSDomain("*.local"), + WithExcludedDNSDomains("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -2011,7 +1979,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ip-excluded-with-dns", // when only IP is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedCIDR("127.0.0.1/24"), + WithExcludedCIDRs("127.0.0.1/24"), }, cert: &x509.Certificate{ DNSNames: []string{"test.local"}, @@ -2022,7 +1990,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ip-excluded-with-mail", // when only IP is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedCIDR("127.0.0.1/24"), + WithExcludedCIDRs("127.0.0.1/24"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, @@ -2033,7 +2001,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/ip-excluded-with-mail", // when only IP is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedCIDR("127.0.0.1/24"), + WithExcludedCIDRs("127.0.0.1/24"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -2049,7 +2017,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-excluded-with-dns", // when only mail is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedEmailAddress("@example.com"), + WithExcludedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ DNSNames: []string{"test.local"}, @@ -2060,7 +2028,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-excluded-with-ip", // when only mail is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedEmailAddress("@example.com"), + WithExcludedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, @@ -2071,7 +2039,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/mail-excluded-with-uri", // when only mail is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedEmailAddress("@example.com"), + WithExcludedEmailAddresses("@example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ @@ -2087,7 +2055,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-excluded-with-dns", // when only URI is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedURIDomain("*.example.local"), + WithExcludedURIDomains("*.example.local"), }, cert: &x509.Certificate{ DNSNames: []string{"test.example.local"}, @@ -2098,7 +2066,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-excluded-with-dns", // when only URI is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedURIDomain("*.example.local"), + WithExcludedURIDomains("*.example.local"), }, cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, @@ -2109,7 +2077,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/uri-excluded-with-mail", // when only URI is exluded, we allow anything else options: []NamePolicyOption{ - WithExcludedURIDomain("*.example.local"), + WithExcludedURIDomains("*.example.local"), }, cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.local"}, @@ -2121,7 +2089,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/dns-excluded-with-subject-ip-name", // when only DNS is exluded, we allow anything else options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithExcludedDNSDomain("*.local"), + WithExcludedDNSDomains("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -2137,10 +2105,10 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/combined-simple-permitted", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedDNSDomain("*.local"), - WithPermittedCIDR("127.0.0.1/24"), - WithPermittedEmailAddress("@example.local"), - WithPermittedURIDomain("*.example.local"), + WithPermittedDNSDomains("*.local"), + WithPermittedCIDRs("127.0.0.1/24"), + WithPermittedEmailAddresses("@example.local"), + WithPermittedURIDomains("*.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -2162,10 +2130,10 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { { name: "ok/combined-simple-permitted-without-subject-verification", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), - WithPermittedCIDR("127.0.0.1/24"), - WithPermittedEmailAddress("@example.local"), - WithPermittedURIDomain("*.example.local"), + WithPermittedDNSDomains("*.local"), + WithPermittedCIDRs("127.0.0.1/24"), + WithPermittedEmailAddresses("@example.local"), + WithPermittedURIDomains("*.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -2188,14 +2156,14 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { name: "ok/combined-simple-all", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), - WithPermittedDNSDomain("*.local"), - WithPermittedCIDR("127.0.0.1/24"), - WithPermittedEmailAddress("@example.local"), - WithPermittedURIDomain("*.example.local"), - WithExcludedDNSDomain("badhost.local"), - WithExcludedCIDR("127.0.0.128/25"), - WithExcludedEmailAddress("badmail@example.local"), - WithExcludedURIDomain("badwww.example.local"), + WithPermittedDNSDomains("*.local"), + WithPermittedCIDRs("127.0.0.1/24"), + WithPermittedEmailAddresses("@example.local"), + WithPermittedURIDomains("*.example.local"), + WithExcludedDNSDomains("badhost.local"), + WithExcludedCIDRs("127.0.0.128/25"), + WithExcludedEmailAddresses("badmail@example.local"), + WithExcludedURIDomains("badwww.example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -2280,7 +2248,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/host-with-permitted-dns-domain", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2294,7 +2262,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/host-with-excluded-dns-domain", options: []NamePolicyOption{ - WithExcludedDNSDomain("*.local"), + WithExcludedDNSDomains("*.local"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2306,9 +2274,9 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/host-with-permitted-ip", + name: "fail/host-with-permitted-cidr", options: []NamePolicyOption{ - WithPermittedCIDR("127.0.0.1/24"), + WithPermittedCIDRs("127.0.0.1/24"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2320,9 +2288,9 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/host-with-excluded-ip", + name: "fail/host-with-excluded-cidr", options: []NamePolicyOption{ - WithExcludedCIDR("127.0.0.1/24"), + WithExcludedCIDRs("127.0.0.1/24"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2336,7 +2304,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/user-with-permitted-email", options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2350,7 +2318,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/user-with-excluded-email", options: []NamePolicyOption{ - WithExcludedEmailAddress("@example.com"), + WithExcludedEmailAddresses("@example.com"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2364,7 +2332,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/host-with-permitted-principals", options: []NamePolicyOption{ - WithPermittedPrincipals([]string{"localhost"}), + WithPermittedPrincipals("localhost"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2378,7 +2346,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/host-with-excluded-principals", options: []NamePolicyOption{ - WithExcludedPrincipals([]string{"localhost"}), + WithExcludedPrincipals("localhost"), }, cert: &ssh.Certificate{ ValidPrincipals: []string{ @@ -2391,7 +2359,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/user-with-permitted-principals", options: []NamePolicyOption{ - WithPermittedPrincipals([]string{"user"}), + WithPermittedPrincipals("user"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2405,7 +2373,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/user-with-excluded-principals", options: []NamePolicyOption{ - WithExcludedPrincipals([]string{"user"}), + WithExcludedPrincipals("user"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2419,7 +2387,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/user-with-permitted-principal-as-mail", options: []NamePolicyOption{ - WithPermittedPrincipals([]string{"ops"}), + WithPermittedPrincipals("ops"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2433,7 +2401,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/host-principal-with-permitted-dns-domain", // when only DNS is permitted, username principals are not allowed. options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2447,7 +2415,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/host-principal-with-permitted-ip-range", // when only IPs are permitted, username principals are not allowed. options: []NamePolicyOption{ - WithPermittedCIDR("127.0.0.1/24"), + WithPermittedCIDRs("127.0.0.1/24"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2461,7 +2429,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/user-principal-with-permitted-email", // when only emails are permitted, username principals are not allowed. options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2475,8 +2443,8 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/combined-user", options: []NamePolicyOption{ - WithPermittedEmailAddress("@smallstep.com"), - WithExcludedEmailAddress("root@smallstep.com"), + WithPermittedEmailAddresses("@smallstep.com"), + WithExcludedEmailAddresses("root@smallstep.com"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2491,8 +2459,8 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "fail/combined-user-with-excluded-user-principal", options: []NamePolicyOption{ - WithPermittedEmailAddress("@smallstep.com"), - WithExcludedPrincipals([]string{"root"}), + WithPermittedEmailAddresses("@smallstep.com"), + WithExcludedPrincipals("root"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2507,7 +2475,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/host-with-permitted-user-principals", options: []NamePolicyOption{ - WithPermittedEmailAddress("@work"), + WithPermittedEmailAddresses("@work"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2521,7 +2489,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/user-with-permitted-user-principals", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2535,7 +2503,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/host-with-permitted-dns-domain", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), + WithPermittedDNSDomains("*.local"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2549,7 +2517,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/host-with-excluded-dns-domain", options: []NamePolicyOption{ - WithExcludedDNSDomain("*.example.com"), + WithExcludedDNSDomains("*.example.com"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2563,7 +2531,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/host-with-permitted-ip", options: []NamePolicyOption{ - WithPermittedCIDR("127.0.0.1/24"), + WithPermittedCIDRs("127.0.0.1/24"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2577,7 +2545,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/host-with-excluded-ip", options: []NamePolicyOption{ - WithExcludedCIDR("127.0.0.1/24"), + WithExcludedCIDRs("127.0.0.1/24"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, @@ -2591,7 +2559,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/user-with-permitted-email", options: []NamePolicyOption{ - WithPermittedEmailAddress("@example.com"), + WithPermittedEmailAddresses("@example.com"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2605,7 +2573,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/user-with-excluded-email", options: []NamePolicyOption{ - WithExcludedEmailAddress("@example.com"), + WithExcludedEmailAddresses("@example.com"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2619,7 +2587,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/user-with-permitted-principals", options: []NamePolicyOption{ - WithPermittedPrincipals([]string{"*"}), + WithPermittedPrincipals("*"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2633,7 +2601,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/user-with-excluded-principals", options: []NamePolicyOption{ - WithExcludedPrincipals([]string{"user"}), + WithExcludedPrincipals("user"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2647,9 +2615,9 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/combined-user", options: []NamePolicyOption{ - WithPermittedEmailAddress("@smallstep.com"), - WithPermittedPrincipals([]string{"*"}), // without specifying the wildcard, "someone" would not be allowed. - WithExcludedEmailAddress("root@smallstep.com"), + WithPermittedEmailAddresses("@smallstep.com"), + WithPermittedPrincipals("*"), // without specifying the wildcard, "someone" would not be allowed. + WithExcludedEmailAddresses("root@smallstep.com"), }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2664,9 +2632,9 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/combined-user-with-excluded-user-principal", options: []NamePolicyOption{ - WithPermittedEmailAddress("@smallstep.com"), - WithExcludedEmailAddress("root@smallstep.com"), - WithExcludedPrincipals([]string{"root"}), // unlike the previous test, this implicitly allows any other username principal + WithPermittedEmailAddresses("@smallstep.com"), + WithExcludedEmailAddresses("root@smallstep.com"), + WithExcludedPrincipals("root"), // unlike the previous test, this implicitly allows any other username principal }, cert: &ssh.Certificate{ CertType: ssh.UserCert, @@ -2681,10 +2649,10 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { { name: "ok/combined-host", options: []NamePolicyOption{ - WithPermittedDNSDomain("*.local"), - WithPermittedCIDR("127.0.0.1/24"), - WithExcludedDNSDomain("badhost.local"), - WithExcludedCIDR("127.0.0.128/25"), + WithPermittedDNSDomains("*.local"), + WithPermittedCIDRs("127.0.0.1/24"), + WithExcludedDNSDomains("badhost.local"), + WithExcludedCIDRs("127.0.0.128/25"), }, cert: &ssh.Certificate{ CertType: ssh.HostCert, diff --git a/policy/options.go b/policy/options.go index e01e082e..d244a311 100755 --- a/policy/options.go +++ b/policy/options.go @@ -26,7 +26,7 @@ func WithAllowLiteralWildcardNames() NamePolicyOption { } } -func WithPermittedDNSDomains(domains []string) NamePolicyOption { +func WithPermittedDNSDomains(domains ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomains := make([]string, len(domains)) for i, domain := range domains { @@ -41,7 +41,7 @@ func WithPermittedDNSDomains(domains []string) NamePolicyOption { } } -func WithExcludedDNSDomains(domains []string) NamePolicyOption { +func WithExcludedDNSDomains(domains ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomains := make([]string, len(domains)) for i, domain := range domains { @@ -56,36 +56,14 @@ func WithExcludedDNSDomains(domains []string) NamePolicyOption { } } -func WithPermittedDNSDomain(domain string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse permitted domain constraint %q: %w", domain, err) - } - e.permittedDNSDomains = []string{normalizedDomain} - return nil - } -} - -func WithExcludedDNSDomain(domain string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse excluded domain constraint %q: %w", domain, err) - } - e.excludedDNSDomains = []string{normalizedDomain} - return nil - } -} - -func WithPermittedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { +func WithPermittedIPRanges(ipRanges ...*net.IPNet) NamePolicyOption { return func(e *NamePolicyEngine) error { e.permittedIPRanges = ipRanges return nil } } -func WithPermittedCIDRs(cidrs []string) NamePolicyOption { +func WithPermittedCIDRs(cidrs ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { networks := make([]*net.IPNet, len(cidrs)) for i, cidr := range cidrs { @@ -100,7 +78,7 @@ func WithPermittedCIDRs(cidrs []string) NamePolicyOption { } } -func WithExcludedCIDRs(cidrs []string) NamePolicyOption { +func WithExcludedCIDRs(cidrs ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { networks := make([]*net.IPNet, len(cidrs)) for i, cidr := range cidrs { @@ -115,7 +93,7 @@ func WithExcludedCIDRs(cidrs []string) NamePolicyOption { } } -func WithPermittedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { +func WithPermittedIPsOrCIDRs(ipsOrCIDRs ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { networks := make([]*net.IPNet, len(ipsOrCIDRs)) for i, ipOrCIDR := range ipsOrCIDRs { @@ -133,7 +111,7 @@ func WithPermittedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { } } -func WithExcludedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { +func WithExcludedIPsOrCIDRs(ipsOrCIDRs ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { networks := make([]*net.IPNet, len(ipsOrCIDRs)) for i, ipOrCIDR := range ipsOrCIDRs { @@ -151,61 +129,14 @@ func WithExcludedIPsOrCIDRs(ipsOrCIDRs []string) NamePolicyOption { } } -func WithPermittedCIDR(cidr string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - _, nw, err := net.ParseCIDR(cidr) - if err != nil { - return fmt.Errorf("cannot parse permitted CIDR constraint %q", cidr) - } - e.permittedIPRanges = []*net.IPNet{nw} - return nil - } -} - -func WithPermittedIP(ip net.IP) NamePolicyOption { - return func(e *NamePolicyEngine) error { - nw := networkFor(ip) - e.permittedIPRanges = []*net.IPNet{nw} - return nil - } -} - -func WithExcludedIPRanges(ipRanges []*net.IPNet) NamePolicyOption { +func WithExcludedIPRanges(ipRanges ...*net.IPNet) NamePolicyOption { return func(e *NamePolicyEngine) error { e.excludedIPRanges = ipRanges return nil } } -func WithExcludedCIDR(cidr string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - _, nw, err := net.ParseCIDR(cidr) - if err != nil { - return fmt.Errorf("cannot parse excluded CIDR constraint %q", cidr) - } - e.excludedIPRanges = []*net.IPNet{nw} - return nil - } -} - -func WithExcludedIP(ip net.IP) NamePolicyOption { - return func(e *NamePolicyEngine) error { - var mask net.IPMask - if !isIPv4(ip) { - mask = net.CIDRMask(128, 128) - } else { - mask = net.CIDRMask(32, 32) - } - nw := &net.IPNet{ - IP: ip, - Mask: mask, - } - e.excludedIPRanges = []*net.IPNet{nw} - return nil - } -} - -func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { +func WithPermittedEmailAddresses(emailAddresses ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddresses := make([]string, len(emailAddresses)) for i, email := range emailAddresses { @@ -220,7 +151,7 @@ func WithPermittedEmailAddresses(emailAddresses []string) NamePolicyOption { } } -func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { +func WithExcludedEmailAddresses(emailAddresses ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedEmailAddresses := make([]string, len(emailAddresses)) for i, email := range emailAddresses { @@ -235,29 +166,7 @@ func WithExcludedEmailAddresses(emailAddresses []string) NamePolicyOption { } } -func WithPermittedEmailAddress(emailAddress string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) - if err != nil { - return fmt.Errorf("cannot parse permitted email constraint %q: %w", emailAddress, err) - } - e.permittedEmailAddresses = []string{normalizedEmailAddress} - return nil - } -} - -func WithExcludedEmailAddress(emailAddress string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(emailAddress) - if err != nil { - return fmt.Errorf("cannot parse excluded email constraint %q: %w", emailAddress, err) - } - e.excludedEmailAddresses = []string{normalizedEmailAddress} - return nil - } -} - -func WithPermittedURIDomains(uriDomains []string) NamePolicyOption { +func WithPermittedURIDomains(uriDomains ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedURIDomains := make([]string, len(uriDomains)) for i, domain := range uriDomains { @@ -272,18 +181,7 @@ func WithPermittedURIDomains(uriDomains []string) NamePolicyOption { } } -func WithPermittedURIDomain(domain string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse permitted URI domain constraint %q: %w", domain, err) - } - e.permittedURIDomains = []string{normalizedURIDomain} - return nil - } -} - -func WithExcludedURIDomains(domains []string) NamePolicyOption { +func WithExcludedURIDomains(domains ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedURIDomains := make([]string, len(domains)) for i, domain := range domains { @@ -298,18 +196,7 @@ func WithExcludedURIDomains(domains []string) NamePolicyOption { } } -func WithExcludedURIDomain(domain string) NamePolicyOption { - return func(e *NamePolicyEngine) error { - normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain) - if err != nil { - return fmt.Errorf("cannot parse excluded URI domain constraint %q: %w", domain, err) - } - e.excludedURIDomains = []string{normalizedURIDomain} - return nil - } -} - -func WithPermittedPrincipals(principals []string) NamePolicyOption { +func WithPermittedPrincipals(principals ...string) NamePolicyOption { return func(g *NamePolicyEngine) error { // TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do. g.permittedPrincipals = principals @@ -317,7 +204,7 @@ func WithPermittedPrincipals(principals []string) NamePolicyOption { } } -func WithExcludedPrincipals(principals []string) NamePolicyOption { +func WithExcludedPrincipals(principals ...string) NamePolicyOption { return func(g *NamePolicyEngine) error { // TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do. g.excludedPrincipals = principals @@ -357,7 +244,7 @@ func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) if strings.LastIndex(normalizedConstraint, "*") > 0 { return "", fmt.Errorf("domain constraint %q can only have wildcard as starting character", constraint) } - if normalizedConstraint[0] == '*' && normalizedConstraint[1] != '.' { + if len(normalizedConstraint) >= 2 && normalizedConstraint[0] == '*' && normalizedConstraint[1] != '.' { return "", fmt.Errorf("wildcard character in domain constraint %q can only be used to match (full) labels", constraint) } if strings.HasPrefix(normalizedConstraint, "*.") { diff --git a/policy/options_test.go b/policy/options_test.go index 78df3b7b..ca2908e4 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -200,7 +200,7 @@ func TestNew(t *testing.T) { "fail/with-permitted-dns-domains": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithPermittedDNSDomains([]string{"**.local"}), + WithPermittedDNSDomains("**.local"), }, want: nil, wantErr: true, @@ -209,25 +209,7 @@ func TestNew(t *testing.T) { "fail/with-excluded-dns-domains": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithExcludedDNSDomains([]string{"**.local"}), - }, - want: nil, - wantErr: true, - } - }, - "fail/with-permitted-dns-domain": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - WithPermittedDNSDomain("**.local"), - }, - want: nil, - wantErr: true, - } - }, - "fail/with-excluded-dns-domain": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - WithExcludedDNSDomain("**.local"), + WithExcludedDNSDomains("**.local"), }, want: nil, wantErr: true, @@ -236,7 +218,7 @@ func TestNew(t *testing.T) { "fail/with-permitted-cidrs": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithPermittedCIDRs([]string{"127.0.0.1//24"}), + WithPermittedCIDRs("127.0.0.1//24"), }, want: nil, wantErr: true, @@ -245,7 +227,7 @@ func TestNew(t *testing.T) { "fail/with-excluded-cidrs": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithExcludedCIDRs([]string{"127.0.0.1//24"}), + WithExcludedCIDRs("127.0.0.1//24"), }, want: nil, wantErr: true, @@ -254,7 +236,7 @@ func TestNew(t *testing.T) { "fail/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithPermittedIPsOrCIDRs([]string{"127.0.0.1//24"}), + WithPermittedIPsOrCIDRs("127.0.0.1//24"), }, want: nil, wantErr: true, @@ -263,7 +245,7 @@ func TestNew(t *testing.T) { "fail/with-permitted-ipsOrCIDRs-ip": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithPermittedIPsOrCIDRs([]string{"127.0.0:1"}), + WithPermittedIPsOrCIDRs("127.0.0:1"), }, want: nil, wantErr: true, @@ -272,7 +254,7 @@ func TestNew(t *testing.T) { "fail/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithExcludedIPsOrCIDRs([]string{"127.0.0.1//24"}), + WithExcludedIPsOrCIDRs("127.0.0.1//24"), }, want: nil, wantErr: true, @@ -281,25 +263,7 @@ func TestNew(t *testing.T) { "fail/with-excluded-ipsOrCIDRs-ip": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithExcludedIPsOrCIDRs([]string{"127.0.0:1"}), - }, - want: nil, - wantErr: true, - } - }, - "fail/with-permitted-cidr": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - WithPermittedCIDR("127.0.0.1//24"), - }, - want: nil, - wantErr: true, - } - }, - "fail/with-excluded-cidr": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - WithExcludedCIDR("127.0.0.1//24"), + WithExcludedIPsOrCIDRs("127.0.0:1"), }, want: nil, wantErr: true, @@ -308,7 +272,7 @@ func TestNew(t *testing.T) { "fail/with-permitted-emails": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithPermittedEmailAddresses([]string{"*.local"}), + WithPermittedEmailAddresses("*.local"), }, want: nil, wantErr: true, @@ -317,25 +281,7 @@ func TestNew(t *testing.T) { "fail/with-excluded-emails": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithExcludedEmailAddresses([]string{"*.local"}), - }, - want: nil, - wantErr: true, - } - }, - "fail/with-permitted-email": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - WithPermittedEmailAddress("*.local"), - }, - want: nil, - wantErr: true, - } - }, - "fail/with-excluded-email": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - WithExcludedEmailAddress("*.local"), + WithExcludedEmailAddresses("*.local"), }, want: nil, wantErr: true, @@ -344,7 +290,7 @@ func TestNew(t *testing.T) { "fail/with-permitted-uris": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithPermittedURIDomains([]string{"**.local"}), + WithPermittedURIDomains("**.local"), }, want: nil, wantErr: true, @@ -353,25 +299,7 @@ func TestNew(t *testing.T) { "fail/with-excluded-uris": func(t *testing.T) test { return test{ options: []NamePolicyOption{ - WithExcludedURIDomains([]string{"**.local"}), - }, - want: nil, - wantErr: true, - } - }, - "fail/with-permitted-uri": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - WithPermittedURIDomain("**.local"), - }, - want: nil, - wantErr: true, - } - }, - "fail/with-excluded-uri": func(t *testing.T) test { - return test{ - options: []NamePolicyOption{ - WithExcludedURIDomain("**.local"), + WithExcludedURIDomains("**.local"), }, want: nil, wantErr: true, @@ -410,7 +338,7 @@ func TestNew(t *testing.T) { }, "ok/with-permitted-dns-wildcard-domains": func(t *testing.T) test { options := []NamePolicyOption{ - WithPermittedDNSDomains([]string{"*.local", "*.example.com"}), + WithPermittedDNSDomains("*.local", "*.example.com"), } return test{ options: options, @@ -425,7 +353,7 @@ func TestNew(t *testing.T) { }, "ok/with-excluded-dns-domains": func(t *testing.T) test { options := []NamePolicyOption{ - WithExcludedDNSDomains([]string{"*.local", "*.example.com"}), + WithExcludedDNSDomains("*.local", "*.example.com"), } return test{ options: options, @@ -438,47 +366,13 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/with-permitted-dns-wildcard-domain": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedDNSDomain("*.example.com"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedDNSDomains: []string{".example.com"}, - numberOfDNSDomainConstraints: 1, - totalNumberOfPermittedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-permitted-dns-domain": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedDNSDomain("www.example.com"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedDNSDomains: []string{"www.example.com"}, - numberOfDNSDomainConstraints: 1, - totalNumberOfPermittedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, "ok/with-permitted-ip-ranges": func(t *testing.T) test { _, nw1, err := net.ParseCIDR("127.0.0.1/24") assert.FatalError(t, err) _, nw2, err := net.ParseCIDR("192.168.0.1/24") assert.FatalError(t, err) options := []NamePolicyOption{ - WithPermittedIPRanges( - []*net.IPNet{ - nw1, nw2, - }, - ), + WithPermittedIPRanges(nw1, nw2), } return test{ options: options, @@ -499,11 +393,7 @@ func TestNew(t *testing.T) { _, nw2, err := net.ParseCIDR("192.168.0.1/24") assert.FatalError(t, err) options := []NamePolicyOption{ - WithExcludedIPRanges( - []*net.IPNet{ - nw1, nw2, - }, - ), + WithExcludedIPRanges(nw1, nw2), } return test{ options: options, @@ -524,7 +414,7 @@ func TestNew(t *testing.T) { _, nw2, err := net.ParseCIDR("192.168.0.1/24") assert.FatalError(t, err) options := []NamePolicyOption{ - WithPermittedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + WithPermittedCIDRs("127.0.0.1/24", "192.168.0.1/24"), } return test{ options: options, @@ -545,7 +435,7 @@ func TestNew(t *testing.T) { _, nw2, err := net.ParseCIDR("192.168.0.1/24") assert.FatalError(t, err) options := []NamePolicyOption{ - WithExcludedCIDRs([]string{"127.0.0.1/24", "192.168.0.1/24"}), + WithExcludedCIDRs("127.0.0.1/24", "192.168.0.1/24"), } return test{ options: options, @@ -565,18 +455,20 @@ func TestNew(t *testing.T) { assert.FatalError(t, err) _, nw2, err := net.ParseCIDR("192.168.0.31/32") assert.FatalError(t, err) + _, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) options := []NamePolicyOption{ - WithPermittedIPsOrCIDRs([]string{"127.0.0.1/24", "192.168.0.31"}), + WithPermittedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"), } return test{ options: options, want: &NamePolicyEngine{ permittedIPRanges: []*net.IPNet{ - nw1, nw2, + nw1, nw2, nw3, }, - numberOfIPRangeConstraints: 2, - totalNumberOfPermittedConstraints: 2, - totalNumberOfConstraints: 2, + numberOfIPRangeConstraints: 3, + totalNumberOfPermittedConstraints: 3, + totalNumberOfConstraints: 3, }, wantErr: false, } @@ -586,139 +478,27 @@ func TestNew(t *testing.T) { assert.FatalError(t, err) _, nw2, err := net.ParseCIDR("192.168.0.31/32") assert.FatalError(t, err) + _, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") + assert.FatalError(t, err) options := []NamePolicyOption{ - WithExcludedIPsOrCIDRs([]string{"127.0.0.1/24", "192.168.0.31"}), + WithExcludedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"), } return test{ options: options, want: &NamePolicyEngine{ excludedIPRanges: []*net.IPNet{ - nw1, nw2, + nw1, nw2, nw3, }, - numberOfIPRangeConstraints: 2, - totalNumberOfExcludedConstraints: 2, - totalNumberOfConstraints: 2, - }, - wantErr: false, - } - }, - "ok/with-permitted-cidr": func(t *testing.T) test { - _, nw1, err := net.ParseCIDR("127.0.0.1/24") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithPermittedCIDR("127.0.0.1/24"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedIPRanges: []*net.IPNet{ - nw1, - }, - numberOfIPRangeConstraints: 1, - totalNumberOfPermittedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-excluded-cidr": func(t *testing.T) test { - _, nw1, err := net.ParseCIDR("127.0.0.1/24") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithExcludedCIDR("127.0.0.1/24"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedIPRanges: []*net.IPNet{ - nw1, - }, - numberOfIPRangeConstraints: 1, - totalNumberOfExcludedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-permitted-ipv4": func(t *testing.T) test { - ip1, nw1, err := net.ParseCIDR("127.0.0.15/32") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithPermittedIP(ip1), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedIPRanges: []*net.IPNet{ - nw1, - }, - numberOfIPRangeConstraints: 1, - totalNumberOfPermittedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-excluded-ipv4": func(t *testing.T) test { - ip1, nw1, err := net.ParseCIDR("127.0.0.15/32") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithExcludedIP(ip1), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedIPRanges: []*net.IPNet{ - nw1, - }, - numberOfIPRangeConstraints: 1, - totalNumberOfExcludedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-permitted-ipv6": func(t *testing.T) test { - ip1, nw1, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithPermittedIP(ip1), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedIPRanges: []*net.IPNet{ - nw1, - }, - numberOfIPRangeConstraints: 1, - totalNumberOfPermittedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-excluded-ipv6": func(t *testing.T) test { - ip1, nw1, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") - assert.FatalError(t, err) - options := []NamePolicyOption{ - WithExcludedIP(ip1), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedIPRanges: []*net.IPNet{ - nw1, - }, - numberOfIPRangeConstraints: 1, - totalNumberOfExcludedConstraints: 1, - totalNumberOfConstraints: 1, + numberOfIPRangeConstraints: 3, + totalNumberOfExcludedConstraints: 3, + totalNumberOfConstraints: 3, }, wantErr: false, } }, "ok/with-permitted-emails": func(t *testing.T) test { options := []NamePolicyOption{ - WithPermittedEmailAddresses([]string{"mail@local", "@example.com"}), + WithPermittedEmailAddresses("mail@local", "@example.com"), } return test{ options: options, @@ -733,7 +513,7 @@ func TestNew(t *testing.T) { }, "ok/with-excluded-emails": func(t *testing.T) test { options := []NamePolicyOption{ - WithExcludedEmailAddresses([]string{"mail@local", "@example.com"}), + WithExcludedEmailAddresses("mail@local", "@example.com"), } return test{ options: options, @@ -746,39 +526,9 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/with-permitted-email": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedEmailAddress("mail@local"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedEmailAddresses: []string{"mail@local"}, - numberOfEmailAddressConstraints: 1, - totalNumberOfPermittedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-excluded-email": func(t *testing.T) test { - options := []NamePolicyOption{ - WithExcludedEmailAddress("mail@local"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedEmailAddresses: []string{"mail@local"}, - numberOfEmailAddressConstraints: 1, - totalNumberOfExcludedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, "ok/with-permitted-uris": func(t *testing.T) test { options := []NamePolicyOption{ - WithPermittedURIDomains([]string{"host.local", "*.example.com"}), + WithPermittedURIDomains("host.local", "*.example.com"), } return test{ options: options, @@ -793,7 +543,7 @@ func TestNew(t *testing.T) { }, "ok/with-excluded-uris": func(t *testing.T) test { options := []NamePolicyOption{ - WithExcludedURIDomains([]string{"host.local", "*.example.com"}), + WithExcludedURIDomains("host.local", "*.example.com"), } return test{ options: options, @@ -806,54 +556,9 @@ func TestNew(t *testing.T) { wantErr: false, } }, - "ok/with-permitted-uri": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedURIDomain("host.local"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedURIDomains: []string{"host.local"}, - numberOfURIDomainConstraints: 1, - totalNumberOfPermittedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-permitted-uri-idna": func(t *testing.T) test { - options := []NamePolicyOption{ - WithPermittedURIDomain("*.bücher.example.com"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - permittedURIDomains: []string{".xn--bcher-kva.example.com"}, - numberOfURIDomainConstraints: 1, - totalNumberOfPermittedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, - "ok/with-excluded-uri": func(t *testing.T) test { - options := []NamePolicyOption{ - WithExcludedURIDomain("host.local"), - } - return test{ - options: options, - want: &NamePolicyEngine{ - excludedURIDomains: []string{"host.local"}, - numberOfURIDomainConstraints: 1, - totalNumberOfExcludedConstraints: 1, - totalNumberOfConstraints: 1, - }, - wantErr: false, - } - }, "ok/with-permitted-principals": func(t *testing.T) test { options := []NamePolicyOption{ - WithPermittedPrincipals([]string{"root", "ops"}), + WithPermittedPrincipals("root", "ops"), } return test{ options: options, @@ -868,7 +573,7 @@ func TestNew(t *testing.T) { }, "ok/with-excluded-principals": func(t *testing.T) test { options := []NamePolicyOption{ - WithExcludedPrincipals([]string{"root", "ops"}), + WithExcludedPrincipals("root", "ops"), } return test{ options: options, From fb81407d6f294a229fd833c52fcbd91bf85a8590 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 21 Apr 2022 13:21:06 +0200 Subject: [PATCH 49/78] Fix ACME policy comments --- acme/api/account_test.go | 9 ++++++ acme/api/order.go | 16 ++++++++--- acme/api/order_test.go | 49 +++++++++++++++++++++++++++----- authority/admin/api/acme.go | 8 +----- authority/admin/api/acme_test.go | 16 ++++++----- 5 files changed, 73 insertions(+), 25 deletions(-) diff --git a/acme/api/account_test.go b/acme/api/account_test.go index a457655c..e389b57f 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -65,6 +65,15 @@ func newACMEProv(t *testing.T) *provisioner.ACME { return a } +func newACMEProvWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME { + p := newProvWithOptions(options) + a, ok := p.(*provisioner.ACME) + if !ok { + t.Fatal("not a valid ACME provisioner") + } + return a +} + func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) (*jose.JSONWebSignature, error) { signer, err := jose.NewSigner( jose.SigningKey{ diff --git a/acme/api/order.go b/acme/api/order.go index 820b642f..5bf35a58 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -103,15 +103,23 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { return } - // TODO(hs): gather all errors, so that we can build one response with subproblems; include the nor.Validate() - // error here too, like in example? + // TODO(hs): gather all errors, so that we can build one response with ACME subproblems + // include the nor.Validate() error here too, like in the example in the ACME RFC? - eak, err := h.db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID) + acmeProv, err := acmeProvisionerFromContext(ctx) if err != nil { - render.Error(w, acme.WrapErrorISE(err, "error retrieving external account binding key")) + render.Error(w, err) return } + var eak *acme.ExternalAccountKey + if acmeProv.RequireEAB { + if eak, err = h.db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID); err != nil { + render.Error(w, acme.WrapErrorISE(err, "error retrieving external account binding key")) + return + } + } + acmePolicy, err := newACMEPolicyEngine(eak) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error creating ACME policy engine")) diff --git a/acme/api/order_test.go b/acme/api/order_test.go index 02034c16..35abab65 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -761,7 +761,7 @@ func TestHandler_NewOrder(t *testing.T) { err: acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty"), } }, - "fail/db.GetExternalAccountKeyByAccountID-error": func(t *testing.T) test { + "fail/acmeProvisionerFromContext-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} fr := &NewOrderRequest{ Identifiers: []acme.Identifier{ @@ -770,7 +770,35 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(fr) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := context.WithValue(context.Background(), provisionerContextKey, &acme.MockProvisioner{}) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + return test{ + ctx: ctx, + statusCode: 500, + ca: &mockCA{}, + db: &acme.MockDB{ + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, errors.New("force") + }, + }, + err: acme.NewErrorISE("error retrieving external account binding key: force"), + } + }, + "fail/db.GetExternalAccountKeyByAccountID-error": func(t *testing.T) test { + acmeProv := newACMEProv(t) + acmeProv.RequireEAB = true + acc := &acme.Account{ID: "accID"} + fr := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "dns", Value: "zap.internal"}, + }, + } + b, err := json.Marshal(fr) + assert.FatalError(t, err) + ctx := context.WithValue(context.Background(), provisionerContextKey, acmeProv) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ @@ -788,6 +816,8 @@ func TestHandler_NewOrder(t *testing.T) { } }, "fail/newACMEPolicyEngine-error": func(t *testing.T) test { + acmeProv := newACMEProv(t) + acmeProv.RequireEAB = true acc := &acme.Account{ID: "accID"} fr := &NewOrderRequest{ Identifiers: []acme.Identifier{ @@ -796,7 +826,7 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(fr) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := context.WithValue(context.Background(), provisionerContextKey, acmeProv) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ @@ -822,6 +852,8 @@ func TestHandler_NewOrder(t *testing.T) { } }, "fail/isIdentifierAllowed-error": func(t *testing.T) test { + acmeProv := newACMEProv(t) + acmeProv.RequireEAB = true acc := &acme.Account{ID: "accID"} fr := &NewOrderRequest{ Identifiers: []acme.Identifier{ @@ -830,7 +862,7 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(fr) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := context.WithValue(context.Background(), provisionerContextKey, acmeProv) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ @@ -863,7 +895,8 @@ func TestHandler_NewOrder(t *testing.T) { }, }, } - provWithPolicy := newProvWithOptions(options) + provWithPolicy := newACMEProvWithOptions(t, options) + provWithPolicy.RequireEAB = true acc := &acme.Account{ID: "accID"} fr := &NewOrderRequest{ Identifiers: []acme.Identifier{ @@ -905,7 +938,8 @@ func TestHandler_NewOrder(t *testing.T) { }, }, } - provWithPolicy := newProvWithOptions(options) + provWithPolicy := newACMEProvWithOptions(t, options) + provWithPolicy.RequireEAB = true acc := &acme.Account{ID: "accID"} fr := &NewOrderRequest{ Identifiers: []acme.Identifier{ @@ -1567,7 +1601,8 @@ func TestHandler_NewOrder(t *testing.T) { }, }, } - provWithPolicy := newProvWithOptions(options) + provWithPolicy := newACMEProvWithOptions(t, options) + provWithPolicy.RequireEAB = true acc := &acme.Account{ID: "accID"} nor := &NewOrderRequest{ Identifiers: []acme.Identifier{ diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index e11ac317..fe667181 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -38,13 +38,7 @@ func (h *Handler) requireEABEnabled(next http.HandlerFunc) http.HandlerFunc { ctx := r.Context() prov := linkedca.ProvisionerFromContext(ctx) - details := prov.GetDetails() - if details == nil { - render.Error(w, admin.NewErrorISE("error getting details for provisioner '%s'", prov.GetName())) - return - } - - acmeProvisioner := details.GetACME() + acmeProvisioner := prov.GetDetails().GetACME() if acmeProvisioner == nil { render.Error(w, admin.NewErrorISE("error getting ACME details for provisioner '%s'", prov.GetName())) return diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index e44b4e9b..aa4aa608 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -13,13 +13,15 @@ import ( "time" "github.com/go-chi/chi" - "github.com/smallstep/assert" - "github.com/smallstep/certificates/acme" - "github.com/smallstep/certificates/authority/admin" - "go.step.sm/linkedca" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" + + "go.step.sm/linkedca" + + "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/authority/admin" ) func readProtoJSON(r io.ReadCloser, m proto.Message) error { @@ -45,15 +47,15 @@ func TestHandler_requireEABEnabled(t *testing.T) { Name: "provName", } ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) - err := admin.NewErrorISE("error getting details for provisioner 'provName'") - err.Message = "error getting details for provisioner 'provName'" + err := admin.NewErrorISE("error getting ACME details for provisioner 'provName'") + err.Message = "error getting ACME details for provisioner 'provName'" return test{ ctx: ctx, err: err, statusCode: 500, } }, - "fail/details.GetACME": func(t *testing.T) test { + "fail/prov.GetDetails.GetACME": func(t *testing.T) test { prov := &linkedca.Provisioner{ Id: "provID", Name: "provName", From b72430f4ea5600b7d036cf639cd5cbaecbde520d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 21 Apr 2022 16:18:55 +0200 Subject: [PATCH 50/78] Block all APIs when using linked deployment mode --- authority/admin/api/acme.go | 38 ++++++------- authority/admin/api/middleware.go | 2 +- authority/admin/api/policy.go | 89 ++++++++++++++++++++++++++++--- ca/ca.go | 2 +- 4 files changed, 99 insertions(+), 32 deletions(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index fe667181..cd4b1e17 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -129,29 +129,21 @@ func linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey { BoundAt: k.BoundAt.AsTime(), } - if k.Policy == nil { - return eak - } - - eak.Policy = &acme.Policy{} - - if k.Policy.X509 == nil { - return eak - } - - eak.Policy.X509 = acme.X509Policy{ - Allowed: acme.PolicyNames{}, - Denied: acme.PolicyNames{}, - } - - if k.Policy.X509.Allow != nil { - eak.Policy.X509.Allowed.DNSNames = k.Policy.X509.Allow.Dns - eak.Policy.X509.Allowed.IPRanges = k.Policy.X509.Allow.Ips - } - - if k.Policy.X509.Deny != nil { - eak.Policy.X509.Denied.DNSNames = k.Policy.X509.Deny.Dns - eak.Policy.X509.Denied.IPRanges = k.Policy.X509.Deny.Ips + if policy := k.GetPolicy(); policy != nil { + eak.Policy = &acme.Policy{} + if x509 := policy.GetX509(); x509 != nil { + eak.Policy.X509 = acme.X509Policy{} + if allow := x509.GetAllow(); allow != nil { + eak.Policy.X509.Allowed = acme.PolicyNames{} + eak.Policy.X509.Allowed.DNSNames = allow.Dns + eak.Policy.X509.Allowed.IPRanges = allow.Ips + } + if deny := x509.GetDeny(); deny != nil { + eak.Policy.X509.Denied = acme.PolicyNames{} + eak.Policy.X509.Denied.DNSNames = deny.Dns + eak.Policy.X509.Denied.IPRanges = deny.Ips + } + } } return eak diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 45f46753..af3dac5d 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -103,7 +103,7 @@ func (h *Handler) checkAction(next http.HandlerFunc, supportedInStandalone bool) } // loadExternalAccountKey is a middleware that searches for an ACME -// External Account Key by accountID, keyID or reference and stores it in the context. +// External Account Key by reference or keyID and stores it in the context. func (h *Handler) loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 34b7bf96..f316ba93 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -31,23 +31,30 @@ type policyAdminResponderInterface interface { // PolicyAdminResponder is responsible for writing ACME admin responses type PolicyAdminResponder struct { - auth adminAuthority - adminDB admin.DB - acmeDB acme.DB + auth adminAuthority + adminDB admin.DB + acmeDB acme.DB + deploymentType string } // NewACMEAdminResponder returns a new ACMEAdminResponder -func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB) *PolicyAdminResponder { +func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, deploymentType string) *PolicyAdminResponder { return &PolicyAdminResponder{ - auth: auth, - adminDB: adminDB, - acmeDB: acmeDB, + auth: auth, + adminDB: adminDB, + acmeDB: acmeDB, + deploymentType: deploymentType, } } // GetAuthorityPolicy handles the GET /admin/authority/policy request func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + policy, err := par.auth.GetAuthorityPolicy(r.Context()) if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) { render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy")) @@ -65,6 +72,11 @@ func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht // CreateAuthorityPolicy handles the POST /admin/authority/policy request func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() policy, err := par.auth.GetAuthorityPolicy(ctx) @@ -111,6 +123,11 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r // UpdateAuthorityPolicy handles the PUT /admin/authority/policy request func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() policy, err := par.auth.GetAuthorityPolicy(ctx) @@ -153,6 +170,11 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r // DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() policy, err := par.auth.GetAuthorityPolicy(ctx) @@ -177,6 +199,11 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r // GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + prov := linkedca.ProvisionerFromContext(r.Context()) policy := prov.GetPolicy() @@ -191,6 +218,11 @@ func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r * // CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() prov := linkedca.ProvisionerFromContext(ctx) @@ -231,6 +263,11 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, // UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() prov := linkedca.ProvisionerFromContext(ctx) @@ -266,6 +303,11 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, // DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() prov := linkedca.ProvisionerFromContext(ctx) @@ -286,6 +328,12 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, } func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() eak := linkedca.ExternalAccountKeyFromContext(ctx) @@ -299,6 +347,12 @@ func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r * } func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() prov := linkedca.ProvisionerFromContext(ctx) eak := linkedca.ExternalAccountKeyFromContext(ctx) @@ -330,6 +384,12 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, } func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() prov := linkedca.ProvisionerFromContext(ctx) eak := linkedca.ExternalAccountKeyFromContext(ctx) @@ -359,6 +419,12 @@ func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, } func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + + if err := par.blockLinkedCA(); err != nil { + render.Error(w, err) + return + } + ctx := r.Context() prov := linkedca.ProvisionerFromContext(ctx) eak := linkedca.ExternalAccountKeyFromContext(ctx) @@ -381,6 +447,15 @@ func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) } +// blockLinkedCA blocks all API operations on linked deployments +func (par *PolicyAdminResponder) blockLinkedCA() error { + // temporary blocking linked deployments based on string comparison (preventing import cycle) + if par.deploymentType == "linked" { + return admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + } + return nil +} + // applyConditionalDefaults applies default settings in case they're not provided // in the request body. func applyConditionalDefaults(p *linkedca.Policy) { diff --git a/ca/ca.go b/ca/ca.go index e63750b5..3c60f9e5 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -219,7 +219,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { adminDB := auth.GetAdminDatabase() if adminDB != nil { acmeAdminResponder := adminAPI.NewACMEAdminResponder() - policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB, acmeDB) + policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB, acmeDB, cfg.AuthorityConfig.DeploymentType) adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder, policyAdminResponder) mux.Route("/admin", func(r chi.Router) { adminHandler.Route(r) From e9f5a1eb9850687d1de7603a3f0e88a70ffaf855 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 21 Apr 2022 17:16:02 +0200 Subject: [PATCH 51/78] Improve policy bad request handling --- authority/admin/api/policy.go | 33 ++- authority/admin/api/policy_test.go | 398 +++++++++++++++++++++-------- 2 files changed, 314 insertions(+), 117 deletions(-) diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index f316ba93..639bdbf2 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -105,11 +105,8 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r var createdPolicy *linkedca.Policy if createdPolicy, err = par.auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil { - var pe *authority.PolicyError - isPolicyError := errors.As(err, &pe) - - if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { - render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error storing authority policy")) + if isBadRequest(err) { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy")) return } @@ -153,10 +150,8 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r var updatedPolicy *linkedca.Policy if updatedPolicy, err = par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil { - var pe *authority.PolicyError - isPolicyError := errors.As(err, &pe) - if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { - render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error updating authority policy")) + if isBadRequest(err) { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy")) return } @@ -246,10 +241,8 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, prov.Policy = newPolicy if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { - var pe *authority.PolicyError - isPolicyError := errors.As(err, &pe) - if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { - render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error creating provisioner policy")) + if isBadRequest(err) { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy")) return } @@ -286,10 +279,8 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, prov.Policy = newPolicy if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { - var pe *authority.PolicyError - isPolicyError := errors.As(err, &pe) - if isPolicyError && pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure { - render.Error(w, admin.WrapError(admin.ErrorBadRequestType, pe, "error updating provisioner policy")) + if isBadRequest(err) { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy")) return } @@ -456,6 +447,14 @@ func (par *PolicyAdminResponder) blockLinkedCA() error { return nil } +// isBadRequest checks if an error should result in a bad request error +// returned to the client. +func isBadRequest(err error) bool { + var pe *authority.PolicyError + isPolicyError := errors.As(err, &pe) + return isPolicyError && (pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure) +} + // applyConditionalDefaults applies default settings in case they're not provided // in the request body. func applyConditionalDefaults(p *linkedca.Policy) { diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index 72a462a4..224ab18d 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -24,14 +24,26 @@ import ( func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { type test struct { - auth adminAuthority - adminDB admin.DB - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + deploymentType string + adminDB admin.DB + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { ctx := context.Background() err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") @@ -87,8 +99,9 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, + auth: tc.auth, + adminDB: tc.adminDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("GET", "/foo", nil) @@ -127,16 +140,28 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { type test struct { - auth adminAuthority - adminDB admin.DB - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + deploymentType string + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { ctx := context.Background() err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") @@ -320,9 +345,10 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -370,16 +396,28 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { type test struct { - auth adminAuthority - adminDB admin.DB - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + deploymentType string + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { ctx := context.Background() err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") @@ -570,9 +608,10 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -620,16 +659,28 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { type test struct { - auth adminAuthority - adminDB admin.DB - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - statusCode int + auth adminAuthority + deploymentType string + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { ctx := context.Background() err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") @@ -713,9 +764,10 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -758,15 +810,27 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { type test struct { - auth adminAuthority - adminDB admin.DB - ctx context.Context - acmeDB acme.DB - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + deploymentType string + adminDB admin.DB + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/prov-no-policy": func(t *testing.T) test { prov := &linkedca.Provisioner{} ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) @@ -801,9 +865,10 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("GET", "/foo", nil) @@ -842,14 +907,26 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { type test struct { - auth adminAuthority - body []byte - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + deploymentType string + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/existing-policy": func(t *testing.T) test { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ @@ -992,7 +1069,8 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - auth: tc.auth, + auth: tc.auth, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -1040,14 +1118,26 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { type test struct { - auth adminAuthority - body []byte - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + deploymentType string + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/no-existing-policy": func(t *testing.T) test { prov := &linkedca.Provisioner{ Name: "provName", @@ -1192,7 +1282,8 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - auth: tc.auth, + auth: tc.auth, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -1240,16 +1331,28 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { type test struct { - auth adminAuthority - adminDB admin.DB - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - statusCode int + auth adminAuthority + deploymentType string + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/no-existing-policy": func(t *testing.T) test { prov := &linkedca.Provisioner{ Name: "provName", @@ -1303,9 +1406,10 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -1348,13 +1452,25 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { type test struct { - ctx context.Context - acmeDB acme.DB - err *admin.Error - policy *linkedca.Policy - statusCode int + deploymentType string + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/no-policy": func(t *testing.T) test { prov := &linkedca.Provisioner{ Name: "provName", @@ -1400,7 +1516,8 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - acmeDB: tc.acmeDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("GET", "/foo", nil) @@ -1439,14 +1556,26 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { type test struct { - acmeDB acme.DB - body []byte - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + deploymentType string + acmeDB acme.DB + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/existing-policy": func(t *testing.T) test { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ @@ -1564,7 +1693,8 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - acmeDB: tc.acmeDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -1612,14 +1742,26 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { type test struct { - acmeDB acme.DB - body []byte - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + deploymentType string + acmeDB acme.DB + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/no-existing-policy": func(t *testing.T) test { prov := &linkedca.Provisioner{ Name: "provName", @@ -1739,7 +1881,8 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - acmeDB: tc.acmeDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -1787,14 +1930,26 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { type test struct { - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - statusCode int + deploymentType string + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int } var tests = map[string]func(t *testing.T) test{ + "fail/linkedca": func(t *testing.T) test { + ctx := context.Background() + err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") + err.Message = "policy operations not yet supported in linked deployments" + return test{ + ctx: ctx, + deploymentType: "linked", + err: err, + statusCode: 501, + } + }, "fail/no-existing-policy": func(t *testing.T) test { prov := &linkedca.Provisioner{ Name: "provName", @@ -1880,7 +2035,8 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { tc := prep(t) t.Run(name, func(t *testing.T) { par := &PolicyAdminResponder{ - acmeDB: tc.acmeDB, + acmeDB: tc.acmeDB, + deploymentType: tc.deploymentType, } req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) @@ -2000,3 +2156,45 @@ func Test_applyConditionalDefaults(t *testing.T) { }) } } + +func Test_isBadRequest(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + { + name: "nil", + err: nil, + want: false, + }, + { + name: "no-policy-error", + err: errors.New("some error"), + want: false, + }, + { + name: "no-bad-request", + err: &authority.PolicyError{ + Typ: authority.InternalFailure, + Err: errors.New("error"), + }, + want: false, + }, + { + name: "bad-request", + err: &authority.PolicyError{ + Typ: authority.AdminLockOut, + Err: errors.New("admin lock out"), + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isBadRequest(tt.err); got != tt.want { + t.Errorf("isBadRequest() = %v, want %v", got, tt.want) + } + }) + } +} From ef110a94df7fce0b2a52277c6448b379cb4d940a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 21 Apr 2022 23:45:05 +0200 Subject: [PATCH 52/78] Change pointer booleans to regular boolean configuration --- authority/admin/api/policy.go | 16 ------ authority/admin/api/policy_test.go | 85 +----------------------------- authority/policy.go | 9 ++-- authority/policy/options.go | 19 +++---- authority/policy/options_test.go | 32 +++++------ authority/policy_test.go | 36 ++++++------- 6 files changed, 43 insertions(+), 154 deletions(-) diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 639bdbf2..c697b67a 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -5,7 +5,6 @@ import ( "net/http" "go.step.sm/linkedca" - "google.golang.org/protobuf/types/known/wrapperspb" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api/read" @@ -97,8 +96,6 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r return } - applyConditionalDefaults(newPolicy) - newPolicy.Deduplicate() adm := linkedca.AdminFromContext(ctx) @@ -234,8 +231,6 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, return } - applyConditionalDefaults(newPolicy) - newPolicy.Deduplicate() prov.Policy = newPolicy @@ -454,14 +449,3 @@ func isBadRequest(err error) bool { isPolicyError := errors.As(err, &pe) return isPolicyError && (pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure) } - -// applyConditionalDefaults applies default settings in case they're not provided -// in the request body. -func applyConditionalDefaults(p *linkedca.Policy) { - if p.GetX509() == nil { - return - } - if p.GetX509().GetVerifySubjectCommonName() == nil { - p.X509.VerifySubjectCommonName = &wrapperspb.BoolValue{Value: true} - } -} diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index 224ab18d..ee97c2cc 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -12,7 +12,6 @@ import ( "testing" "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/types/known/wrapperspb" "go.step.sm/linkedca" @@ -310,7 +309,7 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + DisableSubjectCommonNameVerification: false, }, } body, err := protojson.Marshal(policy) @@ -1047,7 +1046,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + DisableSubjectCommonNameVerification: false, }, } body, err := protojson.Marshal(policy) @@ -2077,86 +2076,6 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { } } -func Test_applyConditionalDefaults(t *testing.T) { - tests := []struct { - name string - policy *linkedca.Policy - expected *linkedca.Policy - }{ - { - name: "no-x509", - policy: &linkedca.Policy{ - Ssh: &linkedca.SSHPolicy{}, - }, - expected: &linkedca.Policy{ - Ssh: &linkedca.SSHPolicy{}, - }, - }, - { - name: "with-x509-verify-subject-common-name", - policy: &linkedca.Policy{ - X509: &linkedca.X509Policy{ - Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, - }, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, - }, - }, - expected: &linkedca.Policy{ - X509: &linkedca.X509Policy{ - Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, - }, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, - }, - }, - }, - { - name: "without-x509-verify-subject-common-name", - policy: &linkedca.Policy{ - X509: &linkedca.X509Policy{ - Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, - }, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: false}, - }, - }, - expected: &linkedca.Policy{ - X509: &linkedca.X509Policy{ - Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, - }, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: false}, - }, - }, - }, - { - name: "no-x509-verify-subject-common-name", - policy: &linkedca.Policy{ - X509: &linkedca.X509Policy{ - Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, - }, - }, - }, - expected: &linkedca.Policy{ - X509: &linkedca.X509Policy{ - Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, - }, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - applyConditionalDefaults(tt.policy) - assert.Equals(t, tt.expected, tt.policy) - }) - } -} - func Test_isBadRequest(t *testing.T) { tests := []struct { name string diff --git a/authority/policy.go b/authority/policy.go index dd24ecf7..d0da3634 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -350,12 +350,9 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { opts.X509.DeniedNames.URIDomains = deny.Uris } } - if v := x509.GetAllowWildcardLiteral(); v != nil { - opts.X509.AllowWildcardLiteral = &v.Value - } - if v := x509.GetVerifySubjectCommonName(); v != nil { - opts.X509.VerifySubjectCommonName = &v.Value - } + + opts.X509.AllowWildcardLiteral = x509.AllowWildcardLiteral + opts.X509.DisableSubjectCommonNameVerification = x509.DisableSubjectCommonNameVerification } // fill ssh policy configuration diff --git a/authority/policy/options.go b/authority/policy/options.go index 68efe45a..ff0eec3d 100644 --- a/authority/policy/options.go +++ b/authority/policy/options.go @@ -44,10 +44,10 @@ type X509PolicyOptions struct { // AllowWildcardLiteral indicates if literal wildcard names // such as *.example.com and @example.com are allowed. Defaults // to false. - AllowWildcardLiteral *bool `json:"allow_wildcard_literal,omitempty"` - // VerifySubjectCommonName indicates if the Subject Common Name - // is verified in addition to the SANs. Defaults to true. - VerifySubjectCommonName *bool `json:"verify_subject_common_name,omitempty"` + AllowWildcardLiteral bool `json:"allow_wildcard_literal,omitempty"` + // DisableSubjectCommonNameVerification indicates if the Subject Common Name + // is verified in addition to the SANs. Defaults to false. + DisableSubjectCommonNameVerification bool `json:"disable_subject_common_name_verification,omitempty"` } // X509NameOptions models the X509 name policy configuration. @@ -83,21 +83,22 @@ func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { return o.DeniedNames } +// IsWildcardLiteralAllowed returns whether the authority allows +// literal wildcard domains and other names to be signed. func (o *X509PolicyOptions) IsWildcardLiteralAllowed() bool { if o == nil { return true } - return o.AllowWildcardLiteral != nil && *o.AllowWildcardLiteral + return o.AllowWildcardLiteral } +// ShouldVerifySubjectCommonName returns whether the authority +// should verify the Subject Common Name in addition to the SANs. func (o *X509PolicyOptions) ShouldVerifySubjectCommonName() bool { if o == nil { return false } - if o.VerifySubjectCommonName == nil { - return true - } - return *o.VerifySubjectCommonName + return !o.DisableSubjectCommonNameVerification } // SSHPolicyOptionsInterface is an interface for providers of diff --git a/authority/policy/options_test.go b/authority/policy/options_test.go index 49330f08..ebcd90fe 100644 --- a/authority/policy/options_test.go +++ b/authority/policy/options_test.go @@ -5,8 +5,6 @@ import ( ) func TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) { - trueValue := true - falseValue := false tests := []struct { name string options *X509PolicyOptions @@ -18,23 +16,21 @@ func TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) { want: true, }, { - name: "nil", - options: &X509PolicyOptions{ - AllowWildcardLiteral: nil, - }, - want: false, + name: "not-set", + options: &X509PolicyOptions{}, + want: false, }, { name: "set-true", options: &X509PolicyOptions{ - AllowWildcardLiteral: &trueValue, + AllowWildcardLiteral: true, }, want: true, }, { name: "set-false", options: &X509PolicyOptions{ - AllowWildcardLiteral: &falseValue, + AllowWildcardLiteral: false, }, want: false, }, @@ -49,8 +45,6 @@ func TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) { } func TestX509PolicyOptions_ShouldVerifySubjectCommonName(t *testing.T) { - trueValue := true - falseValue := false tests := []struct { name string options *X509PolicyOptions @@ -62,25 +56,23 @@ func TestX509PolicyOptions_ShouldVerifySubjectCommonName(t *testing.T) { want: false, }, { - name: "nil", - options: &X509PolicyOptions{ - VerifySubjectCommonName: nil, - }, - want: true, + name: "not-set", + options: &X509PolicyOptions{}, + want: true, }, { name: "set-true", options: &X509PolicyOptions{ - VerifySubjectCommonName: &trueValue, + DisableSubjectCommonNameVerification: true, }, - want: true, + want: false, }, { name: "set-false", options: &X509PolicyOptions{ - VerifySubjectCommonName: &falseValue, + DisableSubjectCommonNameVerification: false, }, - want: false, + want: true, }, } for _, tt := range tests { diff --git a/authority/policy_test.go b/authority/policy_test.go index 514a7a51..bc121a79 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -7,7 +7,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/types/known/wrapperspb" "go.step.sm/linkedca" @@ -193,8 +192,6 @@ func TestAuthority_checkPolicy(t *testing.T) { } func Test_policyToCertificates(t *testing.T) { - trueValue := true - falseValue := false tests := []struct { name string policy *linkedca.Policy @@ -217,8 +214,8 @@ func Test_policyToCertificates(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, - AllowWildcardLiteral: &wrapperspb.BoolValue{Value: false}, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + AllowWildcardLiteral: false, + DisableSubjectCommonNameVerification: false, }, }, want: &policy.Options{ @@ -226,8 +223,8 @@ func Test_policyToCertificates(t *testing.T) { AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.local"}, }, - AllowWildcardLiteral: &falseValue, - VerifySubjectCommonName: &trueValue, + AllowWildcardLiteral: false, + DisableSubjectCommonNameVerification: false, }, }, }, @@ -247,8 +244,8 @@ func Test_policyToCertificates(t *testing.T) { Emails: []string{"badhost.example.com"}, Uris: []string{"https://badhost.local"}, }, - AllowWildcardLiteral: &wrapperspb.BoolValue{Value: true}, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + AllowWildcardLiteral: true, + DisableSubjectCommonNameVerification: false, }, Ssh: &linkedca.SSHPolicy{ Host: &linkedca.SSHHostPolicy{ @@ -289,8 +286,8 @@ func Test_policyToCertificates(t *testing.T) { EmailAddresses: []string{"badhost.example.com"}, URIDomains: []string{"https://badhost.local"}, }, - AllowWildcardLiteral: &trueValue, - VerifySubjectCommonName: &trueValue, + AllowWildcardLiteral: true, + DisableSubjectCommonNameVerification: false, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -335,7 +332,6 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { sshUserPolicy bool sshHostPolicy bool } - trueValue := true tests := []struct { name string config *config.Config @@ -517,8 +513,8 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: &trueValue, - VerifySubjectCommonName: &trueValue, + AllowWildcardLiteral: true, + DisableSubjectCommonNameVerification: false, }, }, }, @@ -637,8 +633,8 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: &trueValue, - VerifySubjectCommonName: &trueValue, + AllowWildcardLiteral: true, + DisableSubjectCommonNameVerification: false, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -770,8 +766,8 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Deny: &linkedca.X509Names{ Dns: []string{"badhost.local"}, }, - AllowWildcardLiteral: &wrapperspb.BoolValue{Value: true}, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + AllowWildcardLiteral: true, + DisableSubjectCommonNameVerification: false, }, Ssh: &linkedca.SSHPolicy{ Host: &linkedca.SSHHostPolicy{ @@ -835,8 +831,8 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Deny: &linkedca.X509Names{ Dns: []string{"badhost.local"}, }, - AllowWildcardLiteral: &wrapperspb.BoolValue{Value: true}, - VerifySubjectCommonName: &wrapperspb.BoolValue{Value: true}, + AllowWildcardLiteral: true, + DisableSubjectCommonNameVerification: false, }, }, nil }, From c40a4d269480c3be7af5408d39ef58b0b58e838c Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 22 Apr 2022 01:20:38 +0200 Subject: [PATCH 53/78] Contain policy engines inside provisioner Controller --- authority/policy.go | 6 ++- authority/provisioner/acme.go | 22 +++----- authority/provisioner/aws.go | 23 +++----- authority/provisioner/azure.go | 19 ++----- authority/provisioner/controller.go | 15 +++++- authority/provisioner/controller_test.go | 68 +++++++++++++++++++++--- authority/provisioner/gcp.go | 23 +++----- authority/provisioner/jwk.go | 41 ++++---------- authority/provisioner/k8sSA.go | 29 ++-------- authority/provisioner/nebula.go | 39 +++++--------- authority/provisioner/oidc.go | 25 ++------- authority/provisioner/policy.go | 65 ++++++++++++++++++++++ authority/provisioner/scep.go | 12 +---- authority/provisioner/sshpop.go | 10 ++-- authority/provisioner/utils_test.go | 18 +++---- authority/provisioner/x5c.go | 45 +++++----------- 16 files changed, 232 insertions(+), 228 deletions(-) create mode 100644 authority/provisioner/policy.go diff --git a/authority/policy.go b/authority/policy.go index d0da3634..9bcbd044 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -7,6 +7,7 @@ import ( "go.step.sm/linkedca" + "github.com/smallstep/certificates/authority/admin" authPolicy "github.com/smallstep/certificates/authority/policy" policy "github.com/smallstep/certificates/policy" ) @@ -228,7 +229,10 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) if err != nil { - return fmt.Errorf("error getting policy to (re)load policy engines: %w", err) + var ae *admin.Error + if isAdminError := errors.As(err, &ae); (isAdminError && ae.Type != admin.ErrorNotFoundType.String()) || !isAdminError { + return fmt.Errorf("error getting policy to (re)load policy engines: %w", err) + } } policyOptions = policyToCertificates(linkedPolicy) } else { diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 219176fd..790c6bb1 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -8,8 +8,6 @@ import ( "time" "github.com/pkg/errors" - - "github.com/smallstep/certificates/authority/policy" ) // ACME is the acme provisioner type, an entity that can authorize the ACME @@ -28,8 +26,7 @@ type ACME struct { Claims *Claims `json:"claims,omitempty"` Options *Options `json:"options,omitempty"` - ctl *Controller - x509Policy policy.X509Policy + ctl *Controller } // GetID returns the provisioner unique identifier. @@ -86,12 +83,7 @@ func (p *ACME) Init(config Config) (err error) { return errors.New("provisioner name cannot be empty") } - // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { - return err - } - - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -115,8 +107,10 @@ type ACMEIdentifier struct { // certificate for an ACME Order Identifier. func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIdentifier) error { + x509Policy := p.ctl.GetPolicy().GetX509() + // identifier is allowed if no policy is configured - if p.x509Policy == nil { + if x509Policy == nil { return nil } @@ -124,9 +118,9 @@ func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIden var err error switch identifier.Type { case IP: - _, err = p.x509Policy.IsIPAllowed(net.ParseIP(identifier.Value)) + _, err = x509Policy.IsIPAllowed(net.ParseIP(identifier.Value)) case DNS: - _, err = p.x509Policy.IsDNSAllowed(identifier.Value) + _, err = x509Policy.IsDNSAllowed(identifier.Value) default: err = fmt.Errorf("invalid ACME identifier type '%s' provided", identifier.Type) } @@ -147,7 +141,7 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509Policy), + newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), } return opts, nil diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 857207e6..ea69269f 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -17,11 +17,12 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/authority/policy" - "github.com/smallstep/certificates/errs" + "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + + "github.com/smallstep/certificates/errs" ) // awsIssuer is the string used as issuer in the generated tokens. @@ -267,8 +268,6 @@ type AWS struct { Options *Options `json:"options,omitempty"` config *awsConfig ctl *Controller - x509Policy policy.X509Policy - sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. @@ -423,18 +422,8 @@ func (p *AWS) Init(config Config) (err error) { } } - // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - config.Audiences = config.Audiences.WithFragment(p.GetIDForToken()) - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -489,7 +478,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, commonNameValidator(payload.Claims.Subject), newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509Policy), + newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), ), nil } @@ -769,6 +758,6 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, nil), + newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), nil), ), nil } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 8f8a7edf..48366430 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -18,7 +18,6 @@ import ( "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" - "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" ) @@ -103,8 +102,6 @@ type Azure struct { oidcConfig openIDConfiguration keyStore *keyStore ctl *Controller - x509Policy policy.X509Policy - sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. @@ -224,17 +221,7 @@ func (p *Azure) Init(config Config) (err error) { return } - // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -375,7 +362,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // validators defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509Policy), + newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), ), nil } @@ -442,7 +429,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, nil), + newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), nil), ), nil } diff --git a/authority/provisioner/controller.go b/authority/provisioner/controller.go index afd28dcc..83de4a83 100644 --- a/authority/provisioner/controller.go +++ b/authority/provisioner/controller.go @@ -21,14 +21,19 @@ type Controller struct { IdentityFunc GetIdentityFunc AuthorizeRenewFunc AuthorizeRenewFunc AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc + policy *policyEngine } // NewController initializes a new provisioner controller. -func NewController(p Interface, claims *Claims, config Config) (*Controller, error) { +func NewController(p Interface, claims *Claims, config Config, options *Options) (*Controller, error) { claimer, err := NewClaimer(claims, config.Claims) if err != nil { return nil, err } + policy, err := newPolicyEngine(options) + if err != nil { + return nil, err + } return &Controller{ Interface: p, Audiences: &config.Audiences, @@ -36,6 +41,7 @@ func NewController(p Interface, claims *Claims, config Config) (*Controller, err IdentityFunc: config.GetIdentityFunc, AuthorizeRenewFunc: config.AuthorizeRenewFunc, AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc, + policy: policy, }, nil } @@ -192,3 +198,10 @@ func SanitizeSSHUserPrincipal(email string) string { } }, strings.ToLower(email)) } + +func (c *Controller) GetPolicy() *policyEngine { + if c == nil { + return nil + } + return c.policy +} diff --git a/authority/provisioner/controller_test.go b/authority/provisioner/controller_test.go index ebd38df1..37cbfd89 100644 --- a/authority/provisioner/controller_test.go +++ b/authority/provisioner/controller_test.go @@ -9,6 +9,8 @@ import ( "time" "golang.org/x/crypto/ssh" + + "github.com/smallstep/certificates/authority/policy" ) var trueValue = true @@ -30,11 +32,40 @@ func mustDuration(t *testing.T, s string) *Duration { return d } +func mustNewPolicyEngine(t *testing.T, options *Options) *policyEngine { + t.Helper() + c, err := newPolicyEngine(options) + if err != nil { + t.Fatal(err) + } + return c +} + func TestNewController(t *testing.T) { + options := &Options{ + X509: &X509Options{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + }, + SSH: &SSHOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + EmailAddresses: []string{"@example.com"}, + }, + }, + }, + } type args struct { - p Interface - claims *Claims - config Config + p Interface + claims *Claims + config Config + options *Options } tests := []struct { name string @@ -45,7 +76,7 @@ func TestNewController(t *testing.T) { {"ok", args{&JWK{}, nil, Config{ Claims: globalProvisionerClaims, Audiences: testAudiences, - }}, &Controller{ + }, nil}, &Controller{ Interface: &JWK{}, Audiences: &testAudiences, Claimer: mustClaimer(t, nil, globalProvisionerClaims), @@ -55,24 +86,49 @@ func TestNewController(t *testing.T) { }, Config{ Claims: globalProvisionerClaims, Audiences: testAudiences, - }}, &Controller{ + }, nil}, &Controller{ Interface: &JWK{}, Audiences: &testAudiences, Claimer: mustClaimer(t, &Claims{ DisableRenewal: &defaultDisableRenewal, }, globalProvisionerClaims), }, false}, + {"ok with claims and options", args{&JWK{}, &Claims{ + DisableRenewal: &defaultDisableRenewal, + }, Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + }, options}, &Controller{ + Interface: &JWK{}, + Audiences: &testAudiences, + Claimer: mustClaimer(t, &Claims{ + DisableRenewal: &defaultDisableRenewal, + }, globalProvisionerClaims), + policy: mustNewPolicyEngine(t, options), + }, false}, {"fail claimer", args{&JWK{}, &Claims{ MinTLSDur: mustDuration(t, "24h"), MaxTLSDur: mustDuration(t, "2h"), }, Config{ Claims: globalProvisionerClaims, Audiences: testAudiences, + }, nil}, nil, true}, + {"fail options", args{&JWK{}, &Claims{ + DisableRenewal: &defaultDisableRenewal, + }, Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + }, &Options{ + X509: &X509Options{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"**.local"}, + }, + }, }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NewController(tt.args.p, tt.args.claims, tt.args.config) + got, err := NewController(tt.args.p, tt.args.claims, tt.args.config, tt.args.options) if (err != nil) != tt.wantErr { t.Errorf("NewController() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 47a46004..29c9637c 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -14,11 +14,12 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/authority/policy" - "github.com/smallstep/certificates/errs" + "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + + "github.com/smallstep/certificates/errs" ) // gcpCertsURL is the url that serves Google OAuth2 public keys. @@ -92,8 +93,6 @@ type GCP struct { config *gcpConfig keyStore *keyStore ctl *Controller - x509Policy policy.X509Policy - sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. The name should uniquely @@ -214,18 +213,8 @@ func (p *GCP) Init(config Config) (err error) { return } - // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - config.Audiences = config.Audiences.WithFragment(p.GetIDForToken()) - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -283,7 +272,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // validators defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509Policy), + newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), ), nil } @@ -447,6 +436,6 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, nil), + newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), nil), ), nil } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 2dd97e31..30b78f56 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -12,7 +12,6 @@ import ( "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" - "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" ) @@ -31,17 +30,14 @@ type stepPayload struct { // signature requests. type JWK struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Key *jose.JSONWebKey `json:"key"` - EncryptedKey string `json:"encryptedKey,omitempty"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - ctl *Controller - x509Policy policy.X509Policy - sshHostPolicy policy.HostPolicy - sshUserPolicy policy.UserPolicy + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Key *jose.JSONWebKey `json:"key"` + EncryptedKey string `json:"encryptedKey,omitempty"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + ctl *Controller } // GetID returns the provisioner unique identifier. The name and credential id @@ -103,22 +99,7 @@ func (p *JWK) Init(config Config) (err error) { return errors.New("provisioner key cannot be empty") } - // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -202,7 +183,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, defaultSANsValidator(claims.SANs), newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509Policy), + newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), }, nil } @@ -285,7 +266,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), + newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), p.ctl.GetPolicy().GetSSHUser()), ), nil } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 39d65e95..9d88327b 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -16,7 +16,6 @@ import ( "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" - "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" ) @@ -52,11 +51,8 @@ type K8sSA struct { Claims *Claims `json:"claims,omitempty"` Options *Options `json:"options,omitempty"` //kauthn kauthn.AuthenticationV1Interface - pubKeys []interface{} - ctl *Controller - x509Policy policy.X509Policy - sshHostPolicy policy.HostPolicy - sshUserPolicy policy.UserPolicy + pubKeys []interface{} + ctl *Controller } // GetID returns the provisioner unique identifier. The name and credential id @@ -144,22 +140,7 @@ func (p *K8sSA) Init(config Config) (err error) { p.kauthn = k8s.AuthenticationV1() */ - // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -261,7 +242,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // validators defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509Policy), + newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), }, nil } @@ -305,7 +286,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), + newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), p.ctl.GetPolicy().GetSSHUser()), ), nil } diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index 131ac11b..d5d76e83 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -10,13 +10,14 @@ import ( "github.com/pkg/errors" nebula "github.com/slackhq/nebula/cert" - "github.com/smallstep/certificates/authority/policy" - "github.com/smallstep/certificates/errs" + "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x25519" "go.step.sm/crypto/x509util" "golang.org/x/crypto/ssh" + + "github.com/smallstep/certificates/errs" ) const ( @@ -35,16 +36,14 @@ const ( // https://signal.org/docs/specifications/xeddsa/#xeddsa and implemented by // go.step.sm/crypto/x25519. type Nebula struct { - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Roots []byte `json:"roots"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - caPool *nebula.NebulaCAPool - ctl *Controller - x509Policy policy.X509Policy - sshHostPolicy policy.HostPolicy + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Roots []byte `json:"roots"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + caPool *nebula.NebulaCAPool + ctl *Controller } // Init verifies and initializes the Nebula provisioner. @@ -63,18 +62,8 @@ func (p *Nebula) Init(config Config) (err error) { return errs.InternalServer("failed to create ca pool: %v", err) } - // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - config.Audiences = config.Audiences.WithFragment(p.GetIDForToken()) - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -174,7 +163,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption, }, defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509Policy), + newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), }, nil } @@ -271,7 +260,7 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, nil), + newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), nil), ), nil } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index b668701b..f1b67e77 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -17,7 +17,6 @@ import ( "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" - "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" ) @@ -96,9 +95,6 @@ type OIDC struct { configuration openIDConfiguration keyStore *keyStore ctl *Controller - x509Policy policy.X509Policy - sshHostPolicy policy.HostPolicy - sshUserPolicy policy.UserPolicy } func sanitizeEmail(email string) string { @@ -201,22 +197,7 @@ func (o *OIDC) Init(config Config) (err error) { return err } - // Initialize the x509 allow/deny policy engine - if o.x509Policy, err = policy.NewX509PolicyEngine(o.Options.GetX509Options()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for user certificates - if o.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(o.Options.GetSSHOptions()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for host certificates - if o.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(o.Options.GetSSHOptions()); err != nil { - return err - } - - o.ctl, err = NewController(o, o.Claims, config) + o.ctl, err = NewController(o, o.Claims, config, o.Options) return } @@ -374,7 +355,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators defaultPublicKeyValidator{}, newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(o.x509Policy), + newX509NamePolicyValidator(o.ctl.GetPolicy().GetX509()), }, nil } @@ -462,7 +443,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(o.sshHostPolicy, o.sshUserPolicy), + newSSHNamePolicyValidator(o.ctl.GetPolicy().GetSSHHost(), o.ctl.GetPolicy().GetSSHUser()), ), nil } diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go new file mode 100644 index 00000000..52a59c97 --- /dev/null +++ b/authority/provisioner/policy.go @@ -0,0 +1,65 @@ +package provisioner + +import "github.com/smallstep/certificates/authority/policy" + +type policyEngine struct { + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy +} + +func newPolicyEngine(options *Options) (*policyEngine, error) { + + if options == nil { + return nil, nil + } + + var ( + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy + err error + ) + + // Initialize the x509 allow/deny policy engine + if x509Policy, err = policy.NewX509PolicyEngine(options.GetX509Options()); err != nil { + return nil, err + } + + // Initialize the SSH allow/deny policy engine for host certificates + if sshHostPolicy, err = policy.NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil { + return nil, err + } + + // Initialize the SSH allow/deny policy engine for user certificates + if sshUserPolicy, err = policy.NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil { + return nil, err + } + + return &policyEngine{ + x509Policy: x509Policy, + sshHostPolicy: sshHostPolicy, + sshUserPolicy: sshUserPolicy, + }, nil +} + +func (p *policyEngine) GetX509() policy.X509Policy { + if p == nil { + return nil + } + return p.x509Policy +} + +func (p *policyEngine) GetSSHHost() policy.HostPolicy { + if p == nil { + return nil + } + return p.sshHostPolicy +} + +func (p *policyEngine) GetSSHUser() policy.UserPolicy { + if p == nil { + return nil + } + return p.sshUserPolicy +} diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index 892e5fb9..6d7bb699 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -5,8 +5,6 @@ import ( "time" "github.com/pkg/errors" - - "github.com/smallstep/certificates/authority/policy" ) // SCEP is the SCEP provisioner type, an entity that can authorize the @@ -36,7 +34,6 @@ type SCEP struct { ctl *Controller secretChallengePassword string encryptionAlgorithm int - x509Policy policy.X509Policy } // GetID returns the provisioner unique identifier. @@ -113,12 +110,7 @@ func (s *SCEP) Init(config Config) (err error) { // TODO: add other, SCEP specific, options? - // Initialize the x509 allow/deny policy engine - if s.x509Policy, err = policy.NewX509PolicyEngine(s.Options.GetX509Options()); err != nil { - return err - } - - s.ctl, err = NewController(s, s.Claims, config) + s.ctl, err = NewController(s, s.Claims, config, s.Options) return } @@ -135,7 +127,7 @@ func (s *SCEP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength), newValidityValidator(s.ctl.Claimer.MinTLSCertDuration(), s.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(s.x509Policy), + newX509NamePolicyValidator(s.ctl.GetPolicy().GetX509()), }, nil } diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index e8bcce7e..c3a1a639 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -8,9 +8,11 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/errs" - "go.step.sm/crypto/jose" "golang.org/x/crypto/ssh" + + "go.step.sm/crypto/jose" + + "github.com/smallstep/certificates/errs" ) // sshPOPPayload extends jwt.Claims with step attributes. @@ -92,12 +94,10 @@ func (p *SSHPOP) Init(config Config) (err error) { return errors.New("provisioner public SSH validation keys cannot be empty") } - // TODO(hs): initialize the policy engine and add it as an SSH cert validator - p.sshPubKeys = config.SSHKeys config.Audiences = config.Audiences.WithFragment(p.GetIDForToken()) - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, nil) return } diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 3d032ea0..0a1d176c 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -184,7 +184,7 @@ func generateJWK() (*JWK, error) { } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences, - }) + }, nil) return p, err } @@ -219,7 +219,7 @@ func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) { } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences, - }) + }, nil) return p, err } @@ -256,7 +256,7 @@ func generateSSHPOP() (*SSHPOP, error) { } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences, - }) + }, nil) return p, err } @@ -305,7 +305,7 @@ M46l92gdOozT } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences, - }) + }, nil) return p, err } @@ -343,7 +343,7 @@ func generateOIDC() (*OIDC, error) { } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences, - }) + }, nil) return p, err } @@ -373,7 +373,7 @@ func generateGCP() (*GCP, error) { } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences.WithFragment("gcp/" + name), - }) + }, nil) return p, err } @@ -411,7 +411,7 @@ func generateAWS() (*AWS, error) { } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences.WithFragment("aws/" + name), - }) + }, nil) return p, err } @@ -518,7 +518,7 @@ func generateAWSV1Only() (*AWS, error) { } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences.WithFragment("aws/" + name), - }) + }, nil) return p, err } @@ -608,7 +608,7 @@ func generateAzure() (*Azure, error) { } p.ctl, err = NewController(p, p.Claims, Config{ Audiences: testAudiences, - }) + }, nil) return p, err } diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 73785103..f040d802 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -8,11 +8,12 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/authority/policy" - "github.com/smallstep/certificates/errs" + "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + + "github.com/smallstep/certificates/errs" ) // x5cPayload extends jwt.Claims with step attributes. @@ -27,17 +28,14 @@ type x5cPayload struct { // signature requests. type X5C struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Roots []byte `json:"roots"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - ctl *Controller - rootPool *x509.CertPool - x509Policy policy.X509Policy - sshHostPolicy policy.HostPolicy - sshUserPolicy policy.UserPolicy + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Roots []byte `json:"roots"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + ctl *Controller + rootPool *x509.CertPool } // GetID returns the provisioner unique identifier. The name and credential id @@ -124,23 +122,8 @@ func (p *X5C) Init(config Config) (err error) { return errors.Errorf("no x509 certificates found in roots attribute for provisioner '%s'", p.GetName()) } - // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - - // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { - return err - } - config.Audiences = config.Audiences.WithFragment(p.GetIDForToken()) - p.ctl, err = NewController(p, p.Claims, config) + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -252,7 +235,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultSANsValidator(claims.SANs), defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.x509Policy), + newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), }, nil } @@ -338,6 +321,6 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy), + newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), p.ctl.GetPolicy().GetSSHUser()), ), nil } From 221ced5c51204f60511980ca7f9079eeeb7450c0 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sat, 23 Apr 2022 10:49:33 +0200 Subject: [PATCH 54/78] add Dockerfile for building with HSM support --- docker/Dockerfile.step-ca.hsm | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docker/Dockerfile.step-ca.hsm diff --git a/docker/Dockerfile.step-ca.hsm b/docker/Dockerfile.step-ca.hsm new file mode 100644 index 00000000..29876659 --- /dev/null +++ b/docker/Dockerfile.step-ca.hsm @@ -0,0 +1,33 @@ +FROM golang:alpine AS builder + +WORKDIR /src +COPY . . + +RUN apk add --no-cache curl git make +RUN apk add --no-cache gcc musl-dev pkgconf pcsc-lite-dev +RUN make V=1 GOFLAGS="" build + +FROM smallstep/step-cli:latest + +COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca +COPY --from=builder /src/bin/step-awskms-init /usr/local/bin/step-awskms-init +COPY --from=builder /src/bin/step-cloudkms-init /usr/local/bin/step-cloudkms-init +COPY --from=builder /src/bin/step-pkcs11-init /usr/local/bin/step-pkcs11-init +COPY --from=builder /src/bin/step-yubikey-init /usr/local/bin/step-yubikey-init + +USER root +RUN apk add --no-cache libcap && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca +RUN apk add --no-cache pcsc-lite-libs +USER step + +ENV CONFIGPATH="/home/step/config/ca.json" +ENV PWDPATH="/home/step/secrets/password" + +VOLUME ["/home/step"] +STOPSIGNAL SIGTERM +HEALTHCHECK CMD step ca health 2>/dev/null | grep "^ok" >/dev/null + +COPY docker/entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] +CMD exec /usr/local/bin/step-ca --password-file $PWDPATH $CONFIGPATH From 6ee48ca63198d059d3e6fb17b6bd755d6ad29c69 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sun, 24 Apr 2022 10:59:26 +0200 Subject: [PATCH 55/78] add pcsc-lite --- docker/Dockerfile.step-ca.hsm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.step-ca.hsm b/docker/Dockerfile.step-ca.hsm index 29876659..4b870d18 100644 --- a/docker/Dockerfile.step-ca.hsm +++ b/docker/Dockerfile.step-ca.hsm @@ -17,7 +17,7 @@ COPY --from=builder /src/bin/step-yubikey-init /usr/local/bin/step-yubikey-init USER root RUN apk add --no-cache libcap && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca -RUN apk add --no-cache pcsc-lite-libs +RUN apk add --no-cache pcsc-lite pcsc-lite-libs USER step ENV CONFIGPATH="/home/step/config/ca.json" From 66ba6048a4ac7e9b8ff345e39c34126a9a12fdd4 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sun, 24 Apr 2022 11:08:51 +0200 Subject: [PATCH 56/78] start pcscd if installed --- docker/entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 1f48c028..49d6b10c 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -53,6 +53,10 @@ function step_ca_init () { mv $STEPPATH/password $PWDPATH } +if [ -f /usr/sbin/pcscd ]; then + /usr/sbin/pcscd +fi + if [ ! -f "${STEPPATH}/config/ca.json" ]; then init_if_possible fi From 3fa96ebf13da203b01b13be7866a0a24f7ca2ed6 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sun, 24 Apr 2022 13:11:32 +0200 Subject: [PATCH 57/78] Improve policy errors returned to client --- authority/ssh.go | 22 +++++++++-- authority/ssh_test.go | 92 ++++++++++++++++++++++++++++++------------- authority/tls.go | 20 +++++++--- authority/tls_test.go | 62 +++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 35 deletions(-) diff --git a/authority/ssh.go b/authority/ssh.go index f2913566..ad9bb431 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -10,14 +10,17 @@ import ( "strings" "time" + "golang.org/x/crypto/ssh" + + "go.step.sm/crypto/randutil" + "go.step.sm/crypto/sshutil" + "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" + policy "github.com/smallstep/certificates/policy" "github.com/smallstep/certificates/templates" - "go.step.sm/crypto/randutil" - "go.step.sm/crypto/sshutil" - "golang.org/x/crypto/ssh" ) const ( @@ -252,6 +255,12 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi if a.sshUserPolicy != nil { allowed, err := a.sshUserPolicy.IsSSHCertificateAllowed(certTpl) if err != nil { + var pe *policy.NamePolicyError + if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: %s", err.Error()), + ) + } return nil, errs.InternalServerErr(err, errs.WithMessage("authority.SignSSH: error creating ssh user certificate"), ) @@ -269,6 +278,13 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi if a.sshHostPolicy != nil { allowed, err := a.sshHostPolicy.IsSSHCertificateAllowed(certTpl) if err != nil { + var pe *policy.NamePolicyError + if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { + return nil, errs.ApplyOptions( + // TODO: show which names were not allowed; they are in the err + errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: %s", err.Error()), + ) + } return nil, errs.InternalServerErr(err, errs.WithMessage("authority.SignSSH: error creating ssh host certificate"), ) diff --git a/authority/ssh_test.go b/authority/ssh_test.go index ce840fe1..2a135f4e 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -20,6 +20,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/templates" @@ -159,6 +160,14 @@ func TestAuthority_SignSSH(t *testing.T) { assert.FatalError(t, err) hostTemplateWithHosts, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, "key-id", []string{"foo.test.com", "bar.test.com"})) assert.FatalError(t, err) + userTemplateWithRoot, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"root"})) + assert.FatalError(t, err) + hostTemplateWithExampleDotCom, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, "key-id", []string{"example.com"})) + assert.FatalError(t, err) + badUserTemplate, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"127.0.0.1"})) + assert.FatalError(t, err) + badHostTemplate, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, "key-id", []string{"host...local"})) + assert.FatalError(t, err) userCustomTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{ SSH: &provisioner.SSHOptions{Template: `{ "type": "{{ .Type }}", @@ -182,11 +191,30 @@ func TestAuthority_SignSSH(t *testing.T) { }, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"})) assert.FatalError(t, err) + policyOptions := &policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"user"}, + }, + }, + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.test.com"}, + }, + }, + } + userPolicy, err := policy.NewSSHUserPolicyEngine(policyOptions) + assert.FatalError(t, err) + hostPolicy, err := policy.NewSSHHostPolicyEngine(policyOptions) + assert.FatalError(t, err) + now := time.Now() type fields struct { sshCAUserCertSignKey ssh.Signer sshCAHostCertSignKey ssh.Signer + sshUserPolicy policy.UserPolicy + sshHostPolicy policy.HostPolicy } type args struct { key ssh.PublicKey @@ -206,39 +234,49 @@ func TestAuthority_SignSSH(t *testing.T) { want want wantErr bool }{ - {"ok-user", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, - {"ok-host", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, - {"ok-user-only", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, - {"ok-host-only", fields{nil, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, - {"ok-opts-type-user", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-type-host", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert}, false}, - {"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, - {"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, - {"ok-opts-valid-after", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false}, - {"ok-opts-valid-before", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false}, - {"ok-cert-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-cert-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-custom-template", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{"user", "admin"}}, false}, - {"fail-opts-type", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{userTemplate}}, want{}, true}, - {"fail-cert-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("an error")}}, want{}, true}, - {"fail-cert-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("an error")}}, want{}, true}, - {"fail-opts-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("an error")}}, want{}, true}, - {"fail-opts-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("an error")}}, want{}, true}, - {"fail-bad-sign-options", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, "wrong type"}}, want{}, true}, - {"fail-no-user-key", fields{nil, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{}, true}, - {"fail-no-host-key", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{}, true}, - {"fail-bad-type", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, sshTestModifier{CertType: 100}}}, want{}, true}, - {"fail-custom-template", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userFailTemplate, userOptions}}, want{}, true}, - {"fail-custom-template-syntax-error-file", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONSyntaxErrorTemplateFile, userOptions}}, want{}, true}, - {"fail-custom-template-syntax-value-file", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONValueErrorTemplateFile, userOptions}}, want{}, true}, + {"ok-user", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, + {"ok-host", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, + {"ok-user-only", fields{signer, nil, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, + {"ok-host-only", fields{nil, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, + {"ok-opts-type-user", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-type-host", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert}, false}, + {"ok-opts-principals", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, + {"ok-opts-principals", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, + {"ok-opts-valid-after", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false}, + {"ok-opts-valid-before", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false}, + {"ok-cert-validator", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-cert-modifier", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-validator", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-modifier", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-custom-template", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{"user", "admin"}}, false}, + {"ok-user-policy", fields{signer, signer, userPolicy, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, + {"ok-host-policy", fields{signer, signer, nil, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, + {"fail-opts-type", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{userTemplate}}, want{}, true}, + {"fail-cert-validator", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("an error")}}, want{}, true}, + {"fail-cert-modifier", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("an error")}}, want{}, true}, + {"fail-opts-validator", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("an error")}}, want{}, true}, + {"fail-opts-modifier", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("an error")}}, want{}, true}, + {"fail-bad-sign-options", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, "wrong type"}}, want{}, true}, + {"fail-no-user-key", fields{nil, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{}, true}, + {"fail-no-host-key", fields{signer, nil, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{}, true}, + {"fail-bad-type", fields{signer, nil, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, sshTestModifier{CertType: 100}}}, want{}, true}, + {"fail-custom-template", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userFailTemplate, userOptions}}, want{}, true}, + {"fail-custom-template-syntax-error-file", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONSyntaxErrorTemplateFile, userOptions}}, want{}, true}, + {"fail-custom-template-syntax-value-file", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONValueErrorTemplateFile, userOptions}}, want{}, true}, + {"fail-user-policy", fields{signer, signer, userPolicy, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"root"}}, []provisioner.SignOption{userTemplateWithRoot}}, want{}, true}, + {"fail-user-policy-with-host-cert", fields{signer, signer, userPolicy, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true}, + {"fail-user-policy-with-bad-user", fields{signer, signer, userPolicy, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{badUserTemplate}}, want{}, true}, + {"fail-host-policy", fields{signer, signer, nil, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true}, + {"fail-host-policy-with-user-cert", fields{signer, signer, nil, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{}, true}, + {"fail-host-policy-with-bad-host", fields{signer, signer, nil, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{badHostTemplate}}, want{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := testAuthority(t) a.sshCAUserCertSignKey = tt.fields.sshCAUserCertSignKey a.sshCAHostCertSignKey = tt.fields.sshCAHostCertSignKey + a.sshUserPolicy = tt.fields.sshUserPolicy + a.sshHostPolicy = tt.fields.sshHostPolicy got, err := a.SignSSH(context.Background(), tt.args.key, tt.args.opts, tt.args.signOpts...) if (err != nil) != tt.wantErr { diff --git a/authority/tls.go b/authority/tls.go index 8438e4fd..e8440fb5 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -16,16 +16,19 @@ import ( "time" "github.com/pkg/errors" + "golang.org/x/crypto/ssh" + + "go.step.sm/crypto/jose" + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/pemutil" + "go.step.sm/crypto/x509util" + "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" - "go.step.sm/crypto/jose" - "go.step.sm/crypto/keyutil" - "go.step.sm/crypto/pemutil" - "go.step.sm/crypto/x509util" - "golang.org/x/crypto/ssh" + "github.com/smallstep/certificates/policy" ) // GetTLSOptions returns the tls options configured. @@ -199,6 +202,13 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Check if authority is allowed to sign the certificate var allowedToSign bool if allowedToSign, err = a.isAllowedToSign(leaf); err != nil { + var pe *policy.NamePolicyError + if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(errors.New("authority not allowed to sign"), err.Error()), + opts..., + ) + } return nil, errs.InternalServerErr(err, errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts), diff --git a/authority/tls_test.go b/authority/tls_test.go index e199e0c5..a96ce1eb 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -27,6 +27,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/cas/softcas" "github.com/smallstep/certificates/db" @@ -511,6 +512,37 @@ ZYtQ9Ot36qc= code: http.StatusForbidden, } }, + "fail with policy": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + aa := testAuthority(t) + aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template + aa.db = &db.MockAuthDB{ + MStoreCertificate: func(crt *x509.Certificate) error { + fmt.Println(crt.Subject) + assert.Equals(t, crt.Subject.CommonName, "smallstep test") + return nil + }, + } + policyOptions := &policy.X509PolicyOptions{ + DeniedNames: &policy.X509NameOptions{ + DNSDomains: []string{"test.smallstep.com"}, + }, + } + engine, err := policy.NewX509PolicyEngine(policyOptions) + assert.FatalError(t, err) + aa.x509Policy = engine + return &signTest{ + auth: aa, + csr: csr, + extraOpts: extraOpts, + signOpts: signOpts, + notBefore: signOpts.NotBefore.Time().Truncate(time.Second), + notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + extensionsCount: 6, + err: errors.New("authority not allowed to sign"), + code: http.StatusForbidden, + } + }, "ok": func(t *testing.T) *signTest { csr := getCSR(t, priv) _a := testAuthority(t) @@ -653,6 +685,36 @@ ZYtQ9Ot36qc= extensionsCount: 7, } }, + "ok with policy": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + aa := testAuthority(t) + aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template + aa.db = &db.MockAuthDB{ + MStoreCertificate: func(crt *x509.Certificate) error { + fmt.Println(crt.Subject) + assert.Equals(t, crt.Subject.CommonName, "smallstep test") + return nil + }, + } + policyOptions := &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.smallstep.com"}, + }, + DisableSubjectCommonNameVerification: true, // allows "smallstep test" + } + engine, err := policy.NewX509PolicyEngine(policyOptions) + assert.FatalError(t, err) + aa.x509Policy = engine + return &signTest{ + auth: aa, + csr: csr, + extraOpts: extraOpts, + signOpts: signOpts, + notBefore: signOpts.NotBefore.Time().Truncate(time.Second), + notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + extensionsCount: 6, + } + }, } for name, genTestCase := range tests { From 6264e8495cbb5ac3deeab8f4469682d8babf00f9 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sun, 24 Apr 2022 16:29:31 +0200 Subject: [PATCH 58/78] Improve policy error handling code coverage --- authority/administrator/collection.go | 4 +- authority/policy.go | 27 +- authority/policy_test.go | 687 ++++++++++++++++++++++++++ 3 files changed, 697 insertions(+), 21 deletions(-) diff --git a/authority/administrator/collection.go b/authority/administrator/collection.go index 300c3e4f..f40e7417 100644 --- a/authority/administrator/collection.go +++ b/authority/administrator/collection.go @@ -59,12 +59,12 @@ func newSubProv(subject, prov string) subProv { return subProv{subject, prov} } -// LoadBySubProv a admin by the subject and provisioner name. +// LoadBySubProv loads an admin by subject and provisioner name. func (c *Collection) LoadBySubProv(sub, provName string) (*linkedca.Admin, bool) { return loadAdmin(c.bySubProv, newSubProv(sub, provName)) } -// LoadByProvisioner a admin by the subject and provisioner name. +// LoadByProvisioner loads admins by provisioner name. func (c *Collection) LoadByProvisioner(provName string) ([]*linkedca.Admin, bool) { val, ok := c.byProv.Load(provName) if !ok { diff --git a/authority/policy.go b/authority/policy.go index 9bcbd044..b5ee7949 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -39,7 +39,10 @@ func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, e p, err := a.adminDB.GetAuthorityPolicy(ctx) if err != nil { - return nil, err + return nil, &PolicyError{ + Typ: InternalFailure, + Err: err, + } } return p, nil @@ -50,10 +53,7 @@ func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm defer a.adminMutex.Unlock() if err := a.checkAuthorityPolicy(ctx, adm, p); err != nil { - return nil, &PolicyError{ - Typ: AdminLockOut, - Err: err, - } + return nil, err } if err := a.adminDB.CreateAuthorityPolicy(ctx, p); err != nil { @@ -91,7 +91,7 @@ func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Adm if err := a.reloadPolicyEngines(ctx); err != nil { return nil, &PolicyError{ Typ: ReloadFailure, - Err: fmt.Errorf("error reloading policy engines when updating authority policy %w", err), + Err: fmt.Errorf("error reloading policy engines when updating authority policy: %w", err), } } @@ -145,14 +145,8 @@ func (a *Authority) checkProvisionerPolicy(ctx context.Context, currentAdmin *li 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"), - } - } + // get all admins for the provisioner; ignoring case in which they're not found + allProvisionerAdmins, _ := a.admins.LoadByProvisioner(provName) return a.checkPolicy(ctx, currentAdmin, allProvisionerAdmins, p) } @@ -222,11 +216,6 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { return nil } - // // temporarily only support the admin nosql DB - // if _, ok := a.adminDB.(*adminDBNosql.DB); !ok { - // return nil - // } - linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) if err != nil { var ae *admin.Error diff --git a/authority/policy_test.go b/authority/policy_test.go index bc121a79..410c3ed3 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -3,6 +3,7 @@ package authority import ( "context" "errors" + "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -11,8 +12,11 @@ import ( "go.step.sm/linkedca" "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/administrator" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/policy" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/db" ) func TestAuthority_checkPolicy(t *testing.T) { @@ -871,3 +875,686 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }) } } + +func TestAuthority_checkAuthorityPolicy(t *testing.T) { + type fields struct { + provisioners *provisioner.Collection + admins *administrator.Collection + db db.AuthDB + adminDB admin.DB + } + type args struct { + ctx context.Context + currentAdmin *linkedca.Admin + provName string + p *linkedca.Policy + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "no policy", + fields: fields{}, + args: args{ + currentAdmin: nil, + provName: "prov", + p: nil, + }, + wantErr: false, + }, + { + name: "fail/adminDB.GetAdmins-error", + fields: fields{ + admins: administrator.NewCollection(nil), + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return nil, errors.New("force") + }, + }, + }, + args: args{ + currentAdmin: &linkedca.Admin{Subject: "step"}, + provName: "prov", + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ok", + fields: fields{ + admins: administrator.NewCollection(nil), + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{}, nil + }, + }, + }, + args: args{ + currentAdmin: &linkedca.Admin{Subject: "step"}, + provName: "prov", + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + provisioners: tt.fields.provisioners, + admins: tt.fields.admins, + db: tt.fields.db, + adminDB: tt.fields.adminDB, + } + if err := a.checkAuthorityPolicy(tt.args.ctx, tt.args.currentAdmin, tt.args.p); (err != nil) != tt.wantErr { + t.Errorf("Authority.checkProvisionerPolicy() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestAuthority_checkProvisionerPolicy(t *testing.T) { + type fields struct { + provisioners *provisioner.Collection + admins *administrator.Collection + db db.AuthDB + adminDB admin.DB + } + type args struct { + ctx context.Context + currentAdmin *linkedca.Admin + provName string + p *linkedca.Policy + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "no policy", + fields: fields{}, + args: args{ + currentAdmin: nil, + provName: "prov", + p: nil, + }, + wantErr: false, + }, + { + name: "ok", + fields: fields{ + admins: administrator.NewCollection(nil), + }, + args: args{ + currentAdmin: &linkedca.Admin{Subject: "step"}, + provName: "prov", + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + provisioners: tt.fields.provisioners, + admins: tt.fields.admins, + db: tt.fields.db, + adminDB: tt.fields.adminDB, + } + if err := a.checkProvisionerPolicy(tt.args.ctx, tt.args.currentAdmin, tt.args.provName, tt.args.p); (err != nil) != tt.wantErr { + t.Errorf("Authority.checkProvisionerPolicy() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestAuthority_RemoveAuthorityPolicy(t *testing.T) { + type fields struct { + config *config.Config + db db.AuthDB + adminDB admin.DB + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + wantErr *PolicyError + }{ + { + name: "fail/adminDB.DeleteAuthorityPolicy", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockDeleteAuthorityPolicy: func(ctx context.Context) error { + return errors.New("force") + }, + }, + }, + wantErr: &PolicyError{ + Typ: StoreFailure, + Err: errors.New("force"), + }, + }, + { + name: "fail/a.reloadPolicyEngines", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockDeleteAuthorityPolicy: func(ctx context.Context) error { + return nil + }, + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, errors.New("force") + }, + }, + }, + wantErr: &PolicyError{ + Typ: ReloadFailure, + Err: errors.New("error reloading policy engines when deleting authority policy: error getting policy to (re)load policy engines: force"), + }, + }, + { + name: "ok", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockDeleteAuthorityPolicy: func(ctx context.Context) error { + return nil + }, + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, nil + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + config: tt.fields.config, + db: tt.fields.db, + adminDB: tt.fields.adminDB, + } + err := a.RemoveAuthorityPolicy(tt.args.ctx) + if err != nil { + pe, ok := err.(*PolicyError) + assert.True(t, ok) + assert.Equal(t, tt.wantErr.Typ, pe.Typ) + assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error()) + return + } + }) + } +} + +func TestAuthority_GetAuthorityPolicy(t *testing.T) { + type fields struct { + config *config.Config + db db.AuthDB + adminDB admin.DB + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + want *linkedca.Policy + wantErr *PolicyError + }{ + { + name: "fail/adminDB.GetAuthorityPolicy", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, errors.New("force") + }, + }, + }, + wantErr: &PolicyError{ + Typ: InternalFailure, + Err: errors.New("force"), + }, + }, + { + name: "ok", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{}, nil + }, + }, + }, + want: &linkedca.Policy{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + config: tt.fields.config, + db: tt.fields.db, + adminDB: tt.fields.adminDB, + } + got, err := a.GetAuthorityPolicy(tt.args.ctx) + if err != nil { + pe, ok := err.(*PolicyError) + assert.True(t, ok) + assert.Equal(t, tt.wantErr.Typ, pe.Typ) + assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error()) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Authority.GetAuthorityPolicy() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthority_CreateAuthorityPolicy(t *testing.T) { + type fields struct { + config *config.Config + db db.AuthDB + adminDB admin.DB + } + type args struct { + ctx context.Context + adm *linkedca.Admin + p *linkedca.Policy + } + tests := []struct { + name string + fields fields + args args + want *linkedca.Policy + wantErr *PolicyError + }{ + { + name: "fail/a.checkAuthorityPolicy", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return nil, errors.New("force") + }, + }, + }, + args: args{ + ctx: context.Background(), + adm: &linkedca.Admin{Subject: "step"}, + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: &PolicyError{ + Typ: InternalFailure, + Err: errors.New("error retrieving admins: force"), + }, + }, + { + name: "fail/adminDB.CreateAuthorityPolicy", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{}, nil + }, + MockCreateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error { + return errors.New("force") + }, + }, + }, + args: args{ + ctx: context.Background(), + adm: &linkedca.Admin{Subject: "step"}, + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: &PolicyError{ + Typ: StoreFailure, + Err: errors.New("force"), + }, + }, + { + name: "fail/a.reloadPolicyEngines", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, errors.New("force") + }, + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{}, nil + }, + }, + }, + args: args{ + ctx: context.Background(), + adm: &linkedca.Admin{Subject: "step"}, + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: &PolicyError{ + Typ: ReloadFailure, + Err: errors.New("error reloading policy engines when creating authority policy: error getting policy to (re)load policy engines: force"), + }, + }, + { + name: "ok", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, nil + }, + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{}, nil + }, + }, + }, + args: args{ + ctx: context.Background(), + adm: &linkedca.Admin{Subject: "step"}, + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + want: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + config: tt.fields.config, + db: tt.fields.db, + adminDB: tt.fields.adminDB, + } + got, err := a.CreateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p) + if err != nil { + pe, ok := err.(*PolicyError) + assert.True(t, ok) + assert.Equal(t, tt.wantErr.Typ, pe.Typ) + assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error()) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Authority.CreateAuthorityPolicy() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthority_UpdateAuthorityPolicy(t *testing.T) { + type fields struct { + config *config.Config + db db.AuthDB + adminDB admin.DB + } + type args struct { + ctx context.Context + adm *linkedca.Admin + p *linkedca.Policy + } + tests := []struct { + name string + fields fields + args args + want *linkedca.Policy + wantErr *PolicyError + }{ + { + name: "fail/a.checkAuthorityPolicy", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return nil, errors.New("force") + }, + }, + }, + args: args{ + ctx: context.Background(), + adm: &linkedca.Admin{Subject: "step"}, + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: &PolicyError{ + Typ: InternalFailure, + Err: errors.New("error retrieving admins: force"), + }, + }, + { + name: "fail/adminDB.UpdateAuthorityPolicy", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{}, nil + }, + MockUpdateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error { + return errors.New("force") + }, + }, + }, + args: args{ + ctx: context.Background(), + adm: &linkedca.Admin{Subject: "step"}, + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: &PolicyError{ + Typ: StoreFailure, + Err: errors.New("force"), + }, + }, + { + name: "fail/a.reloadPolicyEngines", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, errors.New("force") + }, + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{}, nil + }, + }, + }, + args: args{ + ctx: context.Background(), + adm: &linkedca.Admin{Subject: "step"}, + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + wantErr: &PolicyError{ + Typ: ReloadFailure, + Err: errors.New("error reloading policy engines when updating authority policy: error getting policy to (re)load policy engines: force"), + }, + }, + { + name: "ok", + fields: fields{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + EnableAdmin: true, + }, + }, + adminDB: &admin.MockDB{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, nil + }, + MockUpdateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error { + return nil + }, + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{}, nil + }, + }, + }, + args: args{ + ctx: context.Background(), + adm: &linkedca.Admin{Subject: "step"}, + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + want: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"step", "otherAdmin"}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + config: tt.fields.config, + db: tt.fields.db, + adminDB: tt.fields.adminDB, + } + got, err := a.UpdateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p) + if err != nil { + pe, ok := err.(*PolicyError) + assert.True(t, ok) + assert.Equal(t, tt.wantErr.Typ, pe.Typ) + assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error()) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Authority.UpdateAuthorityPolicy() = %v, want %v", got, tt.want) + } + }) + } +} From 20f5d12b997d45cf9bcce25659f9a988b7414bb8 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 25 Apr 2022 11:02:03 +0200 Subject: [PATCH 59/78] Improve test rigour for reloadPolicyEngines --- authority/policy.go | 2 +- authority/policy_test.go | 249 +++++++++++++++++++++++++++++---------- 2 files changed, 188 insertions(+), 63 deletions(-) diff --git a/authority/policy.go b/authority/policy.go index b5ee7949..c847d56a 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -208,7 +208,7 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { err error policyOptions *authPolicy.Options ) - // if admin API is enabled, the CA is running in linked mode + if a.config.AuthorityConfig.EnableAdmin { // temporarily disable policy loading when LinkedCA is in use diff --git a/authority/policy_test.go b/authority/policy_test.go index 410c3ed3..e0955da0 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -331,17 +331,104 @@ func Test_policyToCertificates(t *testing.T) { } func TestAuthority_reloadPolicyEngines(t *testing.T) { - type exp struct { - x509Policy bool - sshUserPolicy bool - sshHostPolicy bool + + existingX509PolicyEngine, err := policy.NewX509PolicyEngine(&policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.hosts.example.com"}, + }, + }) + assert.NoError(t, err) + + existingSSHHostPolicyEngine, err := policy.NewSSHHostPolicyEngine(&policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.hosts.example.com"}, + }, + }, + }) + assert.NoError(t, err) + + existingSSHUserPolicyEngine, err := policy.NewSSHUserPolicyEngine(&policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + EmailAddresses: []string{"@mails.example.com"}, + }, + }, + }) + assert.NoError(t, err) + + newX509PolicyEngine, err := policy.NewX509PolicyEngine(&policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.X509NameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + AllowWildcardLiteral: true, + DisableSubjectCommonNameVerification: false, + }) + assert.NoError(t, err) + + newSSHHostPolicyEngine, err := policy.NewSSHHostPolicyEngine(&policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + }, + }) + assert.NoError(t, err) + + newSSHUserPolicyEngine, err := policy.NewSSHUserPolicyEngine(&policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"*"}, + }, + DeniedNames: &policy.SSHNameOptions{ + Principals: []string{"root"}, + }, + }, + }) + assert.NoError(t, err) + + newAdminX509PolicyEngine, err := policy.NewX509PolicyEngine(&policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + }) + assert.NoError(t, err) + + newAdminSSHHostPolicyEngine, err := policy.NewSSHHostPolicyEngine(&policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + }, + }) + assert.NoError(t, err) + + newAdminSSHUserPolicyEngine, err := policy.NewSSHUserPolicyEngine(&policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + EmailAddresses: []string{"@example.com"}, + }, + }, + }) + assert.NoError(t, err) + + type expected struct { + x509Policy policy.X509Policy + sshUserPolicy policy.UserPolicy + sshHostPolicy policy.HostPolicy } tests := []struct { name string config *config.Config adminDB admin.DB ctx context.Context - expected *exp + expected *expected wantErr bool }{ { @@ -360,6 +447,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: true, + expected: &expected{ + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, + }, }, { name: "fail/standalone-ssh-host-policy", @@ -379,6 +471,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: true, + expected: &expected{ + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, + }, }, { name: "fail/standalone-ssh-user-policy", @@ -398,6 +495,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: true, + expected: &expected{ + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, + }, }, { name: "fail/adminDB.GetAuthorityPolicy-error", @@ -413,6 +515,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: true, + expected: &expected{ + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, + }, }, { name: "fail/admin-x509-policy", @@ -434,6 +541,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: true, + expected: &expected{ + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, + }, }, { name: "fail/admin-ssh-host-policy", @@ -457,6 +569,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: true, + expected: &expected{ + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, + }, }, { name: "fail/admin-ssh-user-policy", @@ -480,6 +597,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: true, + expected: &expected{ + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, + }, }, { name: "ok/linkedca-unsupported", @@ -491,6 +613,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { adminDB: &linkedCaClient{}, ctx: context.Background(), wantErr: false, + expected: &expected{ + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, + }, }, { name: "ok/standalone-no-policy", @@ -500,9 +627,13 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Policy: nil, }, }, - ctx: context.Background(), - wantErr: false, - expected: nil, + ctx: context.Background(), + wantErr: false, + expected: &expected{ + x509Policy: nil, + sshUserPolicy: nil, + sshHostPolicy: nil, + }, }, { name: "ok/standalone-x509-policy", @@ -525,11 +656,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: false, - expected: &exp{ + expected: &expected{ // expect only the X.509 policy to exist - x509Policy: true, - sshHostPolicy: false, - sshUserPolicy: false, + x509Policy: newX509PolicyEngine, + sshHostPolicy: nil, + sshUserPolicy: nil, }, }, { @@ -553,11 +684,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: false, - expected: &exp{ + expected: &expected{ // expect only the SSH host policy to exist - x509Policy: false, - sshHostPolicy: true, - sshUserPolicy: false, + x509Policy: nil, + sshHostPolicy: newSSHHostPolicyEngine, + sshUserPolicy: nil, }, }, { @@ -581,11 +712,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: false, - expected: &exp{ + expected: &expected{ // expect only the SSH user policy to exist - x509Policy: false, - sshHostPolicy: false, - sshUserPolicy: true, + x509Policy: nil, + sshHostPolicy: nil, + sshUserPolicy: newSSHUserPolicyEngine, }, }, { @@ -617,11 +748,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: false, - expected: &exp{ + expected: &expected{ // expect only the SSH policy engines to exist - x509Policy: false, - sshHostPolicy: true, - sshUserPolicy: true, + x509Policy: nil, + sshHostPolicy: newSSHHostPolicyEngine, + sshUserPolicy: newSSHUserPolicyEngine, }, }, { @@ -663,11 +794,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: false, - expected: &exp{ + expected: &expected{ // expect all three policy engines to exist - x509Policy: true, - sshHostPolicy: true, - sshUserPolicy: true, + x509Policy: newX509PolicyEngine, + sshHostPolicy: newSSHHostPolicyEngine, + sshUserPolicy: newSSHUserPolicyEngine, }, }, { @@ -690,10 +821,10 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: false, - expected: &exp{ - x509Policy: true, - sshHostPolicy: false, - sshUserPolicy: false, + expected: &expected{ + x509Policy: newAdminX509PolicyEngine, + sshHostPolicy: nil, + sshUserPolicy: nil, }, }, { @@ -718,10 +849,10 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: false, - expected: &exp{ - x509Policy: false, - sshHostPolicy: true, - sshUserPolicy: false, + expected: &expected{ + x509Policy: nil, + sshHostPolicy: newAdminSSHHostPolicyEngine, + sshUserPolicy: nil, }, }, { @@ -746,10 +877,10 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, ctx: context.Background(), wantErr: false, - expected: &exp{ - x509Policy: false, - sshHostPolicy: false, - sshUserPolicy: true, + expected: &expected{ + x509Policy: nil, + sshHostPolicy: nil, + sshUserPolicy: newAdminSSHUserPolicyEngine, }, }, { @@ -789,11 +920,11 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, wantErr: false, - expected: &exp{ + expected: &expected{ // expect all three policy engines to exist - x509Policy: true, - sshHostPolicy: true, - sshUserPolicy: true, + x509Policy: newX509PolicyEngine, + sshHostPolicy: newAdminSSHHostPolicyEngine, + sshUserPolicy: newAdminSSHUserPolicyEngine, }, }, { @@ -842,36 +973,30 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, wantErr: false, - expected: &exp{ + expected: &expected{ // expect all three policy engines to exist - x509Policy: true, - sshHostPolicy: false, - sshUserPolicy: false, + x509Policy: newX509PolicyEngine, + sshHostPolicy: nil, + sshUserPolicy: nil, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &Authority{ - config: tt.config, - adminDB: tt.adminDB, + config: tt.config, + adminDB: tt.adminDB, + x509Policy: existingX509PolicyEngine, + sshUserPolicy: existingSSHUserPolicyEngine, + sshHostPolicy: existingSSHHostPolicyEngine, } if err := a.reloadPolicyEngines(tt.ctx); (err != nil) != tt.wantErr { t.Errorf("Authority.reloadPolicyEngines() error = %v, wantErr %v", err, tt.wantErr) } - // if expected value is set, check existence of the policy engines - // Check that they're always nil if the expected value is not set, - // which happens on errors. - if tt.expected != nil { - assert.Equal(t, tt.expected.x509Policy, a.x509Policy != nil) - assert.Equal(t, tt.expected.sshHostPolicy, a.sshHostPolicy != nil) - assert.Equal(t, tt.expected.sshUserPolicy, a.sshUserPolicy != nil) - } else { - assert.Nil(t, a.x509Policy) - assert.Nil(t, a.sshHostPolicy) - assert.Nil(t, a.sshUserPolicy) - } + assert.Equal(t, tt.expected.x509Policy, a.x509Policy) + assert.Equal(t, tt.expected.sshHostPolicy, a.sshHostPolicy) + assert.Equal(t, tt.expected.sshUserPolicy, a.sshUserPolicy) }) } } From df8eca2c19a0cb6d47db0fa7b32eeb4aec0e7f27 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Mon, 25 Apr 2022 14:14:23 +0200 Subject: [PATCH 60/78] space --- docker/Dockerfile.step-ca.hsm | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile.step-ca.hsm b/docker/Dockerfile.step-ca.hsm index 4b870d18..ac59c909 100644 --- a/docker/Dockerfile.step-ca.hsm +++ b/docker/Dockerfile.step-ca.hsm @@ -7,6 +7,7 @@ RUN apk add --no-cache curl git make RUN apk add --no-cache gcc musl-dev pkgconf pcsc-lite-dev RUN make V=1 GOFLAGS="" build + FROM smallstep/step-cli:latest COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca From c1425422ddbdd3925c3ab6ce19c68b9ed0f0bcf0 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Mon, 25 Apr 2022 14:25:31 +0200 Subject: [PATCH 61/78] include support for GCP and AWS KMS by default --- docker/Dockerfile.step-ca | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile.step-ca b/docker/Dockerfile.step-ca index 9363b6ae..46677a91 100644 --- a/docker/Dockerfile.step-ca +++ b/docker/Dockerfile.step-ca @@ -3,15 +3,15 @@ FROM golang:alpine AS builder WORKDIR /src COPY . . -RUN apk add --no-cache \ - curl \ - git \ - make && \ - make V=1 bin/step-ca +RUN apk add --no-cache curl git make +RUN make V=1 bin/step-ca bin/step-awskms-init bin/step-cloudkms-init + FROM smallstep/step-cli:latest COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca +COPY --from=builder /src/bin/step-awskms-init /usr/local/bin/step-awskms-init +COPY --from=builder /src/bin/step-cloudkms-init /usr/local/bin/step-cloudkms-init USER root RUN apk add --no-cache libcap && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca From 76112c2da1fb9fc61e458cdbb4e74f60d2a728fd Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 26 Apr 2022 01:47:07 +0200 Subject: [PATCH 62/78] Improve error creation and testing for core policy engine --- authority/policy.go | 2 +- authority/policy_test.go | 4 +- authority/ssh.go | 26 +- authority/tls.go | 13 +- ca/adminClient.go | 18 +- policy/engine.go | 112 ++-- policy/engine_test.go | 1085 +++++++++++++++++++++++++++----------- policy/options_test.go | 31 +- policy/validate.go | 92 ++-- 9 files changed, 974 insertions(+), 409 deletions(-) diff --git a/authority/policy.go b/authority/policy.go index c847d56a..f71e37c7 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -274,7 +274,7 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error { if allowed, err = engine.AreSANsAllowed(sans); err != nil { var policyErr *policy.NamePolicyError isNamePolicyError := errors.As(err, &policyErr) - if isNamePolicyError && policyErr.Reason == policy.NotAuthorizedForThisName { + if isNamePolicyError && policyErr.Reason == policy.NotAllowed { 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), diff --git a/authority/policy_test.go b/authority/policy_test.go index e0955da0..075987c0 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -58,7 +58,7 @@ func TestAuthority_checkPolicy(t *testing.T) { }, err: &PolicyError{ 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{ Typ: EvaluationFailure, - Err: errors.New("cannot parse domain: dns \"**\" cannot be converted to ASCII"), + Err: errors.New("cannot parse dns domain \"**\""), }, } }, diff --git a/authority/ssh.go b/authority/ssh.go index ad9bb431..3f08b88a 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/binary" "errors" + "fmt" "net/http" "strings" "time" @@ -256,10 +257,14 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi allowed, err := a.sshUserPolicy.IsSSHCertificateAllowed(certTpl) if err != nil { var pe *policy.NamePolicyError - if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { - return nil, errs.ApplyOptions( - errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: %s", err.Error()), - ) + if errors.As(err, &pe) && pe.Reason == policy.NotAllowed { + return nil, &errs.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, 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) if err != nil { var pe *policy.NamePolicyError - if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { - return nil, errs.ApplyOptions( - // TODO: show which names were not allowed; they are in the err - errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: %s", err.Error()), - ) + if errors.As(err, &pe) && pe.Reason == policy.NotAllowed { + return nil, &errs.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, errs.WithMessage("authority.SignSSH: error creating ssh host certificate"), diff --git a/authority/tls.go b/authority/tls.go index e8440fb5..cc34ff6a 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -203,11 +203,14 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign var allowedToSign bool if allowedToSign, err = a.isAllowedToSign(leaf); err != nil { var pe *policy.NamePolicyError - if errors.As(err, &pe) && pe.Reason == policy.NotAuthorizedForThisName { - return nil, errs.ApplyOptions( - errs.ForbiddenErr(errors.New("authority not allowed to sign"), err.Error()), - opts..., - ) + if errors.As(err, &pe) && pe.Reason == policy.NotAllowed { + return nil, errs.ApplyOptions(&errs.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()), + }, opts...) } return nil, errs.InternalServerErr(err, errs.WithKeyVal("csr", csr), diff --git a/ca/adminClient.go b/ca/adminClient.go index dc898a2c..bf853e9d 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -13,15 +13,17 @@ import ( "time" "github.com/pkg/errors" - adminAPI "github.com/smallstep/certificates/authority/admin/api" - "github.com/smallstep/certificates/authority/provisioner" - "github.com/smallstep/certificates/errs" + "google.golang.org/protobuf/encoding/protojson" + "go.step.sm/cli-utils/token" "go.step.sm/cli-utils/token/provision" "go.step.sm/crypto/jose" "go.step.sm/crypto/randutil" "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 ( @@ -818,7 +820,7 @@ retry: func (c *AdminClient) GetProvisionerPolicy(provisionerName string) (*linkedca.Policy, error) { 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) if err != nil { 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 { 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) if err != nil { 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 { 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) if err != nil { return nil, fmt.Errorf("error generating admin token: %w", err) @@ -919,7 +921,7 @@ retry: func (c *AdminClient) RemoveProvisionerPolicy(provisionerName string) error { 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) if err != nil { return fmt.Errorf("error generating admin token: %w", err) diff --git a/policy/engine.go b/policy/engine.go index fe86ed5c..d03665ee 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -15,11 +15,11 @@ import ( type NamePolicyReason int const ( - // NotAuthorizedForThisName results when an instance of - // NamePolicyEngine determines that there's a constraint which - // doesn't permit a DNS or another type of SAN to be signed - // (or otherwise used). - NotAuthorizedForThisName NamePolicyReason = iota + _ NamePolicyReason = iota + // NotAllowed results when an instance of NamePolicyEngine + // determines that there's a constraint which doesn't permit + // a DNS or another type of SAN to be signed (or otherwise used). + NotAllowed // CannotParseDomain is returned when an error occurs // when parsing the domain part of SAN or subject. CannotParseDomain @@ -31,26 +31,42 @@ const ( CannotMatchNameToConstraint ) +type NameType string + +const ( + DNSNameType NameType = "dns" + IPNameType NameType = "ip" + EmailNameType NameType = "email" + URINameType NameType = "uri" + PrincipalNameType NameType = "principal" +) + type NamePolicyError struct { - Reason NamePolicyReason - Detail string + Reason NamePolicyReason + NameType NameType + Name string + detail string } func (e *NamePolicyError) Error() string { switch e.Reason { - case NotAuthorizedForThisName: - return "not authorized to sign for this name: " + e.Detail + case NotAllowed: + return fmt.Sprintf("%s name %q not allowed", e.NameType, e.Name) case CannotParseDomain: - return "cannot parse domain: " + e.Detail + return fmt.Sprintf("cannot parse %s domain %q", e.NameType, e.Name) case CannotParseRFC822Name: - return "cannot parse rfc822Name: " + e.Detail + return fmt.Sprintf("cannot parse %s rfc822Name %q", e.NameType, e.Name) 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: - 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 // 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? @@ -98,13 +114,13 @@ func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { } e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains) - e.permittedIPRanges = removeDuplicateIPRanges(e.permittedIPRanges) + e.permittedIPRanges = removeDuplicateIPNets(e.permittedIPRanges) e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses) e.permittedURIDomains = removeDuplicates(e.permittedURIDomains) e.permittedPrincipals = removeDuplicates(e.permittedPrincipals) e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains) - e.excludedIPRanges = removeDuplicateIPRanges(e.excludedIPRanges) + e.excludedIPRanges = removeDuplicateIPNets(e.excludedIPRanges) e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses) e.excludedURIDomains = removeDuplicates(e.excludedURIDomains) e.excludedPrincipals = removeDuplicates(e.excludedPrincipals) @@ -126,35 +142,59 @@ func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { return e, nil } -func removeDuplicates(strSlice []string) []string { - if len(strSlice) == 0 { - return nil +// removeDuplicates returns a new slice of strings with +// duplicate values removed. It retains the order of elements +// in the source slice. +func removeDuplicates(items []string) (ret []string) { + + // no need to remove dupes; return original + if len(items) <= 1 { + return items } - keys := make(map[string]bool) - result := []string{} - for _, item := range strSlice { - if _, value := keys[item]; !value && item != "" { // skip empty constraints - keys[item] = true - result = append(result, item) + + keys := make(map[string]struct{}, len(items)) + + ret = make([]string, 0, len(items)) + for _, item := range items { + if _, ok := keys[item]; ok { + continue } + + keys[item] = struct{}{} + ret = append(ret, item) } - return result + + return } -func removeDuplicateIPRanges(ipRanges []*net.IPNet) []*net.IPNet { - if len(ipRanges) == 0 { - return nil +// removeDuplicateIPNets returns a new slice of net.IPNets with +// duplicate values removed. It retains the order of elements in +// the source slice. An IPNet is considered duplicate if its CIDR +// notation exists multiple times in the slice. +func removeDuplicateIPNets(items []*net.IPNet) (ret []*net.IPNet) { + + // no need to remove dupes; return original + if len(items) <= 1 { + return items } - keys := make(map[string]bool) - result := []*net.IPNet{} - for _, item := range ipRanges { - key := item.String() - if _, value := keys[key]; !value { - keys[key] = true - result = append(result, item) + + keys := make(map[string]struct{}, len(items)) + + ret = make([]*net.IPNet, 0, len(items)) + for _, item := range items { + key := item.String() // use CIDR notation as key + if _, ok := keys[key]; ok { + continue } + + keys[key] = struct{}{} + ret = append(ret, item) } - 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. diff --git a/policy/engine_test.go b/policy/engine_test.go index cce4ad34..a99885ea 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -3,14 +3,15 @@ package policy import ( "crypto/x509" "crypto/x509/pkix" + "errors" "net" "net/url" + "reflect" "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" "golang.org/x/crypto/ssh" - - "github.com/smallstep/assert" ) // TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on @@ -200,7 +201,7 @@ func TestNamePolicyEngine_matchDomainConstraint(t *testing.T) { func Test_matchIPConstraint(t *testing.T) { nat64IP, nat64Net, err := net.ParseCIDR("64:ff9b::/96") - assert.FatalError(t, err) + assert.NoError(t, err) tests := []struct { name string ip net.IP @@ -631,7 +632,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { options []NamePolicyOption cert *x509.Certificate want bool - wantErr bool + wantErr *NamePolicyError }{ // SINGLE SAN TYPE PERMITTED FAILURE TESTS { @@ -642,8 +643,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "www.example.com", + }, }, { name: "fail/dns-permitted-wildcard-literal-x509", @@ -655,8 +660,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "*.x509local", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "*.x509local", + }, }, { name: "fail/dns-permitted-single-host", @@ -666,8 +675,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"differenthost.local"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "differenthost.local", + }, }, { name: "fail/dns-permitted-no-label", @@ -677,8 +690,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"local"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "local", + }, }, { name: "fail/dns-permitted-empty-label", @@ -688,8 +705,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"www..local"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotParseDomain, + NameType: DNSNameType, + Name: "www..local", + }, }, { name: "fail/dns-permitted-dot-domain", @@ -701,8 +722,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { ".local", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: ".local", + }, }, { name: "fail/dns-permitted-wildcard-multiple-subdomains", @@ -714,8 +739,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "sub.example.local", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "sub.example.local", + }, }, { name: "fail/dns-permitted-wildcard-literal", @@ -727,8 +756,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "*.local", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "*.local", + }, }, { name: "fail/dns-permitted-idna-internationalized-domain", @@ -740,8 +773,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { string(byte(0)) + ".例.jp", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotParseDomain, + NameType: DNSNameType, + Name: string(byte(0)) + ".例.jp", + }, }, { name: "fail/ipv4-permitted", @@ -756,8 +793,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "1.1.1.1", + }, }, { name: "fail/ipv6-permitted", @@ -772,8 +813,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("3001:0db8:85a3:0000:0000:8a2e:0370:7334")}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "3001:db8:85a3::8a2e:370:7334", // IPv6 is shortened internally + }, }, { name: "fail/mail-permitted-wildcard", @@ -785,8 +830,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "test@local.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "test@local.com", + }, }, { name: "fail/mail-permitted-wildcard-x509", @@ -798,8 +847,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "test@local.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "test@local.com", + }, }, { name: "fail/mail-permitted-specific-mailbox", @@ -811,8 +864,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "root@local.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "root@local.com", + }, }, { name: "fail/mail-permitted-wildcard-subdomain", @@ -824,8 +881,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "test@sub.example.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "test@sub.example.com", + }, }, { name: "fail/mail-permitted-idna-internationalized-domain", @@ -835,8 +896,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"bücher@例.jp"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotParseRFC822Name, + NameType: EmailNameType, + Name: "bücher@例.jp", + }, }, { name: "fail/mail-permitted-idna-internationalized-domain-rfc822", @@ -846,8 +911,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"bücher@例.jp" + string(byte(0))}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotParseRFC822Name, + NameType: EmailNameType, + Name: "bücher@例.jp" + string(byte(0)), + }, }, { name: "fail/mail-permitted-idna-internationalized-domain-ascii", @@ -857,8 +926,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@xn---bla.jp"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotParseDomain, + NameType: EmailNameType, + Name: "mail@xn---bla.jp", + }, }, { name: "fail/uri-permitted-domain-wildcard", @@ -873,8 +946,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: URINameType, + Name: "https://example.com", + }, }, { name: "fail/uri-permitted", @@ -889,8 +966,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: URINameType, + Name: "https://bad.local", + }, }, { name: "fail/uri-permitted-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld @@ -905,8 +986,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotMatchNameToConstraint, + NameType: URINameType, + Name: "https://*.local", + }, }, { name: "fail/uri-permitted-idna-internationalized-domain", @@ -921,8 +1006,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotMatchNameToConstraint, + NameType: URINameType, + Name: "https://abc.b%C3%BCcher.example.com", + }, }, // SINGLE SAN TYPE EXCLUDED FAILURE TESTS { @@ -933,8 +1022,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "www.example.com", + }, }, { name: "fail/dns-excluded-single-host", @@ -944,8 +1037,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"host.example.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "host.example.com", + }, }, { name: "fail/ipv4-excluded", @@ -960,8 +1057,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "127.0.0.1", + }, }, { name: "fail/ipv6-excluded", @@ -976,8 +1077,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "2001:db8:85a3::8a2e:370:7334", + }, }, { name: "fail/mail-excluded", @@ -987,8 +1092,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "mail@example.com", + }, }, { name: "fail/uri-excluded", @@ -1003,8 +1112,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: URINameType, + Name: "https://www.example.com", + }, }, { name: "fail/uri-excluded-with-literal-wildcard", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld @@ -1019,10 +1132,32 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotMatchNameToConstraint, + NameType: URINameType, + Name: "https://*.local", + }, }, // SUBJECT FAILURE TESTS + { + name: "fail/subject-dns-no-domain", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedDNSDomains("*.local"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "name with space.local", + }, + }, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotParseDomain, + NameType: DNSNameType, + Name: "name with space.local", + }, + }, { name: "fail/subject-dns-permitted", options: []NamePolicyOption{ @@ -1034,8 +1169,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "example.notlocal", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "example.notlocal", + }, }, { name: "fail/subject-dns-excluded", @@ -1048,8 +1187,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "example.local", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "example.local", + }, }, { name: "fail/subject-ipv4-permitted", @@ -1067,8 +1210,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "10.10.10.10", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "10.10.10.10", + }, }, { name: "fail/subject-ipv4-excluded", @@ -1086,8 +1233,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "127.0.0.30", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "127.0.0.30", + }, }, { name: "fail/subject-ipv6-permitted", @@ -1105,8 +1256,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "2002:0db8:85a3:0000:0000:8a2e:0370:7339", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "2002:db8:85a3::8a2e:370:7339", + }, }, { name: "fail/subject-ipv6-excluded", @@ -1124,8 +1279,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "2001:0db8:85a3:0000:0000:8a2e:0370:7339", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "2001:db8:85a3::8a2e:370:7339", + }, }, { name: "fail/subject-email-permitted", @@ -1138,8 +1297,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "mail@smallstep.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "mail@smallstep.com", + }, }, { name: "fail/subject-email-excluded", @@ -1152,8 +1315,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "mail@example.local", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "mail@example.local", + }, }, { name: "fail/subject-uri-permitted", @@ -1166,8 +1333,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "https://www.google.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: URINameType, + Name: "https://www.google.com", + }, }, { name: "fail/subject-uri-excluded", @@ -1180,8 +1351,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "https://www.example.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: URINameType, + Name: "https://www.example.com", + }, }, // DIFFERENT SAN PERMITTED FAILURE TESTS { @@ -1192,8 +1367,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "127.0.0.1", + }, }, { name: "fail/dns-permitted-with-mail", // when only DNS is permitted, mails are not allowed. @@ -1203,8 +1382,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@smallstep.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "mail@smallstep.com", + }, }, { name: "fail/dns-permitted-with-uri", // when only DNS is permitted, URIs are not allowed. @@ -1219,8 +1402,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: URINameType, + Name: "https://www.example.com", + }, }, { name: "fail/ip-permitted-with-dns-name", // when only IP is permitted, DNS names are not allowed. @@ -1235,8 +1422,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "www.example.com", + }, }, { name: "fail/ip-permitted-with-mail", // when only IP is permitted, mails are not allowed. @@ -1251,8 +1442,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@smallstep.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "mail@smallstep.com", + }, }, { name: "fail/ip-permitted-with-uri", // when only IP is permitted, URIs are not allowed. @@ -1272,8 +1467,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: URINameType, + Name: "https://www.example.com", + }, }, { name: "fail/mail-permitted-with-dns-name", // when only mail is permitted, DNS names are not allowed. @@ -1283,8 +1482,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "www.example.com", + }, }, { name: "fail/mail-permitted-with-ip", // when only mail is permitted, IPs are not allowed. @@ -1296,8 +1499,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { net.ParseIP("127.0.0.1"), }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "127.0.0.1", + }, }, { name: "fail/mail-permitted-with-uri", // when only mail is permitted, URIs are not allowed. @@ -1312,8 +1519,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: URINameType, + Name: "https://www.example.com", + }, }, { name: "fail/uri-permitted-with-dns-name", // when only URI is permitted, DNS names are not allowed. @@ -1323,8 +1534,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"host.local"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "host.local", + }, }, { name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, IPs are not allowed. @@ -1336,8 +1551,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "2001:db8:85a3::8a2e:370:7334", + }, }, { name: "fail/uri-permitted-with-ip-name", // when only URI is permitted, mails are not allowed. @@ -1347,8 +1566,12 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@smallstep.com"}, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "mail@smallstep.com", + }, }, // COMBINED FAILURE TESTS { @@ -1378,8 +1601,45 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "badhost.local", + }, + }, + { + name: "fail/combined-simple-all-badmail@example.local", + options: []NamePolicyOption{ + WithPermittedDNSDomains("*.local"), + WithPermittedCIDRs("127.0.0.1/24"), + WithPermittedEmailAddresses("@example.local"), + WithPermittedURIDomains("*.example.local"), + WithExcludedDNSDomains("badhost.local"), + WithExcludedCIDRs("127.0.0.128/25"), + WithExcludedEmailAddresses("badmail@example.local"), + WithExcludedURIDomains("badwww.example.local"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "badhost.local", + }, + DNSNames: []string{"example.local"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.40")}, + EmailAddresses: []string{"mail@example.local", "badmail@example.local"}, + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "badmail@example.local", + }, }, // NO CONSTRAINT SUCCESS TESTS { @@ -1388,8 +1648,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"www.example.com"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ipv4-no-constraints", @@ -1399,8 +1658,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { net.ParseIP("127.0.0.1"), }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ipv6-no-constraints", @@ -1410,8 +1668,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-no-constraints", @@ -1419,8 +1676,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@smallstep.com"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-no-constraints", @@ -1433,8 +1689,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-no-constraints", @@ -1446,8 +1701,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "www.example.com", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-empty-no-constraints", @@ -1459,8 +1713,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "", }, }, - want: true, - wantErr: false, + want: true, }, // SINGLE SAN TYPE PERMITTED SUCCESS TESTS { @@ -1471,8 +1724,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"example.local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/dns-permitted-wildcard", @@ -1486,8 +1738,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "test.x509local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/dns-permitted-wildcard-literal", @@ -1501,8 +1752,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "*.x509local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/dns-permitted-combined", @@ -1516,8 +1766,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "host.example.com", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/dns-permitted-idna-internationalized-domain", @@ -1529,8 +1778,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "JP納豆.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ipv4-permitted", @@ -1540,8 +1788,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ipv6-permitted", @@ -1551,8 +1798,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-permitted-wildcard", @@ -1564,8 +1810,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "test@example.com", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-permitted-plain-domain", @@ -1577,8 +1822,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "test@example.com", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-permitted-specific-mailbox", @@ -1590,8 +1834,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { "test@local.com", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-permitted-idna-internationalized-domain", @@ -1601,8 +1844,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-permitted-domain-wildcard", @@ -1617,8 +1859,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-permitted-specific-uri", @@ -1633,8 +1874,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-permitted-with-port", @@ -1649,8 +1889,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-permitted-idna-internationalized-domain", @@ -1665,8 +1904,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-permitted-idna-internationalized-domain", @@ -1681,8 +1919,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, // SINGLE SAN TYPE EXCLUDED SUCCESS TESTS { @@ -1693,8 +1930,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"example.local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ipv4-excluded", @@ -1709,8 +1945,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("10.10.10.10")}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ipv6-excluded", @@ -1720,8 +1955,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-excluded", @@ -1731,8 +1965,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-excluded-with-subdomain", @@ -1742,8 +1975,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-excluded", @@ -1758,8 +1990,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, // SUBJECT SUCCESS TESTS { @@ -1774,8 +2005,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, DNSNames: []string{"example.local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-dns-permitted", @@ -1788,8 +2018,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "example.local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-dns-excluded", @@ -1802,8 +2031,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "example.local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-ipv4-permitted", @@ -1821,8 +2049,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "127.0.0.20", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-ipv4-excluded", @@ -1840,8 +2067,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "127.0.0.1", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-ipv6-permitted", @@ -1859,8 +2085,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "2001:0db8:85a3:0000:0000:8a2e:0370:7339", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-ipv6-excluded", @@ -1878,8 +2103,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "2009:0db8:85a3:0000:0000:8a2e:0370:7339", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-email-permitted", @@ -1892,8 +2116,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "mail@example.local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-email-excluded", @@ -1906,8 +2129,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "mail@example.local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-uri-permitted", @@ -1920,8 +2142,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "https://www.example.com", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/subject-uri-excluded", @@ -1934,8 +2155,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { CommonName: "https://www.example.com", }, }, - want: true, - wantErr: false, + want: true, }, // DIFFERENT SAN TYPE EXCLUDED SUCCESS TESTS { @@ -1946,8 +2166,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else @@ -1957,8 +2176,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/dns-excluded-with-mail", // when only DNS is exluded, we allow anything else @@ -1973,8 +2191,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ip-excluded-with-dns", // when only IP is exluded, we allow anything else @@ -1984,8 +2201,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"test.local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ip-excluded-with-mail", // when only IP is exluded, we allow anything else @@ -1995,8 +2211,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.com"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/ip-excluded-with-mail", // when only IP is exluded, we allow anything else @@ -2011,8 +2226,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-excluded-with-dns", // when only mail is exluded, we allow anything else @@ -2022,8 +2236,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"test.local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-excluded-with-ip", // when only mail is exluded, we allow anything else @@ -2033,8 +2246,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/mail-excluded-with-uri", // when only mail is exluded, we allow anything else @@ -2049,8 +2261,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-excluded-with-dns", // when only URI is exluded, we allow anything else @@ -2060,8 +2271,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ DNSNames: []string{"test.example.local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-excluded-with-dns", // when only URI is exluded, we allow anything else @@ -2071,8 +2281,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/uri-excluded-with-mail", // when only URI is exluded, we allow anything else @@ -2082,8 +2291,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { cert: &x509.Certificate{ EmailAddresses: []string{"mail@example.local"}, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/dns-excluded-with-subject-ip-name", // when only DNS is exluded, we allow anything else @@ -2097,8 +2305,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, }, - want: true, - wantErr: false, + want: true, }, // COMBINED SUCCESS TESTS { @@ -2124,8 +2331,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/combined-simple-permitted-without-subject-verification", @@ -2149,8 +2355,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/combined-simple-all", @@ -2179,21 +2384,29 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, }, - want: true, - wantErr: false, + want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { engine, err := New(tt.options...) - assert.FatalError(t, err) + assert.NoError(t, err) got, err := engine.IsX509CertificateAllowed(tt.cert) - if (err != nil) != tt.wantErr { + wantErr := tt.wantErr != nil + + if (err != nil) != wantErr { t.Errorf("NamePolicyEngine.IsX509CertificateAllowed() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { - assert.NotEquals(t, "", err.Error()) // TODO(hs): implement a more specific error comparison? + var npe *NamePolicyError + assert.True(t, errors.As(err, &npe)) + assert.NotEqual(t, "", npe.Error()) + assert.Equal(t, tt.wantErr.Reason, npe.Reason) + assert.Equal(t, tt.wantErr.NameType, npe.NameType) + assert.Equal(t, tt.wantErr.Name, npe.Name) + assert.NotEqual(t, "", npe.Detail()) + //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail } if got != tt.want { t.Errorf("NamePolicyEngine.IsX509CertificateAllowed() = %v, want %v", got, tt.want) @@ -2208,12 +2421,20 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { URIs: tt.cert.URIs, } got, err = engine.IsX509CertificateRequestAllowed(csr) - if (err != nil) != tt.wantErr { + wantErr = tt.wantErr != nil + if (err != nil) != wantErr { t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { - assert.NotEquals(t, "", err.Error()) + var npe *NamePolicyError + assert.True(t, errors.As(err, &npe)) + assert.NotEqual(t, "", npe.Error()) + assert.Equal(t, tt.wantErr.Reason, npe.Reason) + assert.Equal(t, tt.wantErr.NameType, npe.NameType) + assert.Equal(t, tt.wantErr.Name, npe.Name) + assert.NotEqual(t, "", npe.Detail()) + //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail } if got != tt.want { t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() = %v, want %v", got, tt.want) @@ -2223,12 +2444,20 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { includeSubject := engine.verifySubjectCommonName // copy behavior of the engine when Subject has to be included as a SAN sans := extractSANs(tt.cert, includeSubject) got, err = engine.AreSANsAllowed(sans) - if (err != nil) != tt.wantErr { + wantErr = tt.wantErr != nil + if (err != nil) != wantErr { t.Errorf("NamePolicyEngine.AreSANsAllowed() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { - assert.NotEquals(t, "", err.Error()) + var npe *NamePolicyError + assert.True(t, errors.As(err, &npe)) + assert.NotEqual(t, "", npe.Error()) + assert.Equal(t, tt.wantErr.Reason, npe.Reason) + assert.Equal(t, tt.wantErr.NameType, npe.NameType) + assert.Equal(t, tt.wantErr.Name, npe.Name) + assert.NotEqual(t, "", npe.Detail()) + //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail } if got != tt.want { t.Errorf("NamePolicyEngine.AreSANsAllowed() = %v, want %v", got, tt.want) @@ -2243,7 +2472,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { options []NamePolicyOption cert *ssh.Certificate want bool - wantErr bool + wantErr *NamePolicyError }{ { name: "fail/host-with-permitted-dns-domain", @@ -2256,8 +2485,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "host.example.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "host.example.com", + }, }, { name: "fail/host-with-excluded-dns-domain", @@ -2270,8 +2503,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "host.local", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "host.local", + }, }, { name: "fail/host-with-permitted-cidr", @@ -2284,8 +2521,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "192.168.0.22", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "192.168.0.22", + }, }, { name: "fail/host-with-excluded-cidr", @@ -2298,8 +2539,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "127.0.0.0", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: IPNameType, + Name: "127.0.0.0", + }, }, { name: "fail/user-with-permitted-email", @@ -2312,8 +2557,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "mail@local", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "mail@local", + }, }, { name: "fail/user-with-excluded-email", @@ -2326,8 +2575,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "mail@example.com", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "mail@example.com", + }, }, { name: "fail/host-with-permitted-principals", @@ -2340,21 +2593,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "host", }, }, - want: false, - wantErr: true, - }, - { - name: "fail/host-with-excluded-principals", - options: []NamePolicyOption{ - WithExcludedPrincipals("localhost"), + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "host", }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{ - "localhost", - }, - }, - want: false, - wantErr: true, }, { name: "fail/user-with-permitted-principals", @@ -2367,8 +2611,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "root", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: PrincipalNameType, + Name: "root", + }, }, { name: "fail/user-with-excluded-principals", @@ -2381,8 +2629,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "user", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: PrincipalNameType, + Name: "user", + }, }, { name: "fail/user-with-permitted-principal-as-mail", @@ -2395,8 +2647,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "ops@work", // this is (currently) parsed as an email-like principal; not allowed with just "ops" as the permitted principal }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "ops@work", + }, }, { name: "fail/host-principal-with-permitted-dns-domain", // when only DNS is permitted, username principals are not allowed. @@ -2409,8 +2665,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "user", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "user", + }, }, { name: "fail/host-principal-with-permitted-ip-range", // when only IPs are permitted, username principals are not allowed. @@ -2423,8 +2683,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "user", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "user", + }, }, { name: "fail/user-principal-with-permitted-email", // when only emails are permitted, username principals are not allowed. @@ -2437,8 +2701,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "user", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: PrincipalNameType, + Name: "user", + }, }, { name: "fail/combined-user", @@ -2453,8 +2721,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "someone", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: PrincipalNameType, + Name: "someone", + }, }, { name: "fail/combined-user-with-excluded-user-principal", @@ -2469,11 +2741,15 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "root", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: PrincipalNameType, + Name: "root", + }, }, { - name: "ok/host-with-permitted-user-principals", + name: "fail/host-with-permitted-user-principals", options: []NamePolicyOption{ WithPermittedEmailAddresses("@work"), }, @@ -2483,11 +2759,15 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "example.work", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "example.work", + }, }, { - name: "ok/user-with-permitted-user-principals", + name: "fail/user-with-permitted-user-principals", options: []NamePolicyOption{ WithPermittedDNSDomains("*.local"), }, @@ -2497,8 +2777,12 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "herman@work", }, }, - want: false, - wantErr: true, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: EmailNameType, + Name: "herman@work", + }, }, { name: "ok/host-with-permitted-dns-domain", @@ -2511,8 +2795,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "host.local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/host-with-excluded-dns-domain", @@ -2525,8 +2808,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "host.local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/host-with-permitted-ip", @@ -2539,8 +2821,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "127.0.0.33", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/host-with-excluded-ip", @@ -2553,8 +2834,20 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "192.168.0.35", }, }, - want: true, - wantErr: false, + want: true, + }, + { + name: "ok/host-with-excluded-principals", + options: []NamePolicyOption{ + WithExcludedPrincipals("localhost"), + }, + cert: &ssh.Certificate{ + CertType: ssh.HostCert, + ValidPrincipals: []string{ + "localhost", + }, + }, + want: true, }, { name: "ok/user-with-permitted-email", @@ -2567,8 +2860,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "mail@example.com", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/user-with-excluded-email", @@ -2581,8 +2873,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "mail@local", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/user-with-permitted-principals", @@ -2595,8 +2886,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "user", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/user-with-excluded-principals", @@ -2609,8 +2899,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "root", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/combined-user", @@ -2626,8 +2915,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "someone", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/combined-user-with-excluded-user-principal", @@ -2643,8 +2931,7 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "someone", }, }, - want: true, - wantErr: false, + want: true, }, { name: "ok/combined-host", @@ -2661,19 +2948,29 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { "127.0.0.31", }, }, - want: true, - wantErr: false, + want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { engine, err := New(tt.options...) - assert.FatalError(t, err) + assert.NoError(t, err) got, err := engine.IsSSHCertificateAllowed(tt.cert) - if (err != nil) != tt.wantErr { + wantErr := tt.wantErr != nil + if (err != nil) != wantErr { t.Errorf("NamePolicyEngine.IsSSHCertificateAllowed() error = %v, wantErr %v", err, tt.wantErr) return } + if err != nil { + var npe *NamePolicyError + assert.True(t, errors.As(err, &npe)) + assert.NotEqual(t, "", npe.Error()) + assert.Equal(t, tt.wantErr.Reason, npe.Reason) + assert.Equal(t, tt.wantErr.NameType, npe.NameType) + assert.Equal(t, tt.wantErr.Name, npe.Name) + assert.NotEqual(t, "", npe.Detail()) + //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail + } if got != tt.want { t.Errorf("NamePolicyEngine.IsSSHCertificateAllowed() = %v, want %v", got, tt.want) } @@ -2844,3 +3141,191 @@ func Test_splitSSHPrincipals(t *testing.T) { }) } } + +func Test_removeDuplicates(t *testing.T) { + tests := []struct { + name string + input []string + want []string + }{ + { + name: "empty-slice", + input: []string{}, + want: []string{}, + }, + { + name: "single-item", + input: []string{"x"}, + want: []string{"x"}, + }, + { + name: "ok", + input: []string{"x", "y", "x", "z", "x", "z", "y"}, + want: []string{"x", "y", "z"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := removeDuplicates(tt.input); !reflect.DeepEqual(got, tt.want) { + t.Errorf("removeDuplicates() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_removeDuplicateIPNets(t *testing.T) { + tests := []struct { + name string + input []*net.IPNet + want []*net.IPNet + }{ + { + name: "empty-slice", + input: []*net.IPNet{}, + want: []*net.IPNet{}, + }, + { + name: "single-item", + input: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 255), + }, + }, + want: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 255), + }, + }, + }, + { + name: "multiple", + input: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 255), + }, + { + IP: net.ParseIP("192.168.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 255), + }, + { + IP: net.ParseIP("10.10.0.0"), + Mask: net.IPv4Mask(255, 255, 0, 0), + }, + { + IP: net.ParseIP("192.168.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 255), + }, + { + IP: net.ParseIP("192.168.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + want: []*net.IPNet{ + { + IP: net.ParseIP("127.0.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 255), + }, + { + IP: net.ParseIP("192.168.0.1"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + { + IP: net.ParseIP("10.10.0.0"), + Mask: net.IPv4Mask(255, 255, 0, 0), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotRet := removeDuplicateIPNets(tt.input); !reflect.DeepEqual(gotRet, tt.want) { + t.Errorf("removeDuplicateIPNets() = %v, want %v", gotRet, tt.want) + } + }) + } +} + +func TestNamePolicyError_Error(t *testing.T) { + type fields struct { + Reason NamePolicyReason + NameType NameType + Name string + detail string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "dns-not-allowed", + fields: fields{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "www.example.com", + }, + want: "dns name \"www.example.com\" not allowed", + }, + { + name: "dns-cannot-parse-domain", + fields: fields{ + Reason: CannotParseDomain, + NameType: DNSNameType, + Name: "www.example.com", + }, + want: "cannot parse dns domain \"www.example.com\"", + }, + { + name: "email-cannot-parse", + fields: fields{ + Reason: CannotParseRFC822Name, + NameType: EmailNameType, + Name: "mail@example.com", + }, + want: "cannot parse email rfc822Name \"mail@example.com\"", + }, + { + name: "uri-cannot-match", + fields: fields{ + Reason: CannotMatchNameToConstraint, + NameType: URINameType, + Name: "https://*.local", + }, + want: "error matching uri name \"https://*.local\" to constraint", + }, + { + name: "unknown", + fields: fields{ + Reason: -1, + NameType: DNSNameType, + Name: "some name", + detail: "detail string", + }, + want: "unknown error reason (-1): detail string", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &NamePolicyError{ + Reason: tt.fields.Reason, + NameType: tt.fields.NameType, + Name: tt.fields.Name, + detail: tt.fields.detail, + } + if got := e.Error(); got != tt.want { + t.Errorf("NamePolicyError.Error() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/policy/options_test.go b/policy/options_test.go index ca2908e4..d1a62a9f 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -5,8 +5,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - - "github.com/smallstep/assert" + "github.com/stretchr/testify/assert" ) 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 { _, 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") - assert.FatalError(t, err) + assert.NoError(t, err) options := []NamePolicyOption{ WithPermittedIPRanges(nw1, nw2), } @@ -389,9 +388,9 @@ func TestNew(t *testing.T) { }, "ok/with-excluded-ip-ranges": func(t *testing.T) test { _, 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") - assert.FatalError(t, err) + assert.NoError(t, err) options := []NamePolicyOption{ WithExcludedIPRanges(nw1, nw2), } @@ -410,9 +409,9 @@ func TestNew(t *testing.T) { }, "ok/with-permitted-cidrs": func(t *testing.T) test { _, 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") - assert.FatalError(t, err) + assert.NoError(t, err) options := []NamePolicyOption{ 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 { _, 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") - assert.FatalError(t, err) + assert.NoError(t, err) options := []NamePolicyOption{ 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 { _, 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") - assert.FatalError(t, err) + assert.NoError(t, err) _, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") - assert.FatalError(t, err) + assert.NoError(t, err) options := []NamePolicyOption{ 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 { _, 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") - assert.FatalError(t, err) + assert.NoError(t, err) _, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") - assert.FatalError(t, err) + assert.NoError(t, err) options := []NamePolicyOption{ WithExcludedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"), } diff --git a/policy/validate.go b/policy/validate.go index fd611b74..fff7120d 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -25,8 +25,6 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA 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 // 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 @@ -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). if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), + Reason: NotAllowed, + NameType: DNSNameType, + Name: dns, + detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns), } } didCutWildcard := false - if strings.HasPrefix(dns, "*.") { - dns = dns[1:] + parsedDNS := dns + if strings.HasPrefix(parsedDNS, "*.") { + parsedDNS = parsedDNS[1:] 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 { return &NamePolicyError{ - Reason: CannotParseDomain, - Detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns), + Reason: CannotParseDomain, + NameType: DNSNameType, + Name: dns, + detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns), } } if didCutWildcard { parsedDNS = "*" + parsedDNS } - if _, ok := domainToReverseLabels(parsedDNS); !ok { + if _, ok := domainToReverseLabels(parsedDNS); !ok { // TODO(hs): this also fails with spaces return &NamePolicyError{ - Reason: CannotParseDomain, - Detail: fmt.Sprintf("cannot parse dns %q", dns), + Reason: CannotParseDomain, + NameType: DNSNameType, + Name: dns, + detail: fmt.Sprintf("cannot parse dns %q", dns), } } if err := checkNameConstraints("dns", dns, parsedDNS, @@ -76,8 +82,10 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA for _, ip := range ips { if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()), + Reason: NotAllowed, + 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, @@ -91,15 +99,19 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA for _, email := range emailAddresses { if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email), + Reason: NotAllowed, + NameType: EmailNameType, + Name: email, + detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email), } } mailbox, ok := parseRFC2821Mailbox(email) if !ok { return &NamePolicyError{ - Reason: CannotParseRFC822Name, - Detail: fmt.Sprintf("invalid rfc822Name %q", mailbox), + Reason: CannotParseRFC822Name, + 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 @@ -108,8 +120,10 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA domainASCII, err := idna.ToASCII(mailbox.domain) if err != nil { return &NamePolicyError{ - Reason: CannotParseDomain, - Detail: fmt.Sprintf("cannot parse email domain %q", email), + Reason: CannotParseDomain, + NameType: EmailNameType, + Name: email, + detail: fmt.Sprintf("cannot parse email domain %q", email), } } mailbox.domain = domainASCII @@ -126,10 +140,14 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA for _, uri := range uris { if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()), + Reason: NotAllowed, + 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, func(parsedName, constraint interface{}) (bool, error) { 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 { if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 { return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal), + Reason: NotAllowed, + 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? @@ -175,15 +195,19 @@ func checkNameConstraints( match, err := match(parsedName, constraint) if err != nil { return &NamePolicyError{ - Reason: CannotMatchNameToConstraint, - Detail: err.Error(), + Reason: CannotMatchNameToConstraint, + NameType: NameType(nameType), + Name: name, + detail: err.Error(), } } if match { return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), + Reason: NotAllowed, + 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 if ok, err = match(parsedName, constraint); err != nil { return &NamePolicyError{ - Reason: CannotMatchNameToConstraint, - Detail: err.Error(), + Reason: CannotMatchNameToConstraint, + NameType: NameType(nameType), + Name: name, + detail: err.Error(), } } @@ -208,8 +234,10 @@ func checkNameConstraints( if !ok { return &NamePolicyError{ - Reason: NotAuthorizedForThisName, - Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), + Reason: NotAllowed, + NameType: NameType(nameType), + Name: name, + detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), } } From 2a7620641f74474a7a055e20c779e6c70293c32d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 26 Apr 2022 10:15:17 +0200 Subject: [PATCH 63/78] Fix more PR comments --- acme/account.go | 6 +- acme/account_test.go | 9 +- acme/api/account_test.go | 4 +- acme/api/eab.go | 4 +- acme/api/eab_test.go | 18 +- acme/db/nosql/eab.go | 13 +- acme/db/nosql/eab_test.go | 59 ++--- authority/admin/api/acme.go | 4 +- authority/admin/api/acme_test.go | 10 +- authority/admin/api/policy.go | 28 +- authority/admin/api/policy_test.go | 359 ++++++++++++-------------- authority/linkedca.go | 7 + authority/policy.go | 5 +- authority/policy/options.go | 20 +- authority/policy/options_test.go | 6 +- authority/policy/policy.go | 2 +- authority/policy_test.go | 20 +- authority/provisioner/options.go | 19 +- authority/provisioner/options_test.go | 32 +-- authority/tls_test.go | 2 +- ca/ca.go | 2 +- 21 files changed, 298 insertions(+), 331 deletions(-) diff --git a/acme/account.go b/acme/account.go index 18c0d646..b83defe1 100644 --- a/acme/account.go +++ b/acme/account.go @@ -91,7 +91,7 @@ func (p *Policy) IsWildcardLiteralAllowed() bool { // ShouldVerifySubjectCommonName returns true by default // for ACME account policies, as this is embedded in the // protocol. -func (p *Policy) ShouldVerifySubjectCommonName() bool { +func (p *Policy) ShouldVerifyCommonName() bool { return true } @@ -101,7 +101,7 @@ type ExternalAccountKey struct { ProvisionerID string `json:"provisionerID"` Reference string `json:"reference"` AccountID string `json:"-"` - KeyBytes []byte `json:"-"` + HmacKey []byte `json:"-"` CreatedAt time.Time `json:"createdAt"` BoundAt time.Time `json:"boundAt,omitempty"` Policy *Policy `json:"policy,omitempty"` @@ -121,6 +121,6 @@ func (eak *ExternalAccountKey) BindTo(account *Account) error { } eak.AccountID = account.ID eak.BoundAt = time.Now() - eak.KeyBytes = []byte{} // clearing the key bytes; can only be used once + eak.HmacKey = []byte{} // clearing the key bytes; can only be used once return nil } diff --git a/acme/account_test.go b/acme/account_test.go index 33524d87..edd1f5b0 100644 --- a/acme/account_test.go +++ b/acme/account_test.go @@ -7,8 +7,9 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/assert" "go.step.sm/crypto/jose" + + "github.com/smallstep/assert" ) func TestKeyToID(t *testing.T) { @@ -95,7 +96,7 @@ func TestExternalAccountKey_BindTo(t *testing.T) { ID: "eakID", ProvisionerID: "provID", Reference: "ref", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, }, acct: &Account{ ID: "accountID", @@ -108,7 +109,7 @@ func TestExternalAccountKey_BindTo(t *testing.T) { ID: "eakID", ProvisionerID: "provID", Reference: "ref", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, AccountID: "someAccountID", BoundAt: boundAt, }, @@ -138,7 +139,7 @@ func TestExternalAccountKey_BindTo(t *testing.T) { assert.Equals(t, ae.Subproblems, tt.err.Subproblems) } else { assert.Equals(t, eak.AccountID, acct.ID) - assert.Equals(t, eak.KeyBytes, []byte{}) + assert.Equals(t, eak.HmacKey, []byte{}) assert.NotNil(t, eak.BoundAt) } }) diff --git a/acme/api/account_test.go b/acme/api/account_test.go index e389b57f..a0161cb4 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -582,7 +582,7 @@ func TestHandler_NewAccount(t *testing.T) { ID: "eakID", ProvisionerID: provID, Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Now(), } return test{ @@ -759,7 +759,7 @@ func TestHandler_NewAccount(t *testing.T) { ID: "eakID", ProvisionerID: provID, Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Now(), }, nil }, diff --git a/acme/api/eab.go b/acme/api/eab.go index 6be906d4..84be6453 100644 --- a/acme/api/eab.go +++ b/acme/api/eab.go @@ -60,7 +60,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key") } - if len(externalAccountKey.KeyBytes) == 0 { + if len(externalAccountKey.HmacKey) == 0 { return nil, acme.NewError(acme.ErrorServerInternalType, "external account binding key with id '%s' does not have secret bytes", keyID) } @@ -68,7 +68,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt) } - payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) + payload, err := eabJWS.Verify(externalAccountKey.HmacKey) if err != nil { return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") } diff --git a/acme/api/eab_test.go b/acme/api/eab_test.go index 760c122c..c2725588 100644 --- a/acme/api/eab_test.go +++ b/acme/api/eab_test.go @@ -156,7 +156,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ID: "eakID", ProvisionerID: provID, Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: createdAt, }, nil }, @@ -170,7 +170,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ID: "eakID", ProvisionerID: provID, Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: createdAt, }, err: nil, @@ -523,7 +523,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { Reference: "testeak", CreatedAt: createdAt, AccountID: "some-account-id", - KeyBytes: []byte{}, + HmacKey: []byte{}, }, nil }, }, @@ -630,7 +630,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { Reference: "testeak", CreatedAt: createdAt, AccountID: "some-account-id", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, BoundAt: boundAt, }, nil }, @@ -686,7 +686,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ID: "eakID", ProvisionerID: provID, Reference: "testeak", - KeyBytes: []byte{1, 2, 3, 4}, + HmacKey: []byte{1, 2, 3, 4}, CreatedAt: time.Now(), }, nil }, @@ -744,7 +744,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ID: "eakID", ProvisionerID: provID, Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Now(), }, nil }, @@ -799,7 +799,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ID: "eakID", ProvisionerID: provID, Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Now(), }, nil }, @@ -855,7 +855,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ID: "eakID", ProvisionerID: provID, Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Now(), }, nil }, @@ -898,7 +898,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { } else { assert.NotNil(t, tc.eak) assert.Equals(t, got.ID, tc.eak.ID) - assert.Equals(t, got.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, got.HmacKey, tc.eak.HmacKey) assert.Equals(t, got.ProvisionerID, tc.eak.ProvisionerID) assert.Equals(t, got.Reference, tc.eak.Reference) assert.Equals(t, got.CreatedAt, tc.eak.CreatedAt) diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go index 5c34c20c..e87aa9bc 100644 --- a/acme/db/nosql/eab.go +++ b/acme/db/nosql/eab.go @@ -8,6 +8,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" nosqlDB "github.com/smallstep/nosql" ) @@ -23,7 +24,7 @@ type dbExternalAccountKey struct { ProvisionerID string `json:"provisionerID"` Reference string `json:"reference"` AccountID string `json:"accountID,omitempty"` - KeyBytes []byte `json:"key"` + HmacKey []byte `json:"key"` CreatedAt time.Time `json:"createdAt"` BoundAt time.Time `json:"boundAt"` } @@ -72,7 +73,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, refer ID: keyID, ProvisionerID: provisionerID, Reference: reference, - KeyBytes: random, + HmacKey: random, CreatedAt: clock.Now(), } @@ -99,7 +100,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, refer ProvisionerID: dbeak.ProvisionerID, Reference: dbeak.Reference, AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, + HmacKey: dbeak.HmacKey, CreatedAt: dbeak.CreatedAt, BoundAt: dbeak.BoundAt, }, nil @@ -124,7 +125,7 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID st ProvisionerID: dbeak.ProvisionerID, Reference: dbeak.Reference, AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, + HmacKey: dbeak.HmacKey, CreatedAt: dbeak.CreatedAt, BoundAt: dbeak.BoundAt, }, nil @@ -191,7 +192,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor } keys = append(keys, &acme.ExternalAccountKey{ ID: eak.ID, - KeyBytes: eak.KeyBytes, + HmacKey: eak.HmacKey, ProvisionerID: eak.ProvisionerID, Reference: eak.Reference, AccountID: eak.AccountID, @@ -256,7 +257,7 @@ func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string ProvisionerID: eak.ProvisionerID, Reference: eak.Reference, AccountID: eak.AccountID, - KeyBytes: eak.KeyBytes, + HmacKey: eak.HmacKey, CreatedAt: eak.CreatedAt, BoundAt: eak.BoundAt, } diff --git a/acme/db/nosql/eab_test.go b/acme/db/nosql/eab_test.go index 568500e9..525afa72 100644 --- a/acme/db/nosql/eab_test.go +++ b/acme/db/nosql/eab_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/pkg/errors" + "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" certdb "github.com/smallstep/certificates/db" @@ -32,7 +33,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: "ref", AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b, err := json.Marshal(dbeak) @@ -108,7 +109,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) { } } else if assert.Nil(t, tc.err) { assert.Equals(t, dbeak.ID, tc.dbeak.ID) - assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) + assert.Equals(t, dbeak.HmacKey, tc.dbeak.HmacKey) assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID) assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) @@ -136,7 +137,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: "ref", AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b, err := json.Marshal(dbeak) @@ -154,7 +155,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: "ref", AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, }, } @@ -179,7 +180,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) { ProvisionerID: "aDifferentProvID", Reference: "ref", AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b, err := json.Marshal(dbeak) @@ -197,7 +198,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: "ref", AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, }, acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"), @@ -225,7 +226,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) { } } else if assert.Nil(t, tc.err) { assert.Equals(t, eak.ID, tc.eak.ID) - assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.HmacKey, tc.eak.HmacKey) assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) assert.Equals(t, eak.Reference, tc.eak.Reference) assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) @@ -255,7 +256,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ @@ -288,7 +289,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, }, err: nil, @@ -392,7 +393,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { assert.Equals(t, eak.AccountID, tc.eak.AccountID) assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.HmacKey, tc.eak.HmacKey) assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) assert.Equals(t, eak.Reference, tc.eak.Reference) } @@ -420,7 +421,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b1, err := json.Marshal(dbeak1) @@ -430,7 +431,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b2, err := json.Marshal(dbeak2) @@ -440,7 +441,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { ProvisionerID: "aDifferentProvID", Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b3, err := json.Marshal(dbeak3) @@ -513,7 +514,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, }, { @@ -521,7 +522,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, }, }, @@ -598,7 +599,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { assert.Equals(t, "", nextCursor) for i, eak := range eaks { assert.Equals(t, eak.ID, tc.eaks[i].ID) - assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) + assert.Equals(t, eak.HmacKey, tc.eaks[i].HmacKey) assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID) assert.Equals(t, eak.Reference, tc.eaks[i].Reference) assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) @@ -627,7 +628,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ @@ -707,7 +708,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { ProvisionerID: "aDifferentProvID", Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b, err := json.Marshal(dbeak) @@ -730,7 +731,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ @@ -780,7 +781,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ @@ -830,7 +831,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ @@ -953,7 +954,7 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { assert.Equals(t, string(key), dbeak.ID) assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID) assert.Equals(t, eak.Reference, dbeak.Reference) - assert.Equals(t, 32, len(dbeak.KeyBytes)) + assert.Equals(t, 32, len(dbeak.HmacKey)) assert.False(t, dbeak.CreatedAt.IsZero()) assert.Equals(t, dbeak.AccountID, eak.AccountID) assert.True(t, dbeak.BoundAt.IsZero()) @@ -1078,7 +1079,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b, err := json.Marshal(dbeak) @@ -1096,7 +1097,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } return test{ @@ -1120,7 +1121,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { assert.Equals(t, dbNew.AccountID, dbeak.AccountID) assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt) - assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes) + assert.Equals(t, dbNew.HmacKey, dbeak.HmacKey) return nu, true, nil }, }, @@ -1148,7 +1149,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { ProvisionerID: "aDifferentProvID", Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b, err := json.Marshal(newDBEAK) @@ -1174,7 +1175,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b, err := json.Marshal(newDBEAK) @@ -1200,7 +1201,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { ProvisionerID: provID, Reference: ref, AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: now, } b, err := json.Marshal(newDBEAK) @@ -1237,7 +1238,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) - assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, dbeak.HmacKey, tc.eak.HmacKey) } }) } diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index cd4b1e17..026443fa 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -90,7 +90,7 @@ func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey { eak := &linkedca.EABKey{ Id: k.ID, - HmacKey: k.KeyBytes, + HmacKey: k.HmacKey, Provisioner: k.ProvisionerID, Reference: k.Reference, Account: k.AccountID, @@ -124,7 +124,7 @@ func linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey { ProvisionerID: k.Provisioner, Reference: k.Reference, AccountID: k.Account, - KeyBytes: k.HmacKey, + HmacKey: k.HmacKey, CreatedAt: k.CreatedAt.AsTime(), BoundAt: k.BoundAt.AsTime(), } diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index aa4aa608..5094d5f0 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -364,7 +364,7 @@ func Test_eakToLinked(t *testing.T) { ProvisionerID: "provID", Reference: "ref", AccountID: "accID", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), Policy: nil, @@ -387,7 +387,7 @@ func Test_eakToLinked(t *testing.T) { ProvisionerID: "provID", Reference: "ref", AccountID: "accID", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), Policy: &acme.Policy{ @@ -463,7 +463,7 @@ func Test_linkedEAKToCertificates(t *testing.T) { ProvisionerID: "provID", Reference: "ref", AccountID: "accID", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), Policy: nil, @@ -486,7 +486,7 @@ func Test_linkedEAKToCertificates(t *testing.T) { ProvisionerID: "provID", Reference: "ref", AccountID: "accID", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), Policy: &acme.Policy{}, @@ -520,7 +520,7 @@ func Test_linkedEAKToCertificates(t *testing.T) { ProvisionerID: "provID", Reference: "ref", AccountID: "accID", - KeyBytes: []byte{1, 3, 3, 7}, + HmacKey: []byte{1, 3, 3, 7}, CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour), BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC), Policy: &acme.Policy{ diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index c697b67a..70c6f01d 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -30,19 +30,25 @@ type policyAdminResponderInterface interface { // PolicyAdminResponder is responsible for writing ACME admin responses type PolicyAdminResponder struct { - auth adminAuthority - adminDB admin.DB - acmeDB acme.DB - deploymentType string + auth adminAuthority + adminDB admin.DB + acmeDB acme.DB + isLinkedCA bool } // NewACMEAdminResponder returns a new ACMEAdminResponder -func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, deploymentType string) *PolicyAdminResponder { +func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB) *PolicyAdminResponder { + + var isLinkedCA bool + if a, ok := adminDB.(interface{ IsLinkedCA() bool }); ok { + isLinkedCA = a.IsLinkedCA() + } + return &PolicyAdminResponder{ - auth: auth, - adminDB: adminDB, - acmeDB: acmeDB, - deploymentType: deploymentType, + auth: auth, + adminDB: adminDB, + acmeDB: acmeDB, + isLinkedCA: isLinkedCA, } } @@ -435,8 +441,8 @@ func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, // blockLinkedCA blocks all API operations on linked deployments func (par *PolicyAdminResponder) blockLinkedCA() error { - // temporary blocking linked deployments based on string comparison (preventing import cycle) - if par.deploymentType == "linked" { + // temporary blocking linked deployments + if par.isLinkedCA { return admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") } return nil diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index ee97c2cc..fffa84f7 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -21,15 +21,22 @@ import ( "github.com/smallstep/certificates/authority/admin" ) +type fakeLinkedCA struct { + admin.MockDB +} + +func (f *fakeLinkedCA) IsLinkedCA() bool { + return true +} + func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { type test struct { - auth adminAuthority - deploymentType string - adminDB admin.DB - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + adminDB admin.DB + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -37,10 +44,10 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { @@ -97,11 +104,8 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(tc.auth, tc.adminDB, nil) req := httptest.NewRequest("GET", "/foo", nil) req = req.WithContext(tc.ctx) @@ -139,15 +143,14 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { type test struct { - auth adminAuthority - deploymentType string - adminDB admin.DB - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -155,10 +158,10 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { @@ -343,12 +346,8 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) @@ -395,15 +394,14 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { type test struct { - auth adminAuthority - deploymentType string - adminDB admin.DB - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -411,10 +409,10 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { @@ -606,12 +604,8 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) @@ -658,14 +652,13 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { type test struct { - auth adminAuthority - deploymentType string - adminDB admin.DB - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - statusCode int + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -674,10 +667,10 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/auth.GetAuthorityPolicy-error": func(t *testing.T) test { @@ -762,12 +755,8 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) @@ -809,14 +798,13 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { type test struct { - auth adminAuthority - deploymentType string - adminDB admin.DB - ctx context.Context - acmeDB acme.DB - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + adminDB admin.DB + ctx context.Context + acmeDB acme.DB + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -824,10 +812,10 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/prov-no-policy": func(t *testing.T) test { @@ -863,12 +851,8 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("GET", "/foo", nil) req = req.WithContext(tc.ctx) @@ -906,13 +890,13 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { type test struct { - auth adminAuthority - deploymentType string - body []byte - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -920,10 +904,10 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/existing-policy": func(t *testing.T) test { @@ -1067,10 +1051,8 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - auth: tc.auth, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(tc.auth, tc.adminDB, nil) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) @@ -1117,13 +1099,13 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { type test struct { - auth adminAuthority - deploymentType string - body []byte - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + auth adminAuthority + body []byte + adminDB admin.DB + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -1131,10 +1113,10 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/no-existing-policy": func(t *testing.T) test { @@ -1280,10 +1262,8 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - auth: tc.auth, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(tc.auth, tc.adminDB, nil) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) @@ -1330,14 +1310,13 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { type test struct { - auth adminAuthority - deploymentType string - adminDB admin.DB - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - statusCode int + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -1346,10 +1325,10 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/no-existing-policy": func(t *testing.T) test { @@ -1404,12 +1383,8 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - auth: tc.auth, - adminDB: tc.adminDB, - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) @@ -1451,12 +1426,12 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { type test struct { - deploymentType string - ctx context.Context - acmeDB acme.DB - err *admin.Error - policy *linkedca.Policy - statusCode int + ctx context.Context + acmeDB acme.DB + adminDB admin.DB + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -1464,10 +1439,10 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/no-policy": func(t *testing.T) test { @@ -1514,10 +1489,8 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(nil, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("GET", "/foo", nil) req = req.WithContext(tc.ctx) @@ -1555,13 +1528,13 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { type test struct { - deploymentType string - acmeDB acme.DB - body []byte - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + acmeDB acme.DB + adminDB admin.DB + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -1569,10 +1542,10 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/existing-policy": func(t *testing.T) test { @@ -1691,10 +1664,8 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(nil, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) @@ -1741,13 +1712,13 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { type test struct { - deploymentType string - acmeDB acme.DB - body []byte - ctx context.Context - err *admin.Error - policy *linkedca.Policy - statusCode int + acmeDB acme.DB + adminDB admin.DB + body []byte + ctx context.Context + err *admin.Error + policy *linkedca.Policy + statusCode int } var tests = map[string]func(t *testing.T) test{ "fail/linkedca": func(t *testing.T) test { @@ -1755,10 +1726,10 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/no-existing-policy": func(t *testing.T) test { @@ -1879,10 +1850,8 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(nil, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) @@ -1929,12 +1898,12 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { type test struct { - deploymentType string - body []byte - ctx context.Context - acmeDB acme.DB - err *admin.Error - statusCode int + body []byte + adminDB admin.DB + ctx context.Context + acmeDB acme.DB + err *admin.Error + statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -1943,10 +1912,10 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments") err.Message = "policy operations not yet supported in linked deployments" return test{ - ctx: ctx, - deploymentType: "linked", - err: err, - statusCode: 501, + ctx: ctx, + adminDB: &fakeLinkedCA{}, + err: err, + statusCode: 501, } }, "fail/no-existing-policy": func(t *testing.T) test { @@ -2033,10 +2002,8 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - par := &PolicyAdminResponder{ - acmeDB: tc.acmeDB, - deploymentType: tc.deploymentType, - } + + par := NewPolicyAdminResponder(nil, tc.adminDB, tc.acmeDB) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) diff --git a/authority/linkedca.go b/authority/linkedca.go index 2e6ed61a..0552f2d1 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -122,6 +122,13 @@ func newLinkedCAClient(token string) (*linkedCaClient, error) { }, nil } +// IsLinkedCA is a sentinel function that can be used to +// check if a linkedCaClient is the underlying type of an +// admin.DB interface. +func (c *linkedCaClient) IsLinkedCA() bool { + return true +} + func (c *linkedCaClient) Run() { c.renewer.Run() } diff --git a/authority/policy.go b/authority/policy.go index f71e37c7..08a26f16 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -15,8 +15,7 @@ import ( type policyErrorType int const ( - _ policyErrorType = iota - AdminLockOut + AdminLockOut policyErrorType = iota + 1 StoreFailure ReloadFailure ConfigurationFailure @@ -345,7 +344,7 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { } opts.X509.AllowWildcardLiteral = x509.AllowWildcardLiteral - opts.X509.DisableSubjectCommonNameVerification = x509.DisableSubjectCommonNameVerification + opts.X509.DisableCommonNameVerification = x509.DisableSubjectCommonNameVerification } // fill ssh policy configuration diff --git a/authority/policy/options.go b/authority/policy/options.go index ff0eec3d..c4b7b9ce 100644 --- a/authority/policy/options.go +++ b/authority/policy/options.go @@ -31,7 +31,7 @@ type X509PolicyOptionsInterface interface { GetAllowedNameOptions() *X509NameOptions GetDeniedNameOptions() *X509NameOptions IsWildcardLiteralAllowed() bool - ShouldVerifySubjectCommonName() bool + ShouldVerifyCommonName() bool } // X509PolicyOptions is a container for x509 allowed and denied @@ -39,15 +39,19 @@ type X509PolicyOptionsInterface interface { type X509PolicyOptions struct { // AllowedNames contains the x509 allowed names AllowedNames *X509NameOptions `json:"allow,omitempty"` + // DeniedNames contains the x509 denied names DeniedNames *X509NameOptions `json:"deny,omitempty"` + // AllowWildcardLiteral indicates if literal wildcard names // such as *.example.com and @example.com are allowed. Defaults // to false. - AllowWildcardLiteral bool `json:"allow_wildcard_literal,omitempty"` - // DisableSubjectCommonNameVerification indicates if the Subject Common Name - // is verified in addition to the SANs. Defaults to false. - DisableSubjectCommonNameVerification bool `json:"disable_subject_common_name_verification,omitempty"` + AllowWildcardLiteral bool `json:"allowWildcardLiteral,omitempty"` + + // DisableCommonNameVerification indicates if the Subject Common Name + // is verified in addition to the SANs. Defaults to false, resulting in + // Common Names being verified. + DisableCommonNameVerification bool `json:"disableCommonNameVerification,omitempty"` } // X509NameOptions models the X509 name policy configuration. @@ -92,13 +96,13 @@ func (o *X509PolicyOptions) IsWildcardLiteralAllowed() bool { return o.AllowWildcardLiteral } -// ShouldVerifySubjectCommonName returns whether the authority +// ShouldVerifyCommonName returns whether the authority // should verify the Subject Common Name in addition to the SANs. -func (o *X509PolicyOptions) ShouldVerifySubjectCommonName() bool { +func (o *X509PolicyOptions) ShouldVerifyCommonName() bool { if o == nil { return false } - return !o.DisableSubjectCommonNameVerification + return !o.DisableCommonNameVerification } // SSHPolicyOptionsInterface is an interface for providers of diff --git a/authority/policy/options_test.go b/authority/policy/options_test.go index ebcd90fe..b4f456a1 100644 --- a/authority/policy/options_test.go +++ b/authority/policy/options_test.go @@ -63,21 +63,21 @@ func TestX509PolicyOptions_ShouldVerifySubjectCommonName(t *testing.T) { { name: "set-true", options: &X509PolicyOptions{ - DisableSubjectCommonNameVerification: true, + DisableCommonNameVerification: true, }, want: false, }, { name: "set-false", options: &X509PolicyOptions{ - DisableSubjectCommonNameVerification: false, + DisableCommonNameVerification: false, }, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.options.ShouldVerifySubjectCommonName(); got != tt.want { + if got := tt.options.ShouldVerifyCommonName(); got != tt.want { t.Errorf("X509PolicyOptions.ShouldVerifySubjectCommonName() = %v, want %v", got, tt.want) } }) diff --git a/authority/policy/policy.go b/authority/policy/policy.go index 564fca24..b68bcb19 100644 --- a/authority/policy/policy.go +++ b/authority/policy/policy.go @@ -50,7 +50,7 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, return nil, nil } - if policyOptions.ShouldVerifySubjectCommonName() { + if policyOptions.ShouldVerifyCommonName() { options = append(options, policy.WithSubjectCommonNameVerification()) } diff --git a/authority/policy_test.go b/authority/policy_test.go index 075987c0..f444a7f0 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -227,8 +227,8 @@ func Test_policyToCertificates(t *testing.T) { AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.local"}, }, - AllowWildcardLiteral: false, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: false, + DisableCommonNameVerification: false, }, }, }, @@ -290,8 +290,8 @@ func Test_policyToCertificates(t *testing.T) { EmailAddresses: []string{"badhost.example.com"}, URIDomains: []string{"https://badhost.local"}, }, - AllowWildcardLiteral: true, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: true, + DisableCommonNameVerification: false, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -364,8 +364,8 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: true, + DisableCommonNameVerification: false, }) assert.NoError(t, err) @@ -648,8 +648,8 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: true, + DisableCommonNameVerification: false, }, }, }, @@ -768,8 +768,8 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: true, + DisableCommonNameVerification: false, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 12e371a6..406b23b4 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -69,10 +69,12 @@ type X509Options struct { // AllowWildcardLiteral indicates if literal wildcard names // such as *.example.com and @example.com are allowed. Defaults // to false. - AllowWildcardLiteral *bool `json:"-"` - // VerifySubjectCommonName indicates if the Subject Common Name - // is verified in addition to the SANs. Defaults to true. - VerifySubjectCommonName *bool `json:"-"` + AllowWildcardLiteral bool `json:"-"` + + // DisableCommonNameVerification indicates if the Subject Common Name + // is verified in addition to the SANs. Defaults to false, resulting + // in Common Names to be verified. + DisableCommonNameVerification bool `json:"-"` } // HasTemplate returns true if a template is defined in the provisioner options. @@ -102,17 +104,14 @@ func (o *X509Options) IsWildcardLiteralAllowed() bool { if o == nil { return true } - return o.AllowWildcardLiteral != nil && *o.AllowWildcardLiteral + return o.AllowWildcardLiteral } -func (o *X509Options) ShouldVerifySubjectCommonName() bool { +func (o *X509Options) ShouldVerifyCommonName() bool { if o == nil { return false } - if o.VerifySubjectCommonName == nil { - return true - } - return *o.VerifySubjectCommonName + return !o.DisableCommonNameVerification } // TemplateOptions generates a CertificateOptions with the template and data diff --git a/authority/provisioner/options_test.go b/authority/provisioner/options_test.go index 32bea92b..2edcdf3e 100644 --- a/authority/provisioner/options_test.go +++ b/authority/provisioner/options_test.go @@ -289,8 +289,6 @@ func Test_unsafeParseSigned(t *testing.T) { } func TestX509Options_IsWildcardLiteralAllowed(t *testing.T) { - trueValue := true - falseValue := false tests := []struct { name string options *X509Options @@ -301,24 +299,17 @@ func TestX509Options_IsWildcardLiteralAllowed(t *testing.T) { options: nil, want: true, }, - { - name: "nil", - options: &X509Options{ - AllowWildcardLiteral: nil, - }, - want: false, - }, { name: "set-true", options: &X509Options{ - AllowWildcardLiteral: &trueValue, + AllowWildcardLiteral: true, }, want: true, }, { name: "set-false", options: &X509Options{ - AllowWildcardLiteral: &falseValue, + AllowWildcardLiteral: false, }, want: false, }, @@ -333,8 +324,6 @@ func TestX509Options_IsWildcardLiteralAllowed(t *testing.T) { } func TestX509Options_ShouldVerifySubjectCommonName(t *testing.T) { - trueValue := true - falseValue := false tests := []struct { name string options *X509Options @@ -345,31 +334,24 @@ func TestX509Options_ShouldVerifySubjectCommonName(t *testing.T) { options: nil, want: false, }, - { - name: "nil", - options: &X509Options{ - VerifySubjectCommonName: nil, - }, - want: true, - }, { name: "set-true", options: &X509Options{ - VerifySubjectCommonName: &trueValue, + DisableCommonNameVerification: true, }, - want: true, + want: false, }, { name: "set-false", options: &X509Options{ - VerifySubjectCommonName: &falseValue, + DisableCommonNameVerification: false, }, - want: false, + want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.options.ShouldVerifySubjectCommonName(); got != tt.want { + if got := tt.options.ShouldVerifyCommonName(); got != tt.want { t.Errorf("X509PolicyOptions.ShouldVerifySubjectCommonName() = %v, want %v", got, tt.want) } }) diff --git a/authority/tls_test.go b/authority/tls_test.go index a96ce1eb..3f9946ba 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -700,7 +700,7 @@ ZYtQ9Ot36qc= AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.smallstep.com"}, }, - DisableSubjectCommonNameVerification: true, // allows "smallstep test" + DisableCommonNameVerification: true, // TODO(hs): allows "smallstep test"; do we want to keep it like this? } engine, err := policy.NewX509PolicyEngine(policyOptions) assert.FatalError(t, err) diff --git a/ca/ca.go b/ca/ca.go index a08dc9e9..3cb4646b 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -219,7 +219,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { adminDB := auth.GetAdminDatabase() if adminDB != nil { acmeAdminResponder := adminAPI.NewACMEAdminResponder() - policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB, acmeDB, cfg.AuthorityConfig.DeploymentType) + policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB, acmeDB) adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder, policyAdminResponder) mux.Route("/admin", func(r chi.Router) { adminHandler.Route(r) From 6e1f8dd7aba9f76ebcc925606f5961fb63f5fca2 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 26 Apr 2022 13:12:16 +0200 Subject: [PATCH 64/78] Refactor policy engines into container --- acme/api/order.go | 10 +- authority/authority.go | 4 +- authority/policy.go | 48 +-- authority/policy/engine.go | 114 ++++++ authority/policy_test.go | 438 +++++++++++----------- authority/provisioner/acme.go | 4 +- authority/provisioner/sign_options.go | 3 +- authority/provisioner/sign_ssh_options.go | 6 +- authority/ssh.go | 73 +--- authority/ssh_test.go | 105 +++--- authority/tls.go | 34 +- authority/tls_test.go | 26 +- policy/engine.go | 41 +- policy/engine_test.go | 52 +-- policy/ssh.go | 2 +- policy/x509.go | 10 +- 16 files changed, 491 insertions(+), 479 deletions(-) create mode 100644 authority/policy/engine.go diff --git a/acme/api/order.go b/acme/api/order.go index 5bf35a58..c37285d2 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "encoding/base64" "encoding/json" - "fmt" "net" "net/http" "strings" @@ -199,14 +198,7 @@ func isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifie if acmePolicy == nil { return nil } - allowed, err := acmePolicy.AreSANsAllowed([]string{identifier.Value}) - if err != nil { - return err - } - if !allowed { - return fmt.Errorf("acme identifier '%s' not allowed", identifier.Value) - } - return nil + return acmePolicy.AreSANsAllowed([]string{identifier.Value}) } func newACMEPolicyEngine(eak *acme.ExternalAccountKey) (policy.X509Policy, error) { diff --git a/authority/authority.go b/authority/authority.go index 84864159..8a0013c0 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -81,9 +81,7 @@ type Authority struct { authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc // Policy engines - x509Policy policy.X509Policy - sshUserPolicy policy.UserPolicy - sshHostPolicy policy.HostPolicy + policyEngine *policy.Engine adminMutex sync.RWMutex } diff --git a/authority/policy.go b/authority/policy.go index 08a26f16..47104e0e 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -227,50 +227,19 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { policyOptions = a.config.AuthorityConfig.Policy } - // if no new or updated policy option is set, clear policy engines that (may have) - // been configured before and return early - if policyOptions == nil { - a.x509Policy = nil - a.sshHostPolicy = nil - a.sshUserPolicy = nil - return nil - } - - var ( - x509Policy authPolicy.X509Policy - sshHostPolicy authPolicy.HostPolicy - sshUserPolicy authPolicy.UserPolicy - ) - - // initialize the x509 allow/deny policy engine - if x509Policy, err = authPolicy.NewX509PolicyEngine(policyOptions.GetX509Options()); err != nil { + engine, err := authPolicy.New(policyOptions) + if err != nil { return err } - // initialize the SSH allow/deny policy engine for host certificates - if sshHostPolicy, err = authPolicy.NewSSHHostPolicyEngine(policyOptions.GetSSHOptions()); err != nil { - return err - } - - // initialize the SSH allow/deny policy engine for user certificates - if sshUserPolicy, err = authPolicy.NewSSHUserPolicyEngine(policyOptions.GetSSHOptions()); err != nil { - return err - } - - // set all policy engines; all or nothing - a.x509Policy = x509Policy - a.sshHostPolicy = sshHostPolicy - a.sshUserPolicy = sshUserPolicy + // only update the policy engine when no error was returned + a.policyEngine = engine return nil } func isAllowed(engine authPolicy.X509Policy, sans []string) error { - var ( - allowed bool - err error - ) - if allowed, err = engine.AreSANsAllowed(sans); err != nil { + if err := engine.AreSANsAllowed(sans); err != nil { var policyErr *policy.NamePolicyError isNamePolicyError := errors.As(err, &policyErr) if isNamePolicyError && policyErr.Reason == policy.NotAllowed { @@ -285,13 +254,6 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error { } } - if !allowed { - 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 } diff --git a/authority/policy/engine.go b/authority/policy/engine.go new file mode 100644 index 00000000..4b21f66b --- /dev/null +++ b/authority/policy/engine.go @@ -0,0 +1,114 @@ +package policy + +import ( + "crypto/x509" + "errors" + "fmt" + + "golang.org/x/crypto/ssh" +) + +// Engine is a container for multiple policies. +type Engine struct { + x509Policy X509Policy + sshUserPolicy UserPolicy + sshHostPolicy HostPolicy +} + +// New returns a new Engine using Options. +func New(options *Options) (*Engine, error) { + + // if no options provided, return early + if options == nil { + return nil, nil + } + + var ( + x509Policy X509Policy + sshHostPolicy HostPolicy + sshUserPolicy UserPolicy + err error + ) + + // initialize the x509 allow/deny policy engine + if x509Policy, err = NewX509PolicyEngine(options.GetX509Options()); err != nil { + return nil, err + } + + // initialize the SSH allow/deny policy engine for host certificates + if sshHostPolicy, err = NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil { + return nil, err + } + + // initialize the SSH allow/deny policy engine for user certificates + if sshUserPolicy, err = NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil { + return nil, err + } + + return &Engine{ + x509Policy: x509Policy, + sshHostPolicy: sshHostPolicy, + sshUserPolicy: sshUserPolicy, + }, nil +} + +// IsX509CertificateAllowed evaluates an X.509 certificate against +// the X.509 policy (if available) and returns an error if one of the +// names in the certificate is not allowed. +func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error { + + // return early if there's no policy to evaluate + if e == nil || e.x509Policy == nil { + return nil + } + + // return result of X.509 policy evaluation + return e.x509Policy.IsX509CertificateAllowed(cert) +} + +// AreSANsAllowed evaluates the slice of SANs against the X.509 policy +// (if available) and returns an error if one of the SANs is not allowed. +func (e *Engine) AreSANsAllowed(sans []string) error { + + // return early if there's no policy to evaluate + if e == nil || e.x509Policy == nil { + return nil + } + + // return result of X.509 policy evaluation + return e.x509Policy.AreSANsAllowed(sans) +} + +// IsSSHCertificateAllowed evaluates an SSH certificate against the +// user or host policy (if configured) and returns an error if one of the +// principals in the certificate is not allowed. +func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error { + + // return early if there's no policy to evaluate + if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) { + return nil + } + + switch cert.CertType { + case ssh.HostCert: + // when no host policy engine is configured, but a user policy engine is + // configured, the host certificate is denied. + if e.sshHostPolicy == nil && e.sshUserPolicy != nil { + return errors.New("authority not allowed to sign ssh host certificates") + } + + // return result of SSH host policy evaluation + return e.sshHostPolicy.IsSSHCertificateAllowed(cert) + case ssh.UserCert: + // when no user policy engine is configured, but a host policy engine is + // configured, the user certificate is denied. + if e.sshUserPolicy == nil && e.sshHostPolicy != nil { + return errors.New("authority not allowed to sign ssh user certificates") + } + + // return result of SSH user policy evaluation + return e.sshUserPolicy.IsSSHCertificateAllowed(cert) + default: + return fmt.Errorf("unexpected ssh certificate type %q", cert.CertType) + } +} diff --git a/authority/policy_test.go b/authority/policy_test.go index f444a7f0..40af879a 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -330,105 +330,193 @@ func Test_policyToCertificates(t *testing.T) { } } +func mustPolicyEngine(t *testing.T, options *policy.Options) *policy.Engine { + engine, err := policy.New(options) + if err != nil { + t.Fatal(err) + } + return engine +} + func TestAuthority_reloadPolicyEngines(t *testing.T) { - existingX509PolicyEngine, err := policy.NewX509PolicyEngine(&policy.X509PolicyOptions{ - AllowedNames: &policy.X509NameOptions{ - DNSDomains: []string{"*.hosts.example.com"}, - }, - }) - assert.NoError(t, err) - - existingSSHHostPolicyEngine, err := policy.NewSSHHostPolicyEngine(&policy.SSHPolicyOptions{ - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ + existingPolicyEngine, err := policy.New(&policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.hosts.example.com"}, }, }, - }) - assert.NoError(t, err) - - existingSSHUserPolicyEngine, err := policy.NewSSHUserPolicyEngine(&policy.SSHPolicyOptions{ - User: &policy.SSHUserCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - EmailAddresses: []string{"@mails.example.com"}, + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.hosts.example.com"}, + }, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + EmailAddresses: []string{"@mails.example.com"}, + }, }, }, }) assert.NoError(t, err) - newX509PolicyEngine, err := policy.NewX509PolicyEngine(&policy.X509PolicyOptions{ - AllowedNames: &policy.X509NameOptions{ - DNSDomains: []string{"*.local"}, - }, - DeniedNames: &policy.X509NameOptions{ - DNSDomains: []string{"badhost.local"}, - }, - AllowWildcardLiteral: true, - DisableCommonNameVerification: false, - }) - assert.NoError(t, err) - - newSSHHostPolicyEngine, err := policy.NewSSHHostPolicyEngine(&policy.SSHPolicyOptions{ - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ + newX509Options := &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.local"}, }, - DeniedNames: &policy.SSHNameOptions{ + DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, + AllowWildcardLiteral: true, + DisableCommonNameVerification: false, }, - }) - assert.NoError(t, err) + } - newSSHUserPolicyEngine, err := policy.NewSSHUserPolicyEngine(&policy.SSHPolicyOptions{ - User: &policy.SSHUserCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - Principals: []string{"*"}, - }, - DeniedNames: &policy.SSHNameOptions{ - Principals: []string{"root"}, + newSSHHostOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, }, }, - }) - assert.NoError(t, err) + } - newAdminX509PolicyEngine, err := policy.NewX509PolicyEngine(&policy.X509PolicyOptions{ - AllowedNames: &policy.X509NameOptions{ - DNSDomains: []string{"*.local"}, + newSSHUserOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"*"}, + }, + DeniedNames: &policy.SSHNameOptions{ + Principals: []string{"root"}, + }, + }, }, - }) - assert.NoError(t, err) + } - newAdminSSHHostPolicyEngine, err := policy.NewSSHHostPolicyEngine(&policy.SSHPolicyOptions{ - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ + newSSHOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"*"}, + }, + DeniedNames: &policy.SSHNameOptions{ + Principals: []string{"root"}, + }, + }, + }, + } + + newOptions := &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.X509NameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + AllowWildcardLiteral: true, + DisableCommonNameVerification: false, + }, + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"*"}, + }, + DeniedNames: &policy.SSHNameOptions{ + Principals: []string{"root"}, + }, + }, + }, + } + + newAdminX509Options := &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.local"}, }, }, - }) - assert.NoError(t, err) + } - newAdminSSHUserPolicyEngine, err := policy.NewSSHUserPolicyEngine(&policy.SSHPolicyOptions{ - User: &policy.SSHUserCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - EmailAddresses: []string{"@example.com"}, + newAdminSSHHostOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, }, }, - }) - assert.NoError(t, err) - - type expected struct { - x509Policy policy.X509Policy - sshUserPolicy policy.UserPolicy - sshHostPolicy policy.HostPolicy } + + newAdminSSHUserOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + EmailAddresses: []string{"@example.com"}, + }, + }, + }, + } + + newAdminOptions := &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.X509NameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + AllowWildcardLiteral: true, + DisableCommonNameVerification: false, + }, + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + }, + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + EmailAddresses: []string{"@example.com"}, + }, + DeniedNames: &policy.SSHNameOptions{ + EmailAddresses: []string{"baduser@example.com"}, + }, + }, + }, + } + tests := []struct { name string config *config.Config adminDB admin.DB ctx context.Context - expected *expected + expected *policy.Engine wantErr bool }{ { @@ -445,13 +533,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, }, - ctx: context.Background(), - wantErr: true, - expected: &expected{ - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, - }, + ctx: context.Background(), + wantErr: true, + expected: existingPolicyEngine, }, { name: "fail/standalone-ssh-host-policy", @@ -469,13 +553,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, }, - ctx: context.Background(), - wantErr: true, - expected: &expected{ - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, - }, + ctx: context.Background(), + wantErr: true, + expected: existingPolicyEngine, }, { name: "fail/standalone-ssh-user-policy", @@ -493,13 +573,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, }, - ctx: context.Background(), - wantErr: true, - expected: &expected{ - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, - }, + ctx: context.Background(), + wantErr: true, + expected: existingPolicyEngine, }, { name: "fail/adminDB.GetAuthorityPolicy-error", @@ -513,13 +589,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { return nil, errors.New("force") }, }, - ctx: context.Background(), - wantErr: true, - expected: &expected{ - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, - }, + ctx: context.Background(), + wantErr: true, + expected: existingPolicyEngine, }, { name: "fail/admin-x509-policy", @@ -539,13 +611,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, nil }, }, - ctx: context.Background(), - wantErr: true, - expected: &expected{ - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, - }, + ctx: context.Background(), + wantErr: true, + expected: existingPolicyEngine, }, { name: "fail/admin-ssh-host-policy", @@ -567,13 +635,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, nil }, }, - ctx: context.Background(), - wantErr: true, - expected: &expected{ - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, - }, + ctx: context.Background(), + wantErr: true, + expected: existingPolicyEngine, }, { name: "fail/admin-ssh-user-policy", @@ -595,13 +659,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, nil }, }, - ctx: context.Background(), - wantErr: true, - expected: &expected{ - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, - }, + ctx: context.Background(), + wantErr: true, + expected: existingPolicyEngine, }, { name: "ok/linkedca-unsupported", @@ -610,14 +670,10 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { EnableAdmin: true, }, }, - adminDB: &linkedCaClient{}, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, - }, + adminDB: &linkedCaClient{}, + ctx: context.Background(), + wantErr: false, + expected: existingPolicyEngine, }, { name: "ok/standalone-no-policy", @@ -627,13 +683,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Policy: nil, }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - x509Policy: nil, - sshUserPolicy: nil, - sshHostPolicy: nil, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, nil), }, { name: "ok/standalone-x509-policy", @@ -654,14 +706,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - // expect only the X.509 policy to exist - x509Policy: newX509PolicyEngine, - sshHostPolicy: nil, - sshUserPolicy: nil, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, newX509Options), }, { name: "ok/standalone-ssh-host-policy", @@ -682,14 +729,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - // expect only the SSH host policy to exist - x509Policy: nil, - sshHostPolicy: newSSHHostPolicyEngine, - sshUserPolicy: nil, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, newSSHHostOptions), }, { name: "ok/standalone-ssh-user-policy", @@ -710,14 +752,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - // expect only the SSH user policy to exist - x509Policy: nil, - sshHostPolicy: nil, - sshUserPolicy: newSSHUserPolicyEngine, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, newSSHUserOptions), }, { name: "ok/standalone-ssh-policy", @@ -746,14 +783,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - // expect only the SSH policy engines to exist - x509Policy: nil, - sshHostPolicy: newSSHHostPolicyEngine, - sshUserPolicy: newSSHUserPolicyEngine, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, newSSHOptions), }, { name: "ok/standalone-full-policy", @@ -792,14 +824,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - // expect all three policy engines to exist - x509Policy: newX509PolicyEngine, - sshHostPolicy: newSSHHostPolicyEngine, - sshUserPolicy: newSSHUserPolicyEngine, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, newOptions), }, { name: "ok/admin-x509-policy", @@ -819,13 +846,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, nil }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - x509Policy: newAdminX509PolicyEngine, - sshHostPolicy: nil, - sshUserPolicy: nil, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, newAdminX509Options), }, { name: "ok/admin-ssh-host-policy", @@ -847,13 +870,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, nil }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - x509Policy: nil, - sshHostPolicy: newAdminSSHHostPolicyEngine, - sshUserPolicy: nil, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, newAdminSSHHostOptions), }, { name: "ok/admin-ssh-user-policy", @@ -875,13 +894,9 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, nil }, }, - ctx: context.Background(), - wantErr: false, - expected: &expected{ - x509Policy: nil, - sshHostPolicy: nil, - sshUserPolicy: newAdminSSHUserPolicyEngine, - }, + ctx: context.Background(), + wantErr: false, + expected: mustPolicyEngine(t, newAdminSSHUserOptions), }, { name: "ok/admin-full-policy", @@ -909,23 +924,24 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Allow: &linkedca.SSHHostNames{ Dns: []string{"*.local"}, }, + Deny: &linkedca.SSHHostNames{ + Dns: []string{"badhost.local"}, + }, }, User: &linkedca.SSHUserPolicy{ Allow: &linkedca.SSHUserNames{ Emails: []string{"@example.com"}, }, + Deny: &linkedca.SSHUserNames{ + Emails: []string{"baduser@example.com"}, + }, }, }, }, nil }, }, - wantErr: false, - expected: &expected{ - // expect all three policy engines to exist - x509Policy: newX509PolicyEngine, - sshHostPolicy: newAdminSSHHostPolicyEngine, - sshUserPolicy: newAdminSSHUserPolicyEngine, - }, + wantErr: false, + expected: mustPolicyEngine(t, newAdminOptions), }, { // both DB and JSON config; DB config is taken if Admin API is enabled @@ -972,31 +988,27 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { }, nil }, }, - wantErr: false, - expected: &expected{ - // expect all three policy engines to exist - x509Policy: newX509PolicyEngine, - sshHostPolicy: nil, - sshUserPolicy: nil, - }, + wantErr: false, + expected: mustPolicyEngine(t, newX509Options), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &Authority{ - config: tt.config, - adminDB: tt.adminDB, - x509Policy: existingX509PolicyEngine, - sshUserPolicy: existingSSHUserPolicyEngine, - sshHostPolicy: existingSSHHostPolicyEngine, + config: tt.config, + adminDB: tt.adminDB, + policyEngine: existingPolicyEngine, } if err := a.reloadPolicyEngines(tt.ctx); (err != nil) != tt.wantErr { t.Errorf("Authority.reloadPolicyEngines() error = %v, wantErr %v", err, tt.wantErr) } - assert.Equal(t, tt.expected.x509Policy, a.x509Policy) - assert.Equal(t, tt.expected.sshHostPolicy, a.sshHostPolicy) - assert.Equal(t, tt.expected.sshUserPolicy, a.sshUserPolicy) + // TODO(hs): fix those + // assert.Equal(t, tt.expected.x509Policy, a.x509Policy) + // assert.Equal(t, tt.expected.sshHostPolicy, a.sshHostPolicy) + // assert.Equal(t, tt.expected.sshUserPolicy, a.sshUserPolicy) + + assert.Equal(t, tt.expected, a.policyEngine) }) } } diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 790c6bb1..c9fa02cc 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -118,9 +118,9 @@ func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIden var err error switch identifier.Type { case IP: - _, err = x509Policy.IsIPAllowed(net.ParseIP(identifier.Value)) + err = x509Policy.IsIPAllowed(net.ParseIP(identifier.Value)) case DNS: - _, err = x509Policy.IsDNSAllowed(identifier.Value) + err = x509Policy.IsDNSAllowed(identifier.Value) default: err = fmt.Errorf("invalid ACME identifier type '%s' provided", identifier.Type) } diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index bac40e69..2eefd331 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -422,8 +422,7 @@ func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) e if v.policyEngine == nil { return nil } - _, err := v.policyEngine.IsX509CertificateAllowed(cert) - return err + return v.policyEngine.IsX509CertificateAllowed(cert) } // var ( diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index a41b8bc1..70dffba2 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -480,16 +480,14 @@ func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) if v.hostPolicyEngine == nil && v.userPolicyEngine != nil { return errors.New("SSH host certificate not authorized") } - _, err := v.hostPolicyEngine.IsSSHCertificateAllowed(cert) - return err + return v.hostPolicyEngine.IsSSHCertificateAllowed(cert) case ssh.UserCert: // when no user policy engine is configured, but a host policy engine is // configured, the user certificate is denied. if v.userPolicyEngine == nil && v.hostPolicyEngine != nil { return errors.New("SSH user certificate not authorized") } - _, err := v.userPolicyEngine.IsSSHCertificateAllowed(cert) - return err + return v.userPolicyEngine.IsSSHCertificateAllowed(cert) default: return fmt.Errorf("unexpected SSH certificate type %d", cert.CertType) // satisfy return; shouldn't happen } diff --git a/authority/ssh.go b/authority/ssh.go index 3f08b88a..0521ab58 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -246,63 +246,21 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType) } - switch certTpl.CertType { - case ssh.UserCert: - // when no user policy engine is configured, but a host policy engine is - // configured, the user certificate is denied. - if a.sshUserPolicy == nil && a.sshHostPolicy != nil { - return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign ssh user certificates"), "authority.SignSSH: error creating ssh user certificate") - } - if a.sshUserPolicy != nil { - allowed, err := a.sshUserPolicy.IsSSHCertificateAllowed(certTpl) - if err != nil { - var pe *policy.NamePolicyError - if errors.As(err, &pe) && pe.Reason == policy.NotAllowed { - return nil, &errs.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, - errs.WithMessage("authority.SignSSH: error creating ssh user certificate"), - ) - } - if !allowed { - return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: error creating ssh user certificate") + // Check if authority is allowed to sign the certificate + if err := a.isAllowedToSignSSHCertificate(certTpl); err != nil { + var pe *policy.NamePolicyError + if errors.As(err, &pe) && pe.Reason == policy.NotAllowed { + return nil, &errs.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()), } } - case ssh.HostCert: - // when no host policy engine is configured, but a user policy engine is - // configured, the host certificate is denied. - if a.sshHostPolicy == nil && a.sshUserPolicy != nil { - return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign ssh host certificates"), "authority.SignSSH: error creating ssh user certificate") - } - if a.sshHostPolicy != nil { - allowed, err := a.sshHostPolicy.IsSSHCertificateAllowed(certTpl) - if err != nil { - var pe *policy.NamePolicyError - if errors.As(err, &pe) && pe.Reason == policy.NotAllowed { - return nil, &errs.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, - errs.WithMessage("authority.SignSSH: error creating ssh host certificate"), - ) - } - if !allowed { - return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: error creating ssh host certificate") - } - } - default: - return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType) + return nil, errs.InternalServerErr(err, + errs.WithMessage("authority.SignSSH: error creating ssh certificate"), + ) } // Sign certificate. @@ -325,6 +283,11 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi return cert, nil } +// isAllowedToSignSSHCertificate checks if the Authority is allowed to sign the SSH certificate. +func (a *Authority) isAllowedToSignSSHCertificate(cert *ssh.Certificate) error { + return a.policyEngine.IsSSHCertificateAllowed(cert) +} + // RenewSSH creates a signed SSH certificate using the old SSH certificate as a template. func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) { if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 2a135f4e..4fd7eaa0 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -191,21 +191,28 @@ func TestAuthority_SignSSH(t *testing.T) { }, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"})) assert.FatalError(t, err) - policyOptions := &policy.SSHPolicyOptions{ - User: &policy.SSHUserCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - Principals: []string{"user"}, - }, - }, - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - DNSDomains: []string{"*.test.com"}, + userPolicyOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"user"}, + }, }, }, } - userPolicy, err := policy.NewSSHUserPolicyEngine(policyOptions) + userPolicy, err := policy.New(userPolicyOptions) assert.FatalError(t, err) - hostPolicy, err := policy.NewSSHHostPolicyEngine(policyOptions) + + hostPolicyOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.test.com"}, + }, + }, + }, + } + hostPolicy, err := policy.New(hostPolicyOptions) assert.FatalError(t, err) now := time.Now() @@ -213,8 +220,7 @@ func TestAuthority_SignSSH(t *testing.T) { type fields struct { sshCAUserCertSignKey ssh.Signer sshCAHostCertSignKey ssh.Signer - sshUserPolicy policy.UserPolicy - sshHostPolicy policy.HostPolicy + policyEngine *policy.Engine } type args struct { key ssh.PublicKey @@ -234,49 +240,48 @@ func TestAuthority_SignSSH(t *testing.T) { want want wantErr bool }{ - {"ok-user", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, - {"ok-host", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, - {"ok-user-only", fields{signer, nil, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, - {"ok-host-only", fields{nil, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, - {"ok-opts-type-user", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-type-host", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert}, false}, - {"ok-opts-principals", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, - {"ok-opts-principals", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, - {"ok-opts-valid-after", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false}, - {"ok-opts-valid-before", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false}, - {"ok-cert-validator", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-cert-modifier", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-validator", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-modifier", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-custom-template", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{"user", "admin"}}, false}, - {"ok-user-policy", fields{signer, signer, userPolicy, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, - {"ok-host-policy", fields{signer, signer, nil, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, - {"fail-opts-type", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{userTemplate}}, want{}, true}, - {"fail-cert-validator", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("an error")}}, want{}, true}, - {"fail-cert-modifier", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("an error")}}, want{}, true}, - {"fail-opts-validator", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("an error")}}, want{}, true}, - {"fail-opts-modifier", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("an error")}}, want{}, true}, - {"fail-bad-sign-options", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, "wrong type"}}, want{}, true}, - {"fail-no-user-key", fields{nil, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{}, true}, - {"fail-no-host-key", fields{signer, nil, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{}, true}, - {"fail-bad-type", fields{signer, nil, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, sshTestModifier{CertType: 100}}}, want{}, true}, - {"fail-custom-template", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userFailTemplate, userOptions}}, want{}, true}, - {"fail-custom-template-syntax-error-file", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONSyntaxErrorTemplateFile, userOptions}}, want{}, true}, - {"fail-custom-template-syntax-value-file", fields{signer, signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONValueErrorTemplateFile, userOptions}}, want{}, true}, - {"fail-user-policy", fields{signer, signer, userPolicy, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"root"}}, []provisioner.SignOption{userTemplateWithRoot}}, want{}, true}, - {"fail-user-policy-with-host-cert", fields{signer, signer, userPolicy, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true}, - {"fail-user-policy-with-bad-user", fields{signer, signer, userPolicy, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{badUserTemplate}}, want{}, true}, - {"fail-host-policy", fields{signer, signer, nil, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true}, - {"fail-host-policy-with-user-cert", fields{signer, signer, nil, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{}, true}, - {"fail-host-policy-with-bad-host", fields{signer, signer, nil, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{badHostTemplate}}, want{}, true}, + {"ok-user", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, + {"ok-host", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, + {"ok-user-only", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, + {"ok-host-only", fields{nil, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, + {"ok-opts-type-user", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-type-host", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert}, false}, + {"ok-opts-principals", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, + {"ok-opts-principals", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, + {"ok-opts-valid-after", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false}, + {"ok-opts-valid-before", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false}, + {"ok-cert-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-cert-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-custom-template", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{"user", "admin"}}, false}, + {"ok-user-policy", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, + {"ok-host-policy", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, + {"fail-opts-type", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{userTemplate}}, want{}, true}, + {"fail-cert-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("an error")}}, want{}, true}, + {"fail-cert-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("an error")}}, want{}, true}, + {"fail-opts-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("an error")}}, want{}, true}, + {"fail-opts-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("an error")}}, want{}, true}, + {"fail-bad-sign-options", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, "wrong type"}}, want{}, true}, + {"fail-no-user-key", fields{nil, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{}, true}, + {"fail-no-host-key", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{}, true}, + {"fail-bad-type", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, sshTestModifier{CertType: 100}}}, want{}, true}, + {"fail-custom-template", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userFailTemplate, userOptions}}, want{}, true}, + {"fail-custom-template-syntax-error-file", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONSyntaxErrorTemplateFile, userOptions}}, want{}, true}, + {"fail-custom-template-syntax-value-file", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONValueErrorTemplateFile, userOptions}}, want{}, true}, + {"fail-user-policy", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"root"}}, []provisioner.SignOption{userTemplateWithRoot}}, want{}, true}, + {"fail-user-policy-with-host-cert", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true}, + {"fail-user-policy-with-bad-user", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{badUserTemplate}}, want{}, true}, + {"fail-host-policy", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true}, + {"fail-host-policy-with-user-cert", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{}, true}, + {"fail-host-policy-with-bad-host", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{badHostTemplate}}, want{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := testAuthority(t) a.sshCAUserCertSignKey = tt.fields.sshCAUserCertSignKey a.sshCAHostCertSignKey = tt.fields.sshCAHostCertSignKey - a.sshUserPolicy = tt.fields.sshUserPolicy - a.sshHostPolicy = tt.fields.sshHostPolicy + a.policyEngine = tt.fields.policyEngine got, err := a.SignSSH(context.Background(), tt.args.key, tt.args.opts, tt.args.signOpts...) if (err != nil) != tt.wantErr { diff --git a/authority/tls.go b/authority/tls.go index cc34ff6a..d23b0da7 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -200,8 +200,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } // Check if authority is allowed to sign the certificate - var allowedToSign bool - if allowedToSign, err = a.isAllowedToSign(leaf); err != nil { + if err := a.isAllowedToSignX509Certificate(leaf); err != nil { var pe *policy.NamePolicyError if errors.As(err, &pe) && pe.Reason == policy.NotAllowed { return nil, errs.ApplyOptions(&errs.Error{ @@ -218,12 +217,6 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign errs.WithMessage("error creating certificate"), ) } - if !allowedToSign { - return nil, errs.ApplyOptions( - errs.ForbiddenErr(errors.New("authority not allowed to sign"), "error creating certificate"), - opts..., - ) - } // Sign certificate lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) @@ -248,31 +241,16 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign return fullchain, nil } -// isAllowedToSign checks if the Authority is allowed to sign the X.509 certificate. -func (a *Authority) isAllowedToSign(cert *x509.Certificate) (bool, error) { - - // if no policy is configured, the cert is implicitly allowed - if a.x509Policy == nil { - return true, nil - } - - return a.x509Policy.IsX509CertificateAllowed(cert) +// isAllowedToSignX509Certificate checks if the Authority is allowed +// to sign the X.509 certificate. +func (a *Authority) isAllowedToSignX509Certificate(cert *x509.Certificate) error { + return a.policyEngine.IsX509CertificateAllowed(cert) } // AreSANsAllowed evaluates the provided sans against the // authority X.509 policy. func (a *Authority) AreSANsAllowed(ctx context.Context, sans []string) error { - - // no policy configured; return early - if a.x509Policy == nil { - return nil - } - - // evaluate the X.509 policy for the provided sans - var err error - _, err = a.x509Policy.AreSANsAllowed(sans) - - return err + return a.policyEngine.AreSANsAllowed(sans) } // Renew creates a new Certificate identical to the old certificate, except diff --git a/authority/tls_test.go b/authority/tls_test.go index 3f9946ba..3739dbff 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -523,14 +523,16 @@ ZYtQ9Ot36qc= return nil }, } - policyOptions := &policy.X509PolicyOptions{ - DeniedNames: &policy.X509NameOptions{ - DNSDomains: []string{"test.smallstep.com"}, + options := &policy.Options{ + X509: &policy.X509PolicyOptions{ + DeniedNames: &policy.X509NameOptions{ + DNSDomains: []string{"test.smallstep.com"}, + }, }, } - engine, err := policy.NewX509PolicyEngine(policyOptions) + engine, err := policy.New(options) assert.FatalError(t, err) - aa.x509Policy = engine + aa.policyEngine = engine return &signTest{ auth: aa, csr: csr, @@ -696,15 +698,17 @@ ZYtQ9Ot36qc= return nil }, } - policyOptions := &policy.X509PolicyOptions{ - AllowedNames: &policy.X509NameOptions{ - DNSDomains: []string{"*.smallstep.com"}, + options := &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.smallstep.com"}, + }, + DisableCommonNameVerification: true, // TODO(hs): allows "smallstep test"; do we want to keep it like this? }, - DisableCommonNameVerification: true, // TODO(hs): allows "smallstep test"; do we want to keep it like this? } - engine, err := policy.NewX509PolicyEngine(policyOptions) + engine, err := policy.New(options) assert.FatalError(t, err) - aa.x509Policy = engine + aa.policyEngine = engine return &signTest{ auth: aa, csr: csr, diff --git a/policy/engine.go b/policy/engine.go index d03665ee..3d8a4755 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -15,11 +15,10 @@ import ( type NamePolicyReason int const ( - _ NamePolicyReason = iota // NotAllowed results when an instance of NamePolicyEngine // determines that there's a constraint which doesn't permit // a DNS or another type of SAN to be signed (or otherwise used). - NotAllowed + NotAllowed NamePolicyReason = iota + 1 // CannotParseDomain is returned when an error occurs // when parsing the domain part of SAN or subject. CannotParseDomain @@ -198,7 +197,7 @@ func removeDuplicateIPNets(items []*net.IPNet) (ret []*net.IPNet) { } // IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed. -func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) (bool, error) { +func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) error { dnsNames, ips, emails, uris := cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs // when Subject Common Name must be verified in addition to the SANs, it is // added to the appropriate slice of names. @@ -206,13 +205,13 @@ func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) (boo appendSubjectCommonName(cert.Subject, &dnsNames, &ips, &emails, &uris) } if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil { - return false, err + return err } - return true, nil + return nil } // IsX509CertificateRequestAllowed verifies that all names in the CSR are allowed. -func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) (bool, error) { +func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error { dnsNames, ips, emails, uris := csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs // when Subject Common Name must be verified in addition to the SANs, it is // added to the appropriate slice of names. @@ -220,47 +219,47 @@ func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.Certificate appendSubjectCommonName(csr.Subject, &dnsNames, &ips, &emails, &uris) } if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil { - return false, err + return err } - return true, nil + return nil } // AreSANSAllowed verifies that all names in the slice of SANs are allowed. // The SANs are first split into DNS names, IPs, email addresses and URIs. -func (e *NamePolicyEngine) AreSANsAllowed(sans []string) (bool, error) { +func (e *NamePolicyEngine) AreSANsAllowed(sans []string) error { dnsNames, ips, emails, uris := x509util.SplitSANs(sans) if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil { - return false, err + return err } - return true, nil + return nil } // IsDNSAllowed verifies a single DNS domain is allowed. -func (e *NamePolicyEngine) IsDNSAllowed(dns string) (bool, error) { +func (e *NamePolicyEngine) IsDNSAllowed(dns string) error { if err := e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}, []string{}); err != nil { - return false, err + return err } - return true, nil + return nil } // IsIPAllowed verifies a single IP domain is allowed. -func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) { +func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) error { if err := e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}, []string{}); err != nil { - return false, err + return err } - return true, nil + return nil } // IsSSHCertificateAllowed verifies that all principals in an SSH certificate are allowed. -func (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) (bool, error) { +func (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) error { dnsNames, ips, emails, principals, err := splitSSHPrincipals(cert) if err != nil { - return false, err + return err } if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, principals); err != nil { - return false, err + return err } - return true, nil + return nil } // appendSubjectCommonName appends the Subject Common Name to the appropriate slice of names. The logic is diff --git a/policy/engine_test.go b/policy/engine_test.go index a99885ea..deec2ff9 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -2391,16 +2391,16 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { t.Run(tt.name, func(t *testing.T) { engine, err := New(tt.options...) assert.NoError(t, err) - got, err := engine.IsX509CertificateAllowed(tt.cert) + gotErr := engine.IsX509CertificateAllowed(tt.cert) wantErr := tt.wantErr != nil - if (err != nil) != wantErr { - t.Errorf("NamePolicyEngine.IsX509CertificateAllowed() error = %v, wantErr %v", err, tt.wantErr) + if (gotErr != nil) != wantErr { + t.Errorf("NamePolicyEngine.IsX509CertificateAllowed() error = %v, wantErr %v", gotErr, tt.wantErr) return } - if err != nil { + if gotErr != nil { var npe *NamePolicyError - assert.True(t, errors.As(err, &npe)) + assert.True(t, errors.As(gotErr, &npe)) assert.NotEqual(t, "", npe.Error()) assert.Equal(t, tt.wantErr.Reason, npe.Reason) assert.Equal(t, tt.wantErr.NameType, npe.NameType) @@ -2408,9 +2408,6 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { assert.NotEqual(t, "", npe.Detail()) //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail } - if got != tt.want { - t.Errorf("NamePolicyEngine.IsX509CertificateAllowed() = %v, want %v", got, tt.want) - } // Perform the same tests for a CSR, which are similar to Certificates csr := &x509.CertificateRequest{ @@ -2420,15 +2417,15 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { IPAddresses: tt.cert.IPAddresses, URIs: tt.cert.URIs, } - got, err = engine.IsX509CertificateRequestAllowed(csr) + gotErr = engine.IsX509CertificateRequestAllowed(csr) wantErr = tt.wantErr != nil - if (err != nil) != wantErr { - t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() error = %v, wantErr %v", err, tt.wantErr) + if (gotErr != nil) != wantErr { + t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() error = %v, wantErr %v", gotErr, tt.wantErr) return } - if err != nil { + if gotErr != nil { var npe *NamePolicyError - assert.True(t, errors.As(err, &npe)) + assert.True(t, errors.As(gotErr, &npe)) assert.NotEqual(t, "", npe.Error()) assert.Equal(t, tt.wantErr.Reason, npe.Reason) assert.Equal(t, tt.wantErr.NameType, npe.NameType) @@ -2436,22 +2433,19 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { assert.NotEqual(t, "", npe.Detail()) //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail } - if got != tt.want { - t.Errorf("NamePolicyEngine.AreCSRNamesAllowed() = %v, want %v", got, tt.want) - } // Perform the same tests for a slice of SANs includeSubject := engine.verifySubjectCommonName // copy behavior of the engine when Subject has to be included as a SAN sans := extractSANs(tt.cert, includeSubject) - got, err = engine.AreSANsAllowed(sans) + gotErr = engine.AreSANsAllowed(sans) wantErr = tt.wantErr != nil - if (err != nil) != wantErr { - t.Errorf("NamePolicyEngine.AreSANsAllowed() error = %v, wantErr %v", err, tt.wantErr) + if (gotErr != nil) != wantErr { + t.Errorf("NamePolicyEngine.AreSANsAllowed() error = %v, wantErr %v", gotErr, tt.wantErr) return } - if err != nil { + if gotErr != nil { var npe *NamePolicyError - assert.True(t, errors.As(err, &npe)) + assert.True(t, errors.As(gotErr, &npe)) assert.NotEqual(t, "", npe.Error()) assert.Equal(t, tt.wantErr.Reason, npe.Reason) assert.Equal(t, tt.wantErr.NameType, npe.NameType) @@ -2459,9 +2453,6 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { assert.NotEqual(t, "", npe.Detail()) //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail } - if got != tt.want { - t.Errorf("NamePolicyEngine.AreSANsAllowed() = %v, want %v", got, tt.want) - } }) } } @@ -2955,15 +2946,15 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { t.Run(tt.name, func(t *testing.T) { engine, err := New(tt.options...) assert.NoError(t, err) - got, err := engine.IsSSHCertificateAllowed(tt.cert) + gotErr := engine.IsSSHCertificateAllowed(tt.cert) wantErr := tt.wantErr != nil - if (err != nil) != wantErr { - t.Errorf("NamePolicyEngine.IsSSHCertificateAllowed() error = %v, wantErr %v", err, tt.wantErr) + if (gotErr != nil) != wantErr { + t.Errorf("NamePolicyEngine.IsSSHCertificateAllowed() error = %v, wantErr %v", gotErr, tt.wantErr) return } - if err != nil { + if gotErr != nil { var npe *NamePolicyError - assert.True(t, errors.As(err, &npe)) + assert.True(t, errors.As(gotErr, &npe)) assert.NotEqual(t, "", npe.Error()) assert.Equal(t, tt.wantErr.Reason, npe.Reason) assert.Equal(t, tt.wantErr.NameType, npe.NameType) @@ -2971,9 +2962,6 @@ func TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) { assert.NotEqual(t, "", npe.Detail()) //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail } - if got != tt.want { - t.Errorf("NamePolicyEngine.IsSSHCertificateAllowed() = %v, want %v", got, tt.want) - } }) } } diff --git a/policy/ssh.go b/policy/ssh.go index 1ebecb2e..725f9b7b 100644 --- a/policy/ssh.go +++ b/policy/ssh.go @@ -5,5 +5,5 @@ import ( ) type SSHNamePolicyEngine interface { - IsSSHCertificateAllowed(cert *ssh.Certificate) (bool, error) + IsSSHCertificateAllowed(cert *ssh.Certificate) error } diff --git a/policy/x509.go b/policy/x509.go index 666e1b5c..8b6c4de9 100644 --- a/policy/x509.go +++ b/policy/x509.go @@ -6,9 +6,9 @@ import ( ) type X509NamePolicyEngine interface { - IsX509CertificateAllowed(cert *x509.Certificate) (bool, error) - IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) (bool, error) - AreSANsAllowed(sans []string) (bool, error) - IsDNSAllowed(dns string) (bool, error) - IsIPAllowed(ip net.IP) (bool, error) + IsX509CertificateAllowed(cert *x509.Certificate) error + IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error + AreSANsAllowed(sans []string) error + IsDNSAllowed(dns string) error + IsIPAllowed(ip net.IP) error } From bddd08d4b069a0ee8669d4ad048867578bfa9b33 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 26 Apr 2022 14:01:16 +0200 Subject: [PATCH 65/78] Remove "proto:" prefix from bad proto JSON messages --- api/read/read.go | 5 ++++- authority/admin/api/policy_test.go | 12 ++++++------ authority/admin/api/provisioner_test.go | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/api/read/read.go b/api/read/read.go index 2f7348bd..9c5ebd07 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net/http" + "strings" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" @@ -33,7 +34,9 @@ func ProtoJSON(r io.Reader, m proto.Message) error { switch err := protojson.Unmarshal(data, m); { case errors.Is(err, proto.Error): - return badProtoJSONError(err.Error()) + // trim the proto prefix for the message + s := strings.TrimSpace(strings.TrimPrefix(err.Error(), "proto:")) + return badProtoJSONError(s) default: return err } diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index fffa84f7..d0c97729 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -376,7 +376,7 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { // a syntax error (in the tests). If the message doesn't start with "proto", // we expect a full string match. if strings.HasPrefix(tc.err.Message, "proto:") { - assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { assert.Equals(t, tc.err.Message, ae.Message) } @@ -634,7 +634,7 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { // a syntax error (in the tests). If the message doesn't start with "proto", // we expect a full string match. if strings.HasPrefix(tc.err.Message, "proto:") { - assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { assert.Equals(t, tc.err.Message, ae.Message) } @@ -1081,7 +1081,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { // a syntax error (in the tests). If the message doesn't start with "proto", // we expect a full string match. if strings.HasPrefix(tc.err.Message, "proto:") { - assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { assert.Equals(t, tc.err.Message, ae.Message) } @@ -1292,7 +1292,7 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { // a syntax error (in the tests). If the message doesn't start with "proto", // we expect a full string match. if strings.HasPrefix(tc.err.Message, "proto:") { - assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { assert.Equals(t, tc.err.Message, ae.Message) } @@ -1694,7 +1694,7 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { // a syntax error (in the tests). If the message doesn't start with "proto", // we expect a full string match. if strings.HasPrefix(tc.err.Message, "proto:") { - assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { assert.Equals(t, tc.err.Message, ae.Message) } @@ -1880,7 +1880,7 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { // a syntax error (in the tests). If the message doesn't start with "proto", // we expect a full string match. if strings.HasPrefix(tc.err.Message, "proto:") { - assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { assert.Equals(t, tc.err.Message, ae.Message) } diff --git a/authority/admin/api/provisioner_test.go b/authority/admin/api/provisioner_test.go index de7c3646..486b6cda 100644 --- a/authority/admin/api/provisioner_test.go +++ b/authority/admin/api/provisioner_test.go @@ -430,7 +430,7 @@ func TestHandler_CreateProvisioner(t *testing.T) { assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) if strings.HasPrefix(tc.err.Message, "proto:") { - assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + assert.True(t, strings.Contains(adminErr.Message, "syntax error")) } else { assert.Equals(t, tc.err.Message, adminErr.Message) } @@ -1087,7 +1087,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) if strings.HasPrefix(tc.err.Message, "proto:") { - assert.True(t, strings.Contains(tc.err.Message, "syntax error")) + assert.True(t, strings.Contains(adminErr.Message, "syntax error")) } else { assert.Equals(t, tc.err.Message, adminErr.Message) } From 74a6e59b1f1cb8411805b17c6ddb26f8e060421b Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 26 Apr 2022 14:56:42 +0200 Subject: [PATCH 66/78] Add tests for ProtoJSON and bad proto messages --- api/read/read.go | 13 +++-- api/read/read_test.go | 119 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 7 deletions(-) diff --git a/api/read/read.go b/api/read/read.go index 9c5ebd07..72530b8c 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -16,7 +16,7 @@ import ( ) // JSON reads JSON from the request body and stores it in the value -// pointed by v. +// pointed to by v. func JSON(r io.Reader, v interface{}) error { if err := json.NewDecoder(r).Decode(v); err != nil { return errs.BadRequestErr(err, "error decoding json") @@ -34,9 +34,7 @@ func ProtoJSON(r io.Reader, m proto.Message) error { switch err := protojson.Unmarshal(data, m); { case errors.Is(err, proto.Error): - // trim the proto prefix for the message - s := strings.TrimSpace(strings.TrimPrefix(err.Error(), "proto:")) - return badProtoJSONError(s) + return badProtoJSONError(err.Error()) default: return err } @@ -59,9 +57,10 @@ func (e badProtoJSONError) Render(w http.ResponseWriter) { Detail string `json:"detail"` Message string `json:"message"` }{ - Type: "badRequest", - Detail: "bad request", - Message: e.Error(), + Type: "badRequest", + Detail: "bad request", + // trim the proto prefix for the message + Message: strings.TrimSpace(strings.TrimPrefix(e.Error(), "proto:")), } render.JSONStatus(w, v, http.StatusBadRequest) } diff --git a/api/read/read_test.go b/api/read/read_test.go index f2eff1bc..8696ba78 100644 --- a/api/read/read_test.go +++ b/api/read/read_test.go @@ -1,10 +1,22 @@ package read import ( + "encoding/json" + "errors" "io" + "io/ioutil" + "net/http" + "net/http/httptest" "reflect" "strings" "testing" + "testing/iotest" + + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + + "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" ) @@ -44,3 +56,110 @@ func TestJSON(t *testing.T) { }) } } + +func TestProtoJSON(t *testing.T) { + + p := new(linkedca.Policy) // TODO(hs): can we use something different, so we don't need the import? + + type args struct { + r io.Reader + m proto.Message + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "fail/io.ReadAll", + args: args{ + r: iotest.ErrReader(errors.New("read error")), + m: p, + }, + wantErr: true, + }, + { + name: "fail/proto", + args: args{ + r: strings.NewReader(`{?}`), + m: p, + }, + wantErr: true, + }, + { + name: "ok", + args: args{ + r: strings.NewReader(`{"x509":{}}`), + m: p, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ProtoJSON(tt.args.r, tt.args.m) + if (err != nil) != tt.wantErr { + t.Errorf("ProtoJSON() error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.wantErr { + switch err.(type) { + case badProtoJSONError: + assert.Contains(t, err.Error(), "syntax error") + case *errs.Error: + var ee *errs.Error + if errors.As(err, &ee) { + assert.Equal(t, http.StatusBadRequest, ee.Status) + } + } + return + } + + assert.Equal(t, protoreflect.FullName("linkedca.Policy"), proto.MessageName(tt.args.m)) + assert.True(t, proto.Equal(&linkedca.Policy{X509: &linkedca.X509Policy{}}, tt.args.m)) + }) + } +} + +func Test_badProtoJSONError_Render(t *testing.T) { + tests := []struct { + name string + e badProtoJSONError + expected string + }{ + { + name: "bad proto normal space", + e: badProtoJSONError("proto: syntax error (line 1:2): invalid value ?"), + expected: "syntax error (line 1:2): invalid value ?", + }, + { + name: "bad proto non breaking space", + e: badProtoJSONError("proto: syntax error (line 1:2): invalid value ?"), + expected: "syntax error (line 1:2): invalid value ?", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + w := httptest.NewRecorder() + tt.e.Render(w) + res := w.Result() + defer res.Body.Close() + + data, err := ioutil.ReadAll(res.Body) + assert.NoError(t, err) + + v := struct { + Type string `json:"type"` + Detail string `json:"detail"` + Message string `json:"message"` + }{} + + assert.NoError(t, json.Unmarshal(data, &v)) + assert.Equal(t, "badRequest", v.Type) + assert.Equal(t, "bad request", v.Detail) + assert.Equal(t, "syntax error (line 1:2): invalid value ?", v.Message) + + }) + } +} From 88a1bf17cf0dc3e69938eb28f03a02ffeecdf3e7 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 27 Apr 2022 11:40:43 -0700 Subject: [PATCH 67/78] Update to pull request template --- .github/PULL_REQUEST_TEMPLATE | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 266e9124..5d38f102 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -1,4 +1,20 @@ -### Description -Please describe your pull request. + +#### Name of feature: + +#### Pain or issue this feature alleviates: + +#### Why is this important to the project (if not answered above): + +#### Is there documentation on how to use this feature? If so, where? + +#### In what environments or workflows is this feature supported? + +#### In what environments or workflows is this feature explicitly NOT supported (if any)? + +#### Supporting links/other PRs/issues: 💔Thank you! From 2b7f6931f3fc8d5e9010a758c07bfeb0657bb36e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 28 Apr 2022 14:49:23 +0200 Subject: [PATCH 68/78] Change Subject Common Name verification Subject Common Names can now also be configured to be allowed or denied, similar to SANs. When a Subject Common Name is not explicitly allowed or denied, its type will be determined and its value will be validated according to the constraints for that type of name (i.e. URI). --- api/read/read_test.go | 3 +- authority/admin/api/policy_test.go | 2 - authority/policy.go | 7 +- authority/policy/options.go | 19 +--- authority/policy/options_test.go | 40 ------- authority/policy/policy.go | 6 +- authority/policy_test.go | 53 ++++----- authority/provisioner/options.go | 12 -- authority/provisioner/options_test.go | 35 ------ authority/tls_test.go | 4 +- policy/engine.go | 60 ++++------ policy/engine_test.go | 157 ++++++++++++++++++-------- policy/options.go | 16 ++- policy/validate.go | 77 ++++++++++--- 14 files changed, 246 insertions(+), 245 deletions(-) diff --git a/api/read/read_test.go b/api/read/read_test.go index 8696ba78..72100584 100644 --- a/api/read/read_test.go +++ b/api/read/read_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "io" - "io/ioutil" "net/http" "net/http/httptest" "reflect" @@ -146,7 +145,7 @@ func Test_badProtoJSONError_Render(t *testing.T) { res := w.Result() defer res.Body.Close() - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) assert.NoError(t, err) v := struct { diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index d0c97729..b5987104 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -312,7 +312,6 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, - DisableSubjectCommonNameVerification: false, }, } body, err := protojson.Marshal(policy) @@ -1030,7 +1029,6 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, - DisableSubjectCommonNameVerification: false, }, } body, err := protojson.Marshal(policy) diff --git a/authority/policy.go b/authority/policy.go index 47104e0e..dd38fd4a 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -288,6 +288,9 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { if allow.Uris != nil { opts.X509.AllowedNames.URIDomains = allow.Uris } + if allow.CommonNames != nil { + opts.X509.AllowedNames.CommonNames = allow.CommonNames + } } if deny := x509.GetDeny(); deny != nil { opts.X509.DeniedNames = &authPolicy.X509NameOptions{} @@ -303,10 +306,12 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { if deny.Uris != nil { opts.X509.DeniedNames.URIDomains = deny.Uris } + if deny.CommonNames != nil { + opts.X509.DeniedNames.CommonNames = deny.CommonNames + } } opts.X509.AllowWildcardLiteral = x509.AllowWildcardLiteral - opts.X509.DisableCommonNameVerification = x509.DisableSubjectCommonNameVerification } // fill ssh policy configuration diff --git a/authority/policy/options.go b/authority/policy/options.go index c4b7b9ce..0f50083d 100644 --- a/authority/policy/options.go +++ b/authority/policy/options.go @@ -31,7 +31,6 @@ type X509PolicyOptionsInterface interface { GetAllowedNameOptions() *X509NameOptions GetDeniedNameOptions() *X509NameOptions IsWildcardLiteralAllowed() bool - ShouldVerifyCommonName() bool } // X509PolicyOptions is a container for x509 allowed and denied @@ -47,15 +46,11 @@ type X509PolicyOptions struct { // such as *.example.com and @example.com are allowed. Defaults // to false. AllowWildcardLiteral bool `json:"allowWildcardLiteral,omitempty"` - - // DisableCommonNameVerification indicates if the Subject Common Name - // is verified in addition to the SANs. Defaults to false, resulting in - // Common Names being verified. - DisableCommonNameVerification bool `json:"disableCommonNameVerification,omitempty"` } // X509NameOptions models the X509 name policy configuration. type X509NameOptions struct { + CommonNames []string `json:"cn,omitempty"` DNSDomains []string `json:"dns,omitempty"` IPRanges []string `json:"ip,omitempty"` EmailAddresses []string `json:"email,omitempty"` @@ -65,7 +60,8 @@ type X509NameOptions struct { // HasNames checks if the AllowedNameOptions has one or more // names configured. func (o *X509NameOptions) HasNames() bool { - return len(o.DNSDomains) > 0 || + return len(o.CommonNames) > 0 || + len(o.DNSDomains) > 0 || len(o.IPRanges) > 0 || len(o.EmailAddresses) > 0 || len(o.URIDomains) > 0 @@ -96,15 +92,6 @@ func (o *X509PolicyOptions) IsWildcardLiteralAllowed() bool { return o.AllowWildcardLiteral } -// ShouldVerifyCommonName returns whether the authority -// should verify the Subject Common Name in addition to the SANs. -func (o *X509PolicyOptions) ShouldVerifyCommonName() bool { - if o == nil { - return false - } - return !o.DisableCommonNameVerification -} - // SSHPolicyOptionsInterface is an interface for providers of // SSH user and host name policy configuration. type SSHPolicyOptionsInterface interface { diff --git a/authority/policy/options_test.go b/authority/policy/options_test.go index b4f456a1..d7d42093 100644 --- a/authority/policy/options_test.go +++ b/authority/policy/options_test.go @@ -43,43 +43,3 @@ func TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) { }) } } - -func TestX509PolicyOptions_ShouldVerifySubjectCommonName(t *testing.T) { - tests := []struct { - name string - options *X509PolicyOptions - want bool - }{ - { - name: "nil-options", - options: nil, - want: false, - }, - { - name: "not-set", - options: &X509PolicyOptions{}, - want: true, - }, - { - name: "set-true", - options: &X509PolicyOptions{ - DisableCommonNameVerification: true, - }, - want: false, - }, - { - name: "set-false", - options: &X509PolicyOptions{ - DisableCommonNameVerification: false, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.options.ShouldVerifyCommonName(); got != tt.want { - t.Errorf("X509PolicyOptions.ShouldVerifySubjectCommonName() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/authority/policy/policy.go b/authority/policy/policy.go index b68bcb19..f5f0fce3 100644 --- a/authority/policy/policy.go +++ b/authority/policy/policy.go @@ -28,6 +28,7 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, allowed := policyOptions.GetAllowedNameOptions() if allowed != nil && allowed.HasNames() { options = append(options, + policy.WithPermittedCommonNames(allowed.CommonNames...), policy.WithPermittedDNSDomains(allowed.DNSDomains...), policy.WithPermittedIPsOrCIDRs(allowed.IPRanges...), policy.WithPermittedEmailAddresses(allowed.EmailAddresses...), @@ -38,6 +39,7 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, denied := policyOptions.GetDeniedNameOptions() if denied != nil && denied.HasNames() { options = append(options, + policy.WithExcludedCommonNames(denied.CommonNames...), policy.WithExcludedDNSDomains(denied.DNSDomains...), policy.WithExcludedIPsOrCIDRs(denied.IPRanges...), policy.WithExcludedEmailAddresses(denied.EmailAddresses...), @@ -50,10 +52,6 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, return nil, nil } - if policyOptions.ShouldVerifyCommonName() { - options = append(options, policy.WithSubjectCommonNameVerification()) - } - if policyOptions.IsWildcardLiteralAllowed() { options = append(options, policy.WithAllowLiteralWildcardNames()) } diff --git a/authority/policy_test.go b/authority/policy_test.go index 40af879a..3f03abd9 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -218,8 +218,7 @@ func Test_policyToCertificates(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, - AllowWildcardLiteral: false, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: false, }, }, want: &policy.Options{ @@ -227,8 +226,7 @@ func Test_policyToCertificates(t *testing.T) { AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.local"}, }, - AllowWildcardLiteral: false, - DisableCommonNameVerification: false, + AllowWildcardLiteral: false, }, }, }, @@ -237,19 +235,20 @@ func Test_policyToCertificates(t *testing.T) { 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"}, + Dns: []string{"step"}, + Ips: []string{"127.0.0.1/24"}, + Emails: []string{"*.example.com"}, + Uris: []string{"https://*.local"}, + CommonNames: []string{"some name"}, }, Deny: &linkedca.X509Names{ - Dns: []string{"bad"}, - Ips: []string{"127.0.0.30"}, - Emails: []string{"badhost.example.com"}, - Uris: []string{"https://badhost.local"}, + Dns: []string{"bad"}, + Ips: []string{"127.0.0.30"}, + Emails: []string{"badhost.example.com"}, + Uris: []string{"https://badhost.local"}, + CommonNames: []string{"another name"}, }, - AllowWildcardLiteral: true, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: true, }, Ssh: &linkedca.SSHPolicy{ Host: &linkedca.SSHHostPolicy{ @@ -283,15 +282,16 @@ func Test_policyToCertificates(t *testing.T) { IPRanges: []string{"127.0.0.1/24"}, EmailAddresses: []string{"*.example.com"}, URIDomains: []string{"https://*.local"}, + CommonNames: []string{"some name"}, }, DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"bad"}, IPRanges: []string{"127.0.0.30"}, EmailAddresses: []string{"badhost.example.com"}, URIDomains: []string{"https://badhost.local"}, + CommonNames: []string{"another name"}, }, - AllowWildcardLiteral: true, - DisableCommonNameVerification: false, + AllowWildcardLiteral: true, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -369,8 +369,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableCommonNameVerification: false, + AllowWildcardLiteral: true, }, } @@ -429,8 +428,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableCommonNameVerification: false, + AllowWildcardLiteral: true, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -488,8 +486,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableCommonNameVerification: false, + AllowWildcardLiteral: true, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -700,8 +697,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableCommonNameVerification: false, + AllowWildcardLiteral: true, }, }, }, @@ -800,8 +796,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableCommonNameVerification: false, + AllowWildcardLiteral: true, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -916,8 +911,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Deny: &linkedca.X509Names{ Dns: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: true, }, Ssh: &linkedca.SSHPolicy{ Host: &linkedca.SSHHostPolicy{ @@ -982,8 +976,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Deny: &linkedca.X509Names{ Dns: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, - DisableSubjectCommonNameVerification: false, + AllowWildcardLiteral: true, }, }, nil }, diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 406b23b4..50af8396 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -70,11 +70,6 @@ type X509Options struct { // such as *.example.com and @example.com are allowed. Defaults // to false. AllowWildcardLiteral bool `json:"-"` - - // DisableCommonNameVerification indicates if the Subject Common Name - // is verified in addition to the SANs. Defaults to false, resulting - // in Common Names to be verified. - DisableCommonNameVerification bool `json:"-"` } // HasTemplate returns true if a template is defined in the provisioner options. @@ -107,13 +102,6 @@ func (o *X509Options) IsWildcardLiteralAllowed() bool { return o.AllowWildcardLiteral } -func (o *X509Options) ShouldVerifyCommonName() bool { - if o == nil { - return false - } - return !o.DisableCommonNameVerification -} - // TemplateOptions generates a CertificateOptions with the template and data // defined in the ProvisionerOptions, the provisioner generated data, and the // user data provided in the request. If no template has been provided, diff --git a/authority/provisioner/options_test.go b/authority/provisioner/options_test.go index 2edcdf3e..7883d045 100644 --- a/authority/provisioner/options_test.go +++ b/authority/provisioner/options_test.go @@ -322,38 +322,3 @@ func TestX509Options_IsWildcardLiteralAllowed(t *testing.T) { }) } } - -func TestX509Options_ShouldVerifySubjectCommonName(t *testing.T) { - tests := []struct { - name string - options *X509Options - want bool - }{ - { - name: "nil-options", - options: nil, - want: false, - }, - { - name: "set-true", - options: &X509Options{ - DisableCommonNameVerification: true, - }, - want: false, - }, - { - name: "set-false", - options: &X509Options{ - DisableCommonNameVerification: false, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.options.ShouldVerifyCommonName(); got != tt.want { - t.Errorf("X509PolicyOptions.ShouldVerifySubjectCommonName() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/authority/tls_test.go b/authority/tls_test.go index 3739dbff..9330f0a3 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -701,9 +701,9 @@ ZYtQ9Ot36qc= options := &policy.Options{ X509: &policy.X509PolicyOptions{ AllowedNames: &policy.X509NameOptions{ - DNSDomains: []string{"*.smallstep.com"}, + CommonNames: []string{"smallstep test"}, + DNSDomains: []string{"*.smallstep.com"}, }, - DisableCommonNameVerification: true, // TODO(hs): allows "smallstep test"; do we want to keep it like this? }, } engine, err := policy.New(options) diff --git a/policy/engine.go b/policy/engine.go index 3d8a4755..d1fb4928 100755 --- a/policy/engine.go +++ b/policy/engine.go @@ -2,7 +2,6 @@ package policy import ( "crypto/x509" - "crypto/x509/pkix" "fmt" "net" "net/url" @@ -33,6 +32,7 @@ const ( type NameType string const ( + CNNameType NameType = "cn" DNSNameType NameType = "dns" IPNameType NameType = "ip" EmailNameType NameType = "email" @@ -80,6 +80,8 @@ type NamePolicyEngine struct { allowLiteralWildcardNames bool // permitted and exluded constraints similar to x509 Name Constraints + permittedCommonNames []string + excludedCommonNames []string permittedDNSDomains []string excludedDNSDomains []string permittedIPRanges []*net.IPNet @@ -92,6 +94,7 @@ type NamePolicyEngine struct { excludedPrincipals []string // some internal counts for housekeeping + numberOfCommonNameConstraints int numberOfDNSDomainConstraints int numberOfIPRangeConstraints int numberOfEmailAddressConstraints int @@ -112,29 +115,34 @@ func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) { } } + e.permittedCommonNames = removeDuplicates(e.permittedCommonNames) e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains) e.permittedIPRanges = removeDuplicateIPNets(e.permittedIPRanges) e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses) e.permittedURIDomains = removeDuplicates(e.permittedURIDomains) e.permittedPrincipals = removeDuplicates(e.permittedPrincipals) + e.excludedCommonNames = removeDuplicates(e.excludedCommonNames) e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains) e.excludedIPRanges = removeDuplicateIPNets(e.excludedIPRanges) e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses) e.excludedURIDomains = removeDuplicates(e.excludedURIDomains) e.excludedPrincipals = removeDuplicates(e.excludedPrincipals) + e.numberOfCommonNameConstraints = len(e.permittedCommonNames) + len(e.excludedCommonNames) e.numberOfDNSDomainConstraints = len(e.permittedDNSDomains) + len(e.excludedDNSDomains) e.numberOfIPRangeConstraints = len(e.permittedIPRanges) + len(e.excludedIPRanges) e.numberOfEmailAddressConstraints = len(e.permittedEmailAddresses) + len(e.excludedEmailAddresses) e.numberOfURIDomainConstraints = len(e.permittedURIDomains) + len(e.excludedURIDomains) e.numberOfPrincipalConstraints = len(e.permittedPrincipals) + len(e.excludedPrincipals) - e.totalNumberOfPermittedConstraints = len(e.permittedDNSDomains) + len(e.permittedIPRanges) + - len(e.permittedEmailAddresses) + len(e.permittedURIDomains) + len(e.permittedPrincipals) + e.totalNumberOfPermittedConstraints = len(e.permittedCommonNames) + len(e.permittedDNSDomains) + + len(e.permittedIPRanges) + len(e.permittedEmailAddresses) + len(e.permittedURIDomains) + + len(e.permittedPrincipals) - e.totalNumberOfExcludedConstraints = len(e.excludedDNSDomains) + len(e.excludedIPRanges) + - len(e.excludedEmailAddresses) + len(e.excludedURIDomains) + len(e.excludedPrincipals) + e.totalNumberOfExcludedConstraints = len(e.excludedCommonNames) + len(e.excludedDNSDomains) + + len(e.excludedIPRanges) + len(e.excludedEmailAddresses) + len(e.excludedURIDomains) + + len(e.excludedPrincipals) e.totalNumberOfConstraints = e.totalNumberOfPermittedConstraints + e.totalNumberOfExcludedConstraints @@ -198,29 +206,27 @@ func removeDuplicateIPNets(items []*net.IPNet) (ret []*net.IPNet) { // IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed. func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) error { - dnsNames, ips, emails, uris := cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs - // when Subject Common Name must be verified in addition to the SANs, it is - // added to the appropriate slice of names. - if e.verifySubjectCommonName { - appendSubjectCommonName(cert.Subject, &dnsNames, &ips, &emails, &uris) - } - if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil { + if err := e.validateNames(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs, []string{}); err != nil { return err } + + if e.verifySubjectCommonName { + return e.validateCommonName(cert.Subject.CommonName) + } + return nil } // IsX509CertificateRequestAllowed verifies that all names in the CSR are allowed. func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error { - dnsNames, ips, emails, uris := csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs - // when Subject Common Name must be verified in addition to the SANs, it is - // added to the appropriate slice of names. - if e.verifySubjectCommonName { - appendSubjectCommonName(csr.Subject, &dnsNames, &ips, &emails, &uris) - } - if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil { + if err := e.validateNames(csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs, []string{}); err != nil { return err } + + if e.verifySubjectCommonName { + return e.validateCommonName(csr.Subject.CommonName) + } + return nil } @@ -262,22 +268,6 @@ func (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) error return nil } -// appendSubjectCommonName appends the Subject Common Name to the appropriate slice of names. The logic is -// similar as x509util.SplitSANs: if the subject can be parsed as an IP, it's added to the ips. If it can -// be parsed as an URL, it is added to the URIs. If it contains an @, it is added to emails. When it's none -// of these, it's added to the DNS names. -func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.IP, emails *[]string, uris *[]*url.URL) { - commonName := subject.CommonName - if commonName == "" { - return - } - subjectDNSNames, subjectIPs, subjectEmails, subjectURIs := x509util.SplitSANs([]string{commonName}) - *dnsNames = append(*dnsNames, subjectDNSNames...) - *ips = append(*ips, subjectIPs...) - *emails = append(*emails, subjectEmails...) - *uris = append(*uris, subjectURIs...) -} - // splitPrincipals splits SSH certificate principals into DNS names, emails and usernames. func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, principals []string, err error) { dnsNames = []string{} diff --git a/policy/engine_test.go b/policy/engine_test.go index deec2ff9..dd6db586 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -610,22 +610,6 @@ func TestNamePolicyEngine_matchURIConstraint(t *testing.T) { } } -func extractSANs(cert *x509.Certificate, includeSubject bool) []string { - sans := []string{} - sans = append(sans, cert.DNSNames...) - for _, ip := range cert.IPAddresses { - sans = append(sans, ip.String()) - } - sans = append(sans, cert.EmailAddresses...) - for _, uri := range cert.URIs { - sans = append(sans, uri.String()) - } - if includeSubject && cert.Subject.CommonName != "" { - sans = append(sans, cert.Subject.CommonName) - } - return sans -} - func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { tests := []struct { name string @@ -1140,6 +1124,42 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, }, // SUBJECT FAILURE TESTS + { + name: "fail/subject-permitted-no-match", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedCommonNames("this name is allowed", "and this one too"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "some certificate name", + }, + }, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, // only permitted names allowed + NameType: CNNameType, + Name: "some certificate name", + }, + }, + { + name: "fail/subject-excluded-match", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithExcludedCommonNames("this name is not allowed"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "this name is not allowed", + }, + }, + want: false, + wantErr: &NamePolicyError{ + Reason: CannotParseDomain, // CN cannot be parsed as DNS in this case + NameType: CNNameType, + Name: "this name is not allowed", + }, + }, { name: "fail/subject-dns-no-domain", options: []NamePolicyOption{ @@ -1154,7 +1174,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: CannotParseDomain, - NameType: DNSNameType, + NameType: CNNameType, Name: "name with space.local", }, }, @@ -1172,7 +1192,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: DNSNameType, + NameType: CNNameType, Name: "example.notlocal", }, }, @@ -1190,7 +1210,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: DNSNameType, + NameType: CNNameType, Name: "example.local", }, }, @@ -1213,7 +1233,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: IPNameType, + NameType: CNNameType, Name: "10.10.10.10", }, }, @@ -1236,7 +1256,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: IPNameType, + NameType: CNNameType, Name: "127.0.0.30", }, }, @@ -1259,7 +1279,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: IPNameType, + NameType: CNNameType, Name: "2002:db8:85a3::8a2e:370:7339", }, }, @@ -1282,7 +1302,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: IPNameType, + NameType: CNNameType, Name: "2001:db8:85a3::8a2e:370:7339", }, }, @@ -1300,7 +1320,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: EmailNameType, + NameType: CNNameType, Name: "mail@smallstep.com", }, }, @@ -1318,7 +1338,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: EmailNameType, + NameType: CNNameType, Name: "mail@example.local", }, }, @@ -1336,7 +1356,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: URINameType, + NameType: CNNameType, Name: "https://www.google.com", }, }, @@ -1354,7 +1374,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: URINameType, + NameType: CNNameType, Name: "https://www.example.com", }, }, @@ -1575,7 +1595,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, // COMBINED FAILURE TESTS { - name: "fail/combined-simple-all-badhost.local", + name: "fail/combined-simple-all-badhost.local-common-name", options: []NamePolicyOption{ WithSubjectCommonNameVerification(), WithPermittedDNSDomains("*.local"), @@ -1604,10 +1624,43 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { want: false, wantErr: &NamePolicyError{ Reason: NotAllowed, - NameType: DNSNameType, + NameType: CNNameType, Name: "badhost.local", }, }, + { + name: "fail/combined-simple-all-anotherbadhost.local-dns", + options: []NamePolicyOption{ + WithPermittedDNSDomains("*.local"), + WithPermittedCIDRs("127.0.0.1/24"), + WithPermittedEmailAddresses("@example.local"), + WithPermittedURIDomains("*.example.local"), + WithExcludedDNSDomains("anotherbadhost.local"), + WithExcludedCIDRs("127.0.0.128/25"), + WithExcludedEmailAddresses("badmail@example.local"), + WithExcludedURIDomains("badwww.example.local"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "badhost.local", + }, + DNSNames: []string{"anotherbadhost.local"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.40")}, + EmailAddresses: []string{"mail@example.local"}, + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "anotherbadhost.local", + }, + }, { name: "fail/combined-simple-all-badmail@example.local", options: []NamePolicyOption{ @@ -1715,6 +1768,32 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { }, want: true, }, + { + name: "ok/subject-permitted-match", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithPermittedCommonNames("this name is allowed"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "this name is allowed", + }, + }, + want: true, + }, + { + name: "ok/subject-excluded-match", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + WithExcludedCommonNames("this name is not allowed"), + }, + cert: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "some other name", + }, + }, + want: true, + }, // SINGLE SAN TYPE PERMITTED SUCCESS TESTS { name: "ok/dns-permitted", @@ -2433,26 +2512,6 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { assert.NotEqual(t, "", npe.Detail()) //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail } - - // Perform the same tests for a slice of SANs - includeSubject := engine.verifySubjectCommonName // copy behavior of the engine when Subject has to be included as a SAN - sans := extractSANs(tt.cert, includeSubject) - gotErr = engine.AreSANsAllowed(sans) - wantErr = tt.wantErr != nil - if (gotErr != nil) != wantErr { - t.Errorf("NamePolicyEngine.AreSANsAllowed() error = %v, wantErr %v", gotErr, tt.wantErr) - return - } - if gotErr != nil { - var npe *NamePolicyError - assert.True(t, errors.As(gotErr, &npe)) - assert.NotEqual(t, "", npe.Error()) - assert.Equal(t, tt.wantErr.Reason, npe.Reason) - assert.Equal(t, tt.wantErr.NameType, npe.NameType) - assert.Equal(t, tt.wantErr.Name, npe.Name) - assert.NotEqual(t, "", npe.Detail()) - //assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail - } }) } } diff --git a/policy/options.go b/policy/options.go index d244a311..79507f43 100755 --- a/policy/options.go +++ b/policy/options.go @@ -26,6 +26,20 @@ func WithAllowLiteralWildcardNames() NamePolicyOption { } } +func WithPermittedCommonNames(commonNames ...string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + g.permittedCommonNames = commonNames + return nil + } +} + +func WithExcludedCommonNames(commonNames ...string) NamePolicyOption { + return func(g *NamePolicyEngine) error { + g.excludedCommonNames = commonNames + return nil + } +} + func WithPermittedDNSDomains(domains ...string) NamePolicyOption { return func(e *NamePolicyEngine) error { normalizedDomains := make([]string, len(domains)) @@ -198,7 +212,6 @@ func WithExcludedURIDomains(domains ...string) NamePolicyOption { func WithPermittedPrincipals(principals ...string) NamePolicyOption { return func(g *NamePolicyEngine) error { - // TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do. g.permittedPrincipals = principals return nil } @@ -206,7 +219,6 @@ func WithPermittedPrincipals(principals ...string) NamePolicyOption { func WithExcludedPrincipals(principals ...string) NamePolicyOption { return func(g *NamePolicyEngine) error { - // TODO(hs): normalize and parse principal into the right type? Seems the safe thing to do. g.excludedPrincipals = principals return nil } diff --git a/policy/validate.go b/policy/validate.go index fff7120d..abd150db 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -15,6 +15,8 @@ import ( "strings" "golang.org/x/net/idna" + + "go.step.sm/crypto/x509util" ) // validateNames verifies that all names are allowed. @@ -71,7 +73,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA detail: fmt.Sprintf("cannot parse dns %q", dns), } } - if err := checkNameConstraints("dns", dns, parsedDNS, + if err := checkNameConstraints(DNSNameType, dns, parsedDNS, func(parsedName, constraint interface{}) (bool, error) { return e.matchDomainConstraint(parsedName.(string), constraint.(string)) }, e.permittedDNSDomains, e.excludedDNSDomains); err != nil { @@ -88,7 +90,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()), } } - if err := checkNameConstraints("ip", ip.String(), ip, + if err := checkNameConstraints(IPNameType, ip.String(), ip, func(parsedName, constraint interface{}) (bool, error) { return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet)) }, e.permittedIPRanges, e.excludedIPRanges); err != nil { @@ -127,7 +129,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } mailbox.domain = domainASCII - if err := checkNameConstraints("email", email, mailbox, + if err := checkNameConstraints(EmailNameType, email, mailbox, func(parsedName, constraint interface{}) (bool, error) { return e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) }, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil { @@ -148,7 +150,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } // 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(URINameType, uri.String(), uri, func(parsedName, constraint interface{}) (bool, error) { return e.matchURIConstraint(parsedName.(*url.URL), constraint.(string)) }, e.permittedURIDomains, e.excludedURIDomains); err != nil { @@ -166,9 +168,9 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA } } // TODO: some validation? I.e. allowed characters? - if err := checkNameConstraints("principal", principal, principal, + if err := checkNameConstraints(PrincipalNameType, principal, principal, func(parsedName, constraint interface{}) (bool, error) { - return matchUsernameConstraint(parsedName.(string), constraint.(string)) + return matchPrincipalConstraint(parsedName.(string), constraint.(string)) }, e.permittedPrincipals, e.excludedPrincipals); err != nil { return err } @@ -178,11 +180,51 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA return nil } +// validateCommonName verifies that the Subject Common Name is allowed +func (e *NamePolicyEngine) validateCommonName(commonName string) error { + + // nothing to compare against; return early + if e.totalNumberOfConstraints == 0 { + return nil + } + + // empty common names are not validated + if commonName == "" { + return nil + } + + if e.numberOfCommonNameConstraints > 0 { + // Check the Common Name using its dedicated matcher if constraints have been + // configured. If no error is returned from matching, the Common Name was + // explicitly allowed and nil is returned immediately. + if err := checkNameConstraints(CNNameType, commonName, commonName, + func(parsedName, constraint interface{}) (bool, error) { + return matchCommonNameConstraint(parsedName.(string), constraint.(string)) + }, e.permittedCommonNames, e.excludedCommonNames); err == nil { + return nil + } + } + + // When an error was returned or when no constraints were configured for Common Names, + // the Common Name should be validated against the other types of constraints too, + // according to what type it is. + dnsNames, ips, emails, uris := x509util.SplitSANs([]string{commonName}) + + err := e.validateNames(dnsNames, ips, emails, uris, []string{}) + + if pe, ok := err.(*NamePolicyError); ok { + // override the name type with CN + pe.NameType = CNNameType + } + + return err +} + // 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. func checkNameConstraints( - nameType string, + nameType NameType, name string, parsedName interface{}, match func(parsedName, constraint interface{}) (match bool, err error), @@ -196,7 +238,7 @@ func checkNameConstraints( if err != nil { return &NamePolicyError{ Reason: CannotMatchNameToConstraint, - NameType: NameType(nameType), + NameType: nameType, Name: name, detail: err.Error(), } @@ -205,7 +247,7 @@ func checkNameConstraints( if match { return &NamePolicyError{ Reason: NotAllowed, - NameType: NameType(nameType), + NameType: nameType, Name: name, detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint), } @@ -221,7 +263,7 @@ func checkNameConstraints( if ok, err = match(parsedName, constraint); err != nil { return &NamePolicyError{ Reason: CannotMatchNameToConstraint, - NameType: NameType(nameType), + NameType: nameType, Name: name, detail: err.Error(), } @@ -235,7 +277,7 @@ func checkNameConstraints( if !ok { return &NamePolicyError{ Reason: NotAllowed, - NameType: NameType(nameType), + NameType: nameType, Name: name, detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name), } @@ -591,11 +633,16 @@ func (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) ( return e.matchDomainConstraint(host, constraint) } -// matchUsernameConstraint performs a string literal match against a constraint. -func matchUsernameConstraint(username, constraint string) (bool, error) { - // allow any plain principal username +// matchPrincipalConstraint performs a string literal equality check against a constraint. +func matchPrincipalConstraint(principal, constraint string) (bool, error) { + // allow any plain principal when wildcard constraint is used if constraint == "*" { return true, nil } - return strings.EqualFold(username, constraint), nil + return strings.EqualFold(principal, constraint), nil +} + +// matchCommonNameConstraint performs a string literal equality check against constraint. +func matchCommonNameConstraint(commonName, constraint string) (bool, error) { + return strings.EqualFold(commonName, constraint), nil } From d82e51b74820bcc20ffef6862feb13b2614f715a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 29 Apr 2022 15:08:19 +0200 Subject: [PATCH 69/78] Update AllowWildcardNames configuration name --- acme/account.go | 25 +++++----- authority/admin/api/acme.go | 2 + authority/admin/api/acme_test.go | 2 + authority/policy.go | 2 +- authority/policy/options.go | 17 ++++--- authority/policy/options_test.go | 6 +-- authority/policy/policy.go | 2 +- authority/policy_test.go | 27 +++++------ authority/provisioner/options.go | 11 ++--- authority/provisioner/options_test.go | 6 +-- policy/engine_test.go | 68 ++++++++++++++++++--------- policy/validate.go | 9 +--- 12 files changed, 94 insertions(+), 83 deletions(-) diff --git a/acme/account.go b/acme/account.go index b83defe1..2dd412db 100644 --- a/acme/account.go +++ b/acme/account.go @@ -53,8 +53,9 @@ type PolicyNames struct { // X509Policy contains ACME account level X.509 policy type X509Policy struct { - Allowed PolicyNames `json:"allow"` - Denied PolicyNames `json:"deny"` + Allowed PolicyNames `json:"allow"` + Denied PolicyNames `json:"deny"` + AllowWildcardNames bool `json:"allowWildcardNames"` } // Policy is an ACME Account level policy @@ -81,18 +82,14 @@ func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions { } } -// IsWildcardLiteralAllowed returns true by default for -// ACME account policies, as authorization is performed on DNS -// level. -func (p *Policy) IsWildcardLiteralAllowed() bool { - return true -} - -// ShouldVerifySubjectCommonName returns true by default -// for ACME account policies, as this is embedded in the -// protocol. -func (p *Policy) ShouldVerifyCommonName() bool { - return true +// AreWildcardNamesAllowed returns if wildcard names +// like *.example.com are allowed to be signed. +// Defaults to false. +func (p *Policy) AreWildcardNamesAllowed() bool { + if p == nil { + return false + } + return p.X509.AllowWildcardNames } // ExternalAccountKey is an ACME External Account Binding key. diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 026443fa..31949081 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -109,6 +109,7 @@ func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey { eak.Policy.X509.Allow.Ips = k.Policy.X509.Allowed.IPRanges eak.Policy.X509.Deny.Dns = k.Policy.X509.Denied.DNSNames eak.Policy.X509.Deny.Ips = k.Policy.X509.Denied.IPRanges + eak.Policy.X509.AllowWildcardNames = k.Policy.X509.AllowWildcardNames } return eak @@ -143,6 +144,7 @@ func linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey { eak.Policy.X509.Denied.DNSNames = deny.Dns eak.Policy.X509.Denied.IPRanges = deny.Ips } + eak.Policy.X509.AllowWildcardNames = x509.AllowWildcardNames } } diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 5094d5f0..3ff32763 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -512,6 +512,7 @@ func Test_linkedEAKToCertificates(t *testing.T) { Dns: []string{"badhost.local"}, Ips: []string{"10.0.0.30"}, }, + AllowWildcardNames: true, }, }, }, @@ -533,6 +534,7 @@ func Test_linkedEAKToCertificates(t *testing.T) { DNSNames: []string{"badhost.local"}, IPRanges: []string{"10.0.0.30"}, }, + AllowWildcardNames: true, }, }, }, diff --git a/authority/policy.go b/authority/policy.go index dd38fd4a..063a464c 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -311,7 +311,7 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { } } - opts.X509.AllowWildcardLiteral = x509.AllowWildcardLiteral + opts.X509.AllowWildcardNames = x509.GetAllowWildcardNames() } // fill ssh policy configuration diff --git a/authority/policy/options.go b/authority/policy/options.go index 0f50083d..b93d2cd1 100644 --- a/authority/policy/options.go +++ b/authority/policy/options.go @@ -30,7 +30,7 @@ func (o *Options) GetSSHOptions() *SSHPolicyOptions { type X509PolicyOptionsInterface interface { GetAllowedNameOptions() *X509NameOptions GetDeniedNameOptions() *X509NameOptions - IsWildcardLiteralAllowed() bool + AreWildcardNamesAllowed() bool } // X509PolicyOptions is a container for x509 allowed and denied @@ -42,10 +42,9 @@ type X509PolicyOptions struct { // DeniedNames contains the x509 denied names DeniedNames *X509NameOptions `json:"deny,omitempty"` - // AllowWildcardLiteral indicates if literal wildcard names - // such as *.example.com and @example.com are allowed. Defaults - // to false. - AllowWildcardLiteral bool `json:"allowWildcardLiteral,omitempty"` + // AllowWildcardNames indicates if literal wildcard names + // like *.example.com are allowed. Defaults to false. + AllowWildcardNames bool `json:"allowWildcardNames,omitempty"` } // X509NameOptions models the X509 name policy configuration. @@ -83,13 +82,13 @@ func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { return o.DeniedNames } -// IsWildcardLiteralAllowed returns whether the authority allows -// literal wildcard domains and other names to be signed. -func (o *X509PolicyOptions) IsWildcardLiteralAllowed() bool { +// AreWildcardNamesAllowed returns whether the authority allows +// literal wildcard names to be signed. +func (o *X509PolicyOptions) AreWildcardNamesAllowed() bool { if o == nil { return true } - return o.AllowWildcardLiteral + return o.AllowWildcardNames } // SSHPolicyOptionsInterface is an interface for providers of diff --git a/authority/policy/options_test.go b/authority/policy/options_test.go index d7d42093..0fd6e7c6 100644 --- a/authority/policy/options_test.go +++ b/authority/policy/options_test.go @@ -23,21 +23,21 @@ func TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) { { name: "set-true", options: &X509PolicyOptions{ - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, want: true, }, { name: "set-false", options: &X509PolicyOptions{ - AllowWildcardLiteral: false, + AllowWildcardNames: false, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.options.IsWildcardLiteralAllowed(); got != tt.want { + if got := tt.options.AreWildcardNamesAllowed(); got != tt.want { t.Errorf("X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v", got, tt.want) } }) diff --git a/authority/policy/policy.go b/authority/policy/policy.go index f5f0fce3..52297d65 100644 --- a/authority/policy/policy.go +++ b/authority/policy/policy.go @@ -52,7 +52,7 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, return nil, nil } - if policyOptions.IsWildcardLiteralAllowed() { + if policyOptions.AreWildcardNamesAllowed() { options = append(options, policy.WithAllowLiteralWildcardNames()) } diff --git a/authority/policy_test.go b/authority/policy_test.go index 3f03abd9..e64752ec 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -218,7 +218,7 @@ func Test_policyToCertificates(t *testing.T) { Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, - AllowWildcardLiteral: false, + AllowWildcardNames: false, }, }, want: &policy.Options{ @@ -226,7 +226,7 @@ func Test_policyToCertificates(t *testing.T) { AllowedNames: &policy.X509NameOptions{ DNSDomains: []string{"*.local"}, }, - AllowWildcardLiteral: false, + AllowWildcardNames: false, }, }, }, @@ -248,7 +248,7 @@ func Test_policyToCertificates(t *testing.T) { Uris: []string{"https://badhost.local"}, CommonNames: []string{"another name"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, Ssh: &linkedca.SSHPolicy{ Host: &linkedca.SSHHostPolicy{ @@ -291,7 +291,7 @@ func Test_policyToCertificates(t *testing.T) { URIDomains: []string{"https://badhost.local"}, CommonNames: []string{"another name"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -369,7 +369,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, } @@ -428,7 +428,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -486,7 +486,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -697,7 +697,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, }, }, @@ -796,7 +796,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, SSH: &policy.SSHPolicyOptions{ Host: &policy.SSHHostCertificateOptions{ @@ -911,7 +911,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Deny: &linkedca.X509Names{ Dns: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, Ssh: &linkedca.SSHPolicy{ Host: &linkedca.SSHHostPolicy{ @@ -976,7 +976,7 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { Deny: &linkedca.X509Names{ Dns: []string{"badhost.local"}, }, - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, }, nil }, @@ -996,11 +996,6 @@ func TestAuthority_reloadPolicyEngines(t *testing.T) { t.Errorf("Authority.reloadPolicyEngines() error = %v, wantErr %v", err, tt.wantErr) } - // TODO(hs): fix those - // assert.Equal(t, tt.expected.x509Policy, a.x509Policy) - // assert.Equal(t, tt.expected.sshHostPolicy, a.sshHostPolicy) - // assert.Equal(t, tt.expected.sshUserPolicy, a.sshUserPolicy) - assert.Equal(t, tt.expected, a.policyEngine) }) } diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 50af8396..f5c919b4 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -66,10 +66,9 @@ type X509Options struct { // DeniedNames contains the SANs the provisioner is not authorized to sign DeniedNames *policy.X509NameOptions `json:"-"` - // AllowWildcardLiteral indicates if literal wildcard names - // such as *.example.com and @example.com are allowed. Defaults - // to false. - AllowWildcardLiteral bool `json:"-"` + // AllowWildcardNames indicates if literal wildcard names + // like *.example.com are allowed. Defaults to false. + AllowWildcardNames bool `json:"-"` } // HasTemplate returns true if a template is defined in the provisioner options. @@ -95,11 +94,11 @@ func (o *X509Options) GetDeniedNameOptions() *policy.X509NameOptions { return o.DeniedNames } -func (o *X509Options) IsWildcardLiteralAllowed() bool { +func (o *X509Options) AreWildcardNamesAllowed() bool { if o == nil { return true } - return o.AllowWildcardLiteral + return o.AllowWildcardNames } // TemplateOptions generates a CertificateOptions with the template and data diff --git a/authority/provisioner/options_test.go b/authority/provisioner/options_test.go index 7883d045..0bcf9ec3 100644 --- a/authority/provisioner/options_test.go +++ b/authority/provisioner/options_test.go @@ -302,21 +302,21 @@ func TestX509Options_IsWildcardLiteralAllowed(t *testing.T) { { name: "set-true", options: &X509Options{ - AllowWildcardLiteral: true, + AllowWildcardNames: true, }, want: true, }, { name: "set-false", options: &X509Options{ - AllowWildcardLiteral: false, + AllowWildcardNames: false, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.options.IsWildcardLiteralAllowed(); got != tt.want { + if got := tt.options.AreWildcardNamesAllowed(); got != tt.want { t.Errorf("X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v", got, tt.want) } }) diff --git a/policy/engine_test.go b/policy/engine_test.go index dd6db586..fabfebb9 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -285,17 +285,6 @@ func TestNamePolicyEngine_matchEmailConstraint(t *testing.T) { want bool wantErr bool }{ - { - name: "fail/asterisk-prefix", - engine: &NamePolicyEngine{}, - mailbox: rfc2821Mailbox{ - local: "mail", - domain: "local", - }, - constraint: "*@example.com", - want: false, - wantErr: true, - }, { name: "fail/asterisk-label", engine: &NamePolicyEngine{}, @@ -307,17 +296,6 @@ func TestNamePolicyEngine_matchEmailConstraint(t *testing.T) { want: false, wantErr: true, }, - { - name: "fail/asterisk-inside-local", - engine: &NamePolicyEngine{}, - mailbox: rfc2821Mailbox{ - local: "mail", - domain: "local", - }, - constraint: "m*il@local", - want: false, - wantErr: true, - }, { name: "fail/asterisk-inside-domain", engine: &NamePolicyEngine{}, @@ -358,7 +336,7 @@ func TestNamePolicyEngine_matchEmailConstraint(t *testing.T) { local: "mail", domain: "local", }, - constraint: ".local", // "wildcard" for the local domain; requires exactly 1 subdomain + constraint: ".local", want: false, wantErr: false, }, @@ -406,6 +384,50 @@ func TestNamePolicyEngine_matchEmailConstraint(t *testing.T) { want: true, wantErr: false, }, + { + name: "ok/asterisk-prefix", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "*@example.com", + want: false, + wantErr: false, + }, + { + name: "ok/asterisk-prefix-match", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "*", + domain: "example.com", + }, + constraint: "*@example.com", + want: true, + wantErr: false, + }, + { + name: "ok/asterisk-inside-local", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "mail", + domain: "local", + }, + constraint: "m*il@local", + want: false, + wantErr: false, + }, + { + name: "ok/asterisk-inside-local-match", + engine: &NamePolicyEngine{}, + mailbox: rfc2821Mailbox{ + local: "m*il", + domain: "local", + }, + constraint: "m*il@local", + want: true, + wantErr: false, + }, { name: "ok/specific-mail", engine: &NamePolicyEngine{}, diff --git a/policy/validate.go b/policy/validate.go index abd150db..968e936d 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -125,7 +125,7 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA Reason: CannotParseDomain, NameType: EmailNameType, Name: email, - detail: fmt.Sprintf("cannot parse email domain %q", email), + detail: fmt.Errorf("cannot parse email domain %q: %w", email, err).Error(), } } mailbox.domain = domainASCII @@ -577,11 +577,6 @@ func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) { // SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { - // TODO(hs): handle literal wildcard case for emails? Does that even make sense? - // If the constraint contains an @, then it specifies an exact mailbox name (currently) - if strings.Contains(constraint, "*") { - return false, fmt.Errorf("email constraint %q cannot contain asterisk", constraint) - } if strings.Contains(constraint, "@") { constraintMailbox, ok := parseRFC2821Mailbox(constraint) if !ok { @@ -617,7 +612,7 @@ func (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) ( if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") { var err error - host, _, err = net.SplitHostPort(uri.Host) + host, _, err = net.SplitHostPort(host) if err != nil { return false, err } From 77893ea55c6aeeac4f469de725c4e63d71df6782 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 2 May 2022 15:55:26 +0200 Subject: [PATCH 70/78] Change authority policy to use dbPolicy model --- authority/admin/db/nosql/policy.go | 230 ++++++++++- authority/admin/db/nosql/policy_test.go | 497 +++++++++++++++++++++++- 2 files changed, 706 insertions(+), 21 deletions(-) diff --git a/authority/admin/db/nosql/policy.go b/authority/admin/db/nosql/policy.go index b309f50c..d4f2e9f9 100644 --- a/authority/admin/db/nosql/policy.go +++ b/authority/admin/db/nosql/policy.go @@ -11,17 +11,64 @@ import ( "github.com/smallstep/nosql" ) +type dbX509Policy struct { + Allow *dbX509Names `json:"allow,omitempty"` + Deny *dbX509Names `json:"deny,omitempty"` + AllowWildcardNames bool `json:"allow_wildcard_names,omitempty"` +} + +type dbX509Names struct { + CommonNames []string `json:"cn,omitempty"` + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` + EmailAddresses []string `json:"email,omitempty"` + URIDomains []string `json:"uri,omitempty"` +} + +type dbSSHPolicy struct { + // User contains SSH user certificate options. + User *dbSSHUserPolicy `json:"user,omitempty"` + // Host contains SSH host certificate options. + Host *dbSSHHostPolicy `json:"host,omitempty"` +} + +type dbSSHHostPolicy struct { + Allow *dbSSHHostNames `json:"allow,omitempty"` + Deny *dbSSHHostNames `json:"deny,omitempty"` +} + +type dbSSHHostNames struct { + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` + Principals []string `json:"principal,omitempty"` +} + +type dbSSHUserPolicy struct { + Allow *dbSSHUserNames `json:"allow,omitempty"` + Deny *dbSSHUserNames `json:"deny,omitempty"` +} + +type dbSSHUserNames struct { + EmailAddresses []string `json:"email,omitempty"` + Principals []string `json:"principal,omitempty"` +} + +type dbPolicy struct { + X509 *dbX509Policy `json:"x509,omitempty"` + SSH *dbSSHPolicy `json:"ssh,omitempty"` +} + type dbAuthorityPolicy struct { - ID string `json:"id"` - AuthorityID string `json:"authorityID"` - Policy *linkedca.Policy `json:"policy"` + ID string `json:"id"` + AuthorityID string `json:"authorityID"` + Policy *dbPolicy `json:"policy,omitempty"` } func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy { if dbap == nil { return nil } - return dbap.Policy + return dbToLinked(dbap.Policy) } func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) { @@ -69,7 +116,7 @@ func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy dbap := &dbAuthorityPolicy{ ID: db.authorityID, AuthorityID: db.authorityID, - Policy: policy, + Policy: linkedToDB(policy), } if err := db.save(ctx, dbap.ID, dbap, nil, "authority_policy", authorityPoliciesTable); err != nil { @@ -97,7 +144,7 @@ func (db *DB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy dbap := &dbAuthorityPolicy{ ID: db.authorityID, AuthorityID: db.authorityID, - Policy: policy, + Policy: linkedToDB(policy), } if err := db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable); err != nil { @@ -119,3 +166,174 @@ func (db *DB) DeleteAuthorityPolicy(ctx context.Context) error { return nil } + +func dbToLinked(p *dbPolicy) *linkedca.Policy { + if p == nil { + return nil + } + r := &linkedca.Policy{} + if x509 := p.X509; x509 != nil { + r.X509 = &linkedca.X509Policy{} + if allow := x509.Allow; allow != nil { + r.X509.Allow = &linkedca.X509Names{} + r.X509.Allow.Dns = allow.DNSDomains + r.X509.Allow.Emails = allow.EmailAddresses + r.X509.Allow.Ips = allow.IPRanges + r.X509.Allow.Uris = allow.URIDomains + r.X509.Allow.CommonNames = allow.CommonNames + } + if deny := x509.Deny; deny != nil { + r.X509.Deny = &linkedca.X509Names{} + r.X509.Deny.Dns = deny.DNSDomains + r.X509.Deny.Emails = deny.EmailAddresses + r.X509.Deny.Ips = deny.IPRanges + r.X509.Deny.Uris = deny.URIDomains + r.X509.Deny.CommonNames = deny.CommonNames + } + r.X509.AllowWildcardNames = x509.AllowWildcardNames + } + if ssh := p.SSH; ssh != nil { + r.Ssh = &linkedca.SSHPolicy{} + if host := ssh.Host; host != nil { + r.Ssh.Host = &linkedca.SSHHostPolicy{} + if allow := host.Allow; allow != nil { + r.Ssh.Host.Allow = &linkedca.SSHHostNames{} + r.Ssh.Host.Allow.Dns = allow.DNSDomains + r.Ssh.Host.Allow.Ips = allow.IPRanges + r.Ssh.Host.Allow.Principals = allow.Principals + } + if deny := host.Deny; deny != nil { + r.Ssh.Host.Deny = &linkedca.SSHHostNames{} + r.Ssh.Host.Deny.Dns = deny.DNSDomains + r.Ssh.Host.Deny.Ips = deny.IPRanges + r.Ssh.Host.Deny.Principals = deny.Principals + } + } + if user := ssh.User; user != nil { + r.Ssh.User = &linkedca.SSHUserPolicy{} + if allow := user.Allow; allow != nil { + r.Ssh.User.Allow = &linkedca.SSHUserNames{} + r.Ssh.User.Allow.Emails = allow.EmailAddresses + r.Ssh.User.Allow.Principals = allow.Principals + } + if deny := user.Deny; deny != nil { + r.Ssh.User.Deny = &linkedca.SSHUserNames{} + r.Ssh.User.Deny.Emails = deny.EmailAddresses + r.Ssh.User.Deny.Principals = deny.Principals + } + } + } + + return r +} + +func linkedToDB(p *linkedca.Policy) *dbPolicy { + + if p == nil { + return nil + } + + // return early if x509 nor SSH is set + if p.GetX509() == nil && p.GetSsh() == nil { + return nil + } + + r := &dbPolicy{} + // fill x509 policy configuration + if x509 := p.GetX509(); x509 != nil { + r.X509 = &dbX509Policy{} + if allow := x509.GetAllow(); allow != nil { + r.X509.Allow = &dbX509Names{} + if allow.Dns != nil { + r.X509.Allow.DNSDomains = allow.Dns + } + if allow.Ips != nil { + r.X509.Allow.IPRanges = allow.Ips + } + if allow.Emails != nil { + r.X509.Allow.EmailAddresses = allow.Emails + } + if allow.Uris != nil { + r.X509.Allow.URIDomains = allow.Uris + } + if allow.CommonNames != nil { + r.X509.Allow.CommonNames = allow.CommonNames + } + } + if deny := x509.GetDeny(); deny != nil { + r.X509.Deny = &dbX509Names{} + if deny.Dns != nil { + r.X509.Deny.DNSDomains = deny.Dns + } + if deny.Ips != nil { + r.X509.Deny.IPRanges = deny.Ips + } + if deny.Emails != nil { + r.X509.Deny.EmailAddresses = deny.Emails + } + if deny.Uris != nil { + r.X509.Deny.URIDomains = deny.Uris + } + if deny.CommonNames != nil { + r.X509.Deny.CommonNames = deny.CommonNames + } + } + + r.X509.AllowWildcardNames = x509.GetAllowWildcardNames() + } + + // fill ssh policy configuration + if ssh := p.GetSsh(); ssh != nil { + r.SSH = &dbSSHPolicy{} + if host := ssh.GetHost(); host != nil { + r.SSH.Host = &dbSSHHostPolicy{} + if allow := host.GetAllow(); allow != nil { + r.SSH.Host.Allow = &dbSSHHostNames{} + if allow.Dns != nil { + r.SSH.Host.Allow.DNSDomains = allow.Dns + } + if allow.Ips != nil { + r.SSH.Host.Allow.IPRanges = allow.Ips + } + if allow.Principals != nil { + r.SSH.Host.Allow.Principals = allow.Principals + } + } + if deny := host.GetDeny(); deny != nil { + r.SSH.Host.Deny = &dbSSHHostNames{} + if deny.Dns != nil { + r.SSH.Host.Deny.DNSDomains = deny.Dns + } + if deny.Ips != nil { + r.SSH.Host.Deny.IPRanges = deny.Ips + } + if deny.Principals != nil { + r.SSH.Host.Deny.Principals = deny.Principals + } + } + } + if user := ssh.GetUser(); user != nil { + r.SSH.User = &dbSSHUserPolicy{} + if allow := user.GetAllow(); allow != nil { + r.SSH.User.Allow = &dbSSHUserNames{} + if allow.Emails != nil { + r.SSH.User.Allow.EmailAddresses = allow.Emails + } + if allow.Principals != nil { + r.SSH.User.Allow.Principals = allow.Principals + } + } + if deny := user.GetDeny(); deny != nil { + r.SSH.User.Deny = &dbSSHUserNames{} + if deny.Emails != nil { + r.SSH.User.Deny.EmailAddresses = deny.Emails + } + if deny.Principals != nil { + r.SSH.User.Deny.Principals = deny.Principals + } + } + } + } + + return r +} diff --git a/authority/admin/db/nosql/policy_test.go b/authority/admin/db/nosql/policy_test.go index 39be7e13..3ffded6b 100644 --- a/authority/admin/db/nosql/policy_test.go +++ b/authority/admin/db/nosql/policy_test.go @@ -4,15 +4,15 @@ import ( "context" "encoding/json" "errors" + "reflect" "testing" - "go.step.sm/linkedca" - "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" nosqldb "github.com/smallstep/nosql/database" + "go.step.sm/linkedca" ) func TestDB_getDBAuthorityPolicyBytes(t *testing.T) { @@ -136,13 +136,13 @@ func TestDB_getDBAuthorityPolicy(t *testing.T) { dbp := &dbAuthorityPolicy{ ID: "ID", AuthorityID: "diffAuthID", - Policy: &linkedca.Policy{ + Policy: linkedToDB(&linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, }, - }, + }), } b, err := json.Marshal(dbp) assert.FatalError(t, err) @@ -177,13 +177,13 @@ func TestDB_getDBAuthorityPolicy(t *testing.T) { dbap := &dbAuthorityPolicy{ ID: "ID", AuthorityID: authID, - Policy: &linkedca.Policy{ + Policy: linkedToDB(&linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ Dns: []string{"*.local"}, }, }, - }, + }), } b, err := json.Marshal(dbap) assert.FatalError(t, err) @@ -266,7 +266,7 @@ func TestDB_CreateAuthorityPolicy(t *testing.T) { assert.Equals(t, _dbap.ID, authID) assert.Equals(t, _dbap.AuthorityID, authID) - assert.Equals(t, _dbap.Policy, policy) + assert.Equals(t, _dbap.Policy, linkedToDB(policy)) return nil, false, errors.New("force") }, @@ -296,7 +296,7 @@ func TestDB_CreateAuthorityPolicy(t *testing.T) { assert.Equals(t, _dbap.ID, authID) assert.Equals(t, _dbap.AuthorityID, authID) - assert.Equals(t, _dbap.Policy, policy) + assert.Equals(t, _dbap.Policy, linkedToDB(policy)) return nil, true, nil }, @@ -388,7 +388,7 @@ func TestDB_GetAuthorityPolicy(t *testing.T) { dbap := &dbAuthorityPolicy{ ID: authID, AuthorityID: authID, - Policy: policy, + Policy: linkedToDB(policy), } b, err := json.Marshal(dbap) @@ -496,7 +496,7 @@ func TestDB_UpdateAuthorityPolicy(t *testing.T) { dbap := &dbAuthorityPolicy{ ID: authID, AuthorityID: authID, - Policy: oldPolicy, + Policy: linkedToDB(oldPolicy), } b, err := json.Marshal(dbap) @@ -513,7 +513,7 @@ func TestDB_UpdateAuthorityPolicy(t *testing.T) { assert.Equals(t, _dbap.ID, authID) assert.Equals(t, _dbap.AuthorityID, authID) - assert.Equals(t, _dbap.Policy, policy) + assert.Equals(t, _dbap.Policy, linkedToDB(policy)) return nil, false, errors.New("force") }, @@ -548,7 +548,7 @@ func TestDB_UpdateAuthorityPolicy(t *testing.T) { dbap := &dbAuthorityPolicy{ ID: authID, AuthorityID: authID, - Policy: oldPolicy, + Policy: linkedToDB(oldPolicy), } b, err := json.Marshal(dbap) @@ -565,7 +565,7 @@ func TestDB_UpdateAuthorityPolicy(t *testing.T) { assert.Equals(t, _dbap.ID, authID) assert.Equals(t, _dbap.AuthorityID, authID) - assert.Equals(t, _dbap.Policy, policy) + assert.Equals(t, _dbap.Policy, linkedToDB(policy)) return nil, true, nil }, @@ -656,7 +656,7 @@ func TestDB_DeleteAuthorityPolicy(t *testing.T) { dbap := &dbAuthorityPolicy{ ID: authID, AuthorityID: authID, - Policy: oldPolicy, + Policy: linkedToDB(oldPolicy), } b, err := json.Marshal(dbap) @@ -694,7 +694,7 @@ func TestDB_DeleteAuthorityPolicy(t *testing.T) { dbap := &dbAuthorityPolicy{ ID: authID, AuthorityID: authID, - Policy: oldPolicy, + Policy: linkedToDB(oldPolicy), } b, err := json.Marshal(dbap) @@ -737,3 +737,470 @@ func TestDB_DeleteAuthorityPolicy(t *testing.T) { }) } } + +func Test_linkedToDB(t *testing.T) { + type args struct { + p *linkedca.Policy + } + tests := []struct { + name string + args args + want *dbPolicy + }{ + { + name: "nil policy", + args: args{ + p: nil, + }, + want: nil, + }, + { + name: "no x509 nor ssh", + args: args{ + p: &linkedca.Policy{}, + }, + want: nil, + }, + { + name: "x509", + args: args{ + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + Ips: []string{"192.168.0.1/24"}, + Emails: []string{"@example.com"}, + Uris: []string{"*.example.com"}, + CommonNames: []string{"some name"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"badhost.local"}, + Ips: []string{"192.168.0.30"}, + Emails: []string{"root@example.com"}, + Uris: []string{"bad.example.com"}, + CommonNames: []string{"bad name"}, + }, + AllowWildcardNames: true, + }, + }, + }, + want: &dbPolicy{ + X509: &dbX509Policy{ + Allow: &dbX509Names{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"192.168.0.1/24"}, + EmailAddresses: []string{"@example.com"}, + URIDomains: []string{"*.example.com"}, + CommonNames: []string{"some name"}, + }, + Deny: &dbX509Names{ + DNSDomains: []string{"badhost.local"}, + IPRanges: []string{"192.168.0.30"}, + EmailAddresses: []string{"root@example.com"}, + URIDomains: []string{"bad.example.com"}, + CommonNames: []string{"bad name"}, + }, + AllowWildcardNames: true, + }, + }, + }, + { + name: "ssh user", + args: args{ + p: &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + Principals: []string{"user"}, + }, + Deny: &linkedca.SSHUserNames{ + Emails: []string{"root@example.com"}, + Principals: []string{"root"}, + }, + }, + }, + }, + }, + want: &dbPolicy{ + SSH: &dbSSHPolicy{ + User: &dbSSHUserPolicy{ + Allow: &dbSSHUserNames{ + EmailAddresses: []string{"@example.com"}, + Principals: []string{"user"}, + }, + Deny: &dbSSHUserNames{ + EmailAddresses: []string{"root@example.com"}, + Principals: []string{"root"}, + }, + }, + }, + }, + }, + { + name: "full ssh policy", + args: args{ + p: &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.local"}, + Ips: []string{"192.168.0.1/24"}, + Principals: []string{"host"}, + }, + Deny: &linkedca.SSHHostNames{ + Dns: []string{"badhost.local"}, + Ips: []string{"192.168.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, + }, + want: &dbPolicy{ + SSH: &dbSSHPolicy{ + Host: &dbSSHHostPolicy{ + Allow: &dbSSHHostNames{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"192.168.0.1/24"}, + Principals: []string{"host"}, + }, + Deny: &dbSSHHostNames{ + DNSDomains: []string{"badhost.local"}, + IPRanges: []string{"192.168.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, + }, + { + name: "full policy", + args: args{ + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + Ips: []string{"192.168.0.1/24"}, + Emails: []string{"@example.com"}, + Uris: []string{"*.example.com"}, + CommonNames: []string{"some name"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"badhost.local"}, + Ips: []string{"192.168.0.30"}, + Emails: []string{"root@example.com"}, + Uris: []string{"bad.example.com"}, + CommonNames: []string{"bad name"}, + }, + AllowWildcardNames: true, + }, + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + Principals: []string{"user"}, + }, + Deny: &linkedca.SSHUserNames{ + Emails: []string{"root@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.local"}, + Ips: []string{"192.168.0.1/24"}, + Principals: []string{"host"}, + }, + Deny: &linkedca.SSHHostNames{ + Dns: []string{"badhost.local"}, + Ips: []string{"192.168.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, + }, + want: &dbPolicy{ + X509: &dbX509Policy{ + Allow: &dbX509Names{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"192.168.0.1/24"}, + EmailAddresses: []string{"@example.com"}, + URIDomains: []string{"*.example.com"}, + CommonNames: []string{"some name"}, + }, + Deny: &dbX509Names{ + DNSDomains: []string{"badhost.local"}, + IPRanges: []string{"192.168.0.30"}, + EmailAddresses: []string{"root@example.com"}, + URIDomains: []string{"bad.example.com"}, + CommonNames: []string{"bad name"}, + }, + AllowWildcardNames: true, + }, + SSH: &dbSSHPolicy{ + User: &dbSSHUserPolicy{ + Allow: &dbSSHUserNames{ + EmailAddresses: []string{"@example.com"}, + Principals: []string{"user"}, + }, + Deny: &dbSSHUserNames{ + EmailAddresses: []string{"root@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &dbSSHHostPolicy{ + Allow: &dbSSHHostNames{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"192.168.0.1/24"}, + Principals: []string{"host"}, + }, + Deny: &dbSSHHostNames{ + DNSDomains: []string{"badhost.local"}, + IPRanges: []string{"192.168.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := linkedToDB(tt.args.p); !reflect.DeepEqual(got, tt.want) { + t.Errorf("linkedToDB() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_dbToLinked(t *testing.T) { + type args struct { + p *dbPolicy + } + tests := []struct { + name string + args args + want *linkedca.Policy + }{ + { + name: "nil policy", + args: args{ + p: nil, + }, + want: nil, + }, + { + name: "x509", + args: args{ + p: &dbPolicy{ + X509: &dbX509Policy{ + Allow: &dbX509Names{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"192.168.0.1/24"}, + EmailAddresses: []string{"@example.com"}, + URIDomains: []string{"*.example.com"}, + CommonNames: []string{"some name"}, + }, + Deny: &dbX509Names{ + DNSDomains: []string{"badhost.local"}, + IPRanges: []string{"192.168.0.30"}, + EmailAddresses: []string{"root@example.com"}, + URIDomains: []string{"bad.example.com"}, + CommonNames: []string{"bad name"}, + }, + AllowWildcardNames: true, + }, + }, + }, + want: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + Ips: []string{"192.168.0.1/24"}, + Emails: []string{"@example.com"}, + Uris: []string{"*.example.com"}, + CommonNames: []string{"some name"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"badhost.local"}, + Ips: []string{"192.168.0.30"}, + Emails: []string{"root@example.com"}, + Uris: []string{"bad.example.com"}, + CommonNames: []string{"bad name"}, + }, + AllowWildcardNames: true, + }, + }, + }, + { + name: "ssh user", + args: args{ + p: &dbPolicy{ + SSH: &dbSSHPolicy{ + User: &dbSSHUserPolicy{ + Allow: &dbSSHUserNames{ + EmailAddresses: []string{"@example.com"}, + Principals: []string{"user"}, + }, + Deny: &dbSSHUserNames{ + EmailAddresses: []string{"root@example.com"}, + Principals: []string{"root"}, + }, + }, + }, + }, + }, + want: &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + Principals: []string{"user"}, + }, + Deny: &linkedca.SSHUserNames{ + Emails: []string{"root@example.com"}, + Principals: []string{"root"}, + }, + }, + }, + }, + }, + { + name: "ssh host", + args: args{ + p: &dbPolicy{ + SSH: &dbSSHPolicy{ + Host: &dbSSHHostPolicy{ + Allow: &dbSSHHostNames{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"192.168.0.1/24"}, + Principals: []string{"host"}, + }, + Deny: &dbSSHHostNames{ + DNSDomains: []string{"badhost.local"}, + IPRanges: []string{"192.168.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, + }, + want: &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.local"}, + Ips: []string{"192.168.0.1/24"}, + Principals: []string{"host"}, + }, + Deny: &linkedca.SSHHostNames{ + Dns: []string{"badhost.local"}, + Ips: []string{"192.168.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, + }, + { + name: "full policy", + args: args{ + p: &dbPolicy{ + X509: &dbX509Policy{ + Allow: &dbX509Names{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"192.168.0.1/24"}, + EmailAddresses: []string{"@example.com"}, + URIDomains: []string{"*.example.com"}, + CommonNames: []string{"some name"}, + }, + Deny: &dbX509Names{ + DNSDomains: []string{"badhost.local"}, + IPRanges: []string{"192.168.0.30"}, + EmailAddresses: []string{"root@example.com"}, + URIDomains: []string{"bad.example.com"}, + CommonNames: []string{"bad name"}, + }, + AllowWildcardNames: true, + }, + SSH: &dbSSHPolicy{ + User: &dbSSHUserPolicy{ + Allow: &dbSSHUserNames{ + EmailAddresses: []string{"@example.com"}, + Principals: []string{"user"}, + }, + Deny: &dbSSHUserNames{ + EmailAddresses: []string{"root@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &dbSSHHostPolicy{ + Allow: &dbSSHHostNames{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"192.168.0.1/24"}, + Principals: []string{"host"}, + }, + Deny: &dbSSHHostNames{ + DNSDomains: []string{"badhost.local"}, + IPRanges: []string{"192.168.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, + }, + want: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + Ips: []string{"192.168.0.1/24"}, + Emails: []string{"@example.com"}, + Uris: []string{"*.example.com"}, + CommonNames: []string{"some name"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"badhost.local"}, + Ips: []string{"192.168.0.30"}, + Emails: []string{"root@example.com"}, + Uris: []string{"bad.example.com"}, + CommonNames: []string{"bad name"}, + }, + AllowWildcardNames: true, + }, + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + Principals: []string{"user"}, + }, + Deny: &linkedca.SSHUserNames{ + Emails: []string{"root@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.local"}, + Ips: []string{"192.168.0.1/24"}, + Principals: []string{"host"}, + }, + Deny: &linkedca.SSHHostNames{ + Dns: []string{"badhost.local"}, + Ips: []string{"192.168.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := dbToLinked(tt.args.p); !reflect.DeepEqual(got, tt.want) { + t.Errorf("dbToLinked() = %v, want %v", got, tt.want) + } + }) + } +} From 60d8b22d89a5eb5b68e9156e27c68b49a8511949 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 5 May 2022 11:05:57 +0200 Subject: [PATCH 71/78] Change context retrievers to MustTFromContext --- authority/admin/api/acme.go | 2 +- authority/admin/api/middleware.go | 2 +- authority/admin/api/middleware_test.go | 8 ++--- authority/admin/api/policy.go | 26 +++++++-------- authority/provisioners.go | 4 +-- go.mod | 14 ++++++-- go.sum | 46 +++++++++++++++++--------- 7 files changed, 63 insertions(+), 39 deletions(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 31949081..da491dfe 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -36,7 +36,7 @@ type GetExternalAccountKeysResponse struct { func (h *Handler) requireEABEnabled(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - prov := linkedca.ProvisionerFromContext(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) acmeProvisioner := prov.GetDetails().GetACME() if acmeProvisioner == nil { diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index af3dac5d..24adfdf2 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -107,7 +107,7 @@ func (h *Handler) checkAction(next http.HandlerFunc, supportedInStandalone bool) func (h *Handler) loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - prov := linkedca.ProvisionerFromContext(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) reference := chi.URLParam(r, "reference") keyID := chi.URLParam(r, "keyID") diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index 5936563d..42caed9a 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -176,7 +176,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { } next := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - adm := linkedca.AdminFromContext(ctx) // verifying that the context now has a linkedca.Admin + adm := linkedca.MustAdminFromContext(ctx) // verifying that the context now has a linkedca.Admin opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} if !cmp.Equal(adm, adm, opts...) { t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(adm, adm, opts...)) @@ -314,7 +314,7 @@ func TestHandler_loadProvisionerByName(t *testing.T) { adminDB: db, statusCode: 200, next: func(w http.ResponseWriter, r *http.Request) { - prov := linkedca.ProvisionerFromContext(r.Context()) + prov := linkedca.MustProvisionerFromContext(r.Context()) assert.NotNil(t, prov) assert.Equals(t, "provID", prov.GetId()) assert.Equals(t, "provName", prov.GetName()) @@ -588,7 +588,7 @@ func TestHandler_loadExternalAccountKey(t *testing.T) { }, }, next: func(w http.ResponseWriter, r *http.Request) { - contextEAK := linkedca.ExternalAccountKeyFromContext(r.Context()) + contextEAK := linkedca.MustExternalAccountKeyFromContext(r.Context()) assert.NotNil(t, eak) exp := &linkedca.EABKey{ Id: "eakID", @@ -632,7 +632,7 @@ func TestHandler_loadExternalAccountKey(t *testing.T) { }, }, next: func(w http.ResponseWriter, r *http.Request) { - contextEAK := linkedca.ExternalAccountKeyFromContext(r.Context()) + contextEAK := linkedca.MustExternalAccountKeyFromContext(r.Context()) assert.NotNil(t, eak) exp := &linkedca.EABKey{ Id: "eakID", diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 70c6f01d..275b947c 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -104,7 +104,7 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r newPolicy.Deduplicate() - adm := linkedca.AdminFromContext(ctx) + adm := linkedca.MustAdminFromContext(ctx) var createdPolicy *linkedca.Policy if createdPolicy, err = par.auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil { @@ -149,7 +149,7 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r newPolicy.Deduplicate() - adm := linkedca.AdminFromContext(ctx) + adm := linkedca.MustAdminFromContext(ctx) var updatedPolicy *linkedca.Policy if updatedPolicy, err = par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil { @@ -202,7 +202,7 @@ func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r * return } - prov := linkedca.ProvisionerFromContext(r.Context()) + prov := linkedca.MustProvisionerFromContext(r.Context()) policy := prov.GetPolicy() if policy == nil { @@ -222,7 +222,7 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, } ctx := r.Context() - prov := linkedca.ProvisionerFromContext(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) policy := prov.GetPolicy() if policy != nil { @@ -263,7 +263,7 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, } ctx := r.Context() - prov := linkedca.ProvisionerFromContext(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) if prov.Policy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) @@ -301,7 +301,7 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, } ctx := r.Context() - prov := linkedca.ProvisionerFromContext(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) if prov.Policy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) @@ -327,7 +327,7 @@ func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r * } ctx := r.Context() - eak := linkedca.ExternalAccountKeyFromContext(ctx) + eak := linkedca.MustExternalAccountKeyFromContext(ctx) policy := eak.GetPolicy() if policy == nil { @@ -346,8 +346,8 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, } ctx := r.Context() - prov := linkedca.ProvisionerFromContext(ctx) - eak := linkedca.ExternalAccountKeyFromContext(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) + eak := linkedca.MustExternalAccountKeyFromContext(ctx) policy := eak.GetPolicy() if policy != nil { @@ -383,8 +383,8 @@ func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, } ctx := r.Context() - prov := linkedca.ProvisionerFromContext(ctx) - eak := linkedca.ExternalAccountKeyFromContext(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) + eak := linkedca.MustExternalAccountKeyFromContext(ctx) policy := eak.GetPolicy() if policy == nil { @@ -418,8 +418,8 @@ func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, } ctx := r.Context() - prov := linkedca.ProvisionerFromContext(ctx) - eak := linkedca.ExternalAccountKeyFromContext(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) + eak := linkedca.MustExternalAccountKeyFromContext(ctx) policy := eak.GetPolicy() if policy == nil { diff --git a/authority/provisioners.go b/authority/provisioners.go index 26aff4d8..cde4a6e9 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -173,7 +173,7 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi return admin.WrapErrorISE(err, "error generating provisioner config") } - adm := linkedca.AdminFromContext(ctx) + adm := linkedca.MustAdminFromContext(ctx) if err := a.checkProvisionerPolicy(ctx, adm, prov.Name, prov.Policy); err != nil { return err @@ -224,7 +224,7 @@ func (a *Authority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisio return admin.WrapErrorISE(err, "error generating provisioner config") } - adm := linkedca.AdminFromContext(ctx) + adm := linkedca.MustAdminFromContext(ctx) if err := a.checkProvisionerPolicy(ctx, adm, nu.Name, nu.Policy); err != nil { return err diff --git a/go.mod b/go.mod index 104538a3..aa836f4f 100644 --- a/go.mod +++ b/go.mod @@ -14,21 +14,26 @@ require ( github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Masterminds/sprig/v3 v3.2.2 github.com/ThalesIgnite/crypto11 v1.2.4 - github.com/aws/aws-sdk-go v1.30.29 + github.com/aws/aws-sdk-go v1.37.0 github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect + github.com/fatih/color v1.9.0 // indirect + github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/go-chi/chi v4.0.2+incompatible github.com/go-kit/kit v0.10.0 // indirect github.com/go-piv/piv-go v1.7.0 + github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/mock v1.6.0 - github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.7 github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.1.1 github.com/hashicorp/vault/api v1.3.1 github.com/hashicorp/vault/api/auth/approle v0.1.1 + github.com/jhump/protoreflect v1.9.0 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.13 // indirect github.com/micromdm/scep/v2 v2.1.0 + github.com/miekg/pkcs11 v1.0.3 // indirect github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.9.1 github.com/rs/xid v1.2.1 @@ -38,18 +43,21 @@ require ( github.com/smallstep/nosql v0.4.0 github.com/stretchr/testify v1.7.1 github.com/urfave/cli v1.22.4 + go.etcd.io/bbolt v1.3.6 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.0 go.step.sm/crypto v0.16.1 - go.step.sm/linkedca v0.15.0 + go.step.sm/linkedca v0.16.0 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20220403103023-749bd193bc2b golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect + golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/api v0.70.0 google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de google.golang.org/grpc v1.45.0 google.golang.org/protobuf v1.28.0 gopkg.in/square/go-jose.v2 v2.6.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) // replace github.com/smallstep/nosql => ../nosql diff --git a/go.sum b/go.sum index b2f0e658..a0b45a39 100644 --- a/go.sum +++ b/go.sum @@ -143,8 +143,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0= -github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.37.0 h1:GzFnhOIsrGyQ69s7VgqtrG2BG8v7X7vwB3Xpbd/DBBk= +github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -235,13 +235,15 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= @@ -269,8 +271,9 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-piv/piv-go v1.7.0 h1:rfjdFdASfGV5KLJhSjgpGJ5lzVZVtRWn8ovy/H9HQ/U= github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -287,8 +290,9 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -369,6 +373,7 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -511,11 +516,14 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w= +github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -585,8 +593,9 @@ github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXma github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f h1:eVB9ELsoq5ouItQBr5Tj334bhPJG/MX+m7rTchmzVUQ= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -624,6 +633,7 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f/go.mod h1:nwPd6pDNId/Xi16qtKrFHrauSwMNuvk+zcjk89wrnlA= github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU= github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= @@ -782,8 +792,9 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= @@ -804,8 +815,6 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/ go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk= go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= -go.step.sm/linkedca v0.15.0 h1:lEkGRDY+u7FudGKt8yEo7nBy5OzceO9s3rl+/sZVL5M= -go.step.sm/linkedca v0.15.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1008,6 +1017,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1064,8 +1074,9 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1110,8 +1121,10 @@ golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWc golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -1296,6 +1309,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= @@ -1325,11 +1339,13 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From f0272dc717b3856469f3d3a5f05e9c3676d18d00 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 5 May 2022 11:10:21 +0200 Subject: [PATCH 72/78] Fix import replacement of linkedca --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index aa836f4f..c2dbad15 100644 --- a/go.mod +++ b/go.mod @@ -63,4 +63,4 @@ require ( // replace github.com/smallstep/nosql => ../nosql // replace go.step.sm/crypto => ../crypto // replace go.step.sm/cli-utils => ../cli-utils -replace go.step.sm/linkedca => ../linkedca +// replace go.step.sm/linkedca => ../linkedca diff --git a/go.sum b/go.sum index a0b45a39..24acc602 100644 --- a/go.sum +++ b/go.sum @@ -815,6 +815,8 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/ go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk= go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= +go.step.sm/linkedca v0.16.0 h1:9xdE150lRTEoBq1gJl+prePpSmNqXRXsez3qzRs3Lic= +go.step.sm/linkedca v0.16.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= From 5e9bce508de70471eb9475fb6768eb97782d8c72 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 5 May 2022 12:32:53 +0200 Subject: [PATCH 73/78] Unexport GetPolicy() --- authority/provisioner/acme.go | 4 ++-- authority/provisioner/aws.go | 4 ++-- authority/provisioner/azure.go | 4 ++-- authority/provisioner/controller.go | 2 +- authority/provisioner/gcp.go | 4 ++-- authority/provisioner/jwk.go | 4 ++-- authority/provisioner/k8sSA.go | 4 ++-- authority/provisioner/nebula.go | 4 ++-- authority/provisioner/oidc.go | 4 ++-- authority/provisioner/policy.go | 6 +++--- authority/provisioner/scep.go | 2 +- authority/provisioner/x5c.go | 4 ++-- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index c9fa02cc..9374d985 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -107,7 +107,7 @@ type ACMEIdentifier struct { // certificate for an ACME Order Identifier. func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIdentifier) error { - x509Policy := p.ctl.GetPolicy().GetX509() + x509Policy := p.ctl.getPolicy().getX509() // identifier is allowed if no policy is configured if x509Policy == nil { @@ -141,7 +141,7 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), } return opts, nil diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index ea69269f..8433fde5 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -478,7 +478,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, commonNameValidator(payload.Claims.Subject), newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), ), nil } @@ -758,6 +758,6 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), nil), + newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), ), nil } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 48366430..438ab5b3 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -362,7 +362,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // validators defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), ), nil } @@ -429,7 +429,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), nil), + newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), ), nil } diff --git a/authority/provisioner/controller.go b/authority/provisioner/controller.go index 83de4a83..0ca40267 100644 --- a/authority/provisioner/controller.go +++ b/authority/provisioner/controller.go @@ -199,7 +199,7 @@ func SanitizeSSHUserPrincipal(email string) string { }, strings.ToLower(email)) } -func (c *Controller) GetPolicy() *policyEngine { +func (c *Controller) getPolicy() *policyEngine { if c == nil { return nil } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 29c9637c..94c19e17 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -272,7 +272,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // validators defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), ), nil } @@ -436,6 +436,6 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), nil), + newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), ), nil } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 30b78f56..336736db 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -183,7 +183,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, defaultSANsValidator(claims.SANs), newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), }, nil } @@ -266,7 +266,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), p.ctl.GetPolicy().GetSSHUser()), + newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()), ), nil } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 9d88327b..e2dbf840 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -242,7 +242,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // validators defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), }, nil } @@ -286,7 +286,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Require and validate all the default fields in the SSH certificate. &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), p.ctl.GetPolicy().GetSSHUser()), + newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()), ), nil } diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index d5d76e83..38a2409f 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -163,7 +163,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption, }, defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), }, nil } @@ -260,7 +260,7 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), nil), + newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), ), nil } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index f1b67e77..9f389b29 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -355,7 +355,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators defaultPublicKeyValidator{}, newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(o.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(o.ctl.getPolicy().getX509()), }, nil } @@ -443,7 +443,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(o.ctl.GetPolicy().GetSSHHost(), o.ctl.GetPolicy().GetSSHUser()), + newSSHNamePolicyValidator(o.ctl.getPolicy().getSSHHost(), o.ctl.getPolicy().getSSHUser()), ), nil } diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go index 52a59c97..95ef4163 100644 --- a/authority/provisioner/policy.go +++ b/authority/provisioner/policy.go @@ -43,21 +43,21 @@ func newPolicyEngine(options *Options) (*policyEngine, error) { }, nil } -func (p *policyEngine) GetX509() policy.X509Policy { +func (p *policyEngine) getX509() policy.X509Policy { if p == nil { return nil } return p.x509Policy } -func (p *policyEngine) GetSSHHost() policy.HostPolicy { +func (p *policyEngine) getSSHHost() policy.HostPolicy { if p == nil { return nil } return p.sshHostPolicy } -func (p *policyEngine) GetSSHUser() policy.UserPolicy { +func (p *policyEngine) getSSHUser() policy.UserPolicy { if p == nil { return nil } diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index 6d7bb699..c49c993e 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -127,7 +127,7 @@ func (s *SCEP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // validators newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength), newValidityValidator(s.ctl.Claimer.MinTLSCertDuration(), s.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(s.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(s.ctl.getPolicy().getX509()), }, nil } diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index f040d802..69576da5 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -235,7 +235,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultSANsValidator(claims.SANs), defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), - newX509NamePolicyValidator(p.ctl.GetPolicy().GetX509()), + newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), }, nil } @@ -321,6 +321,6 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Require all the fields in the SSH certificate &sshCertDefaultValidator{}, // Ensure that all principal names are allowed - newSSHNamePolicyValidator(p.ctl.GetPolicy().GetSSHHost(), p.ctl.GetPolicy().GetSSHUser()), + newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()), ), nil } From 105211392c6d54592087f2324362e2d72cc4ceca Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 5 May 2022 14:10:52 +0200 Subject: [PATCH 74/78] Don't rely on linkedca model stability in API response bodies --- authority/admin/api/policy_test.go | 754 +++++++++++++++++++++-------- 1 file changed, 549 insertions(+), 205 deletions(-) diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index b5987104..77879190 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -11,11 +11,11 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "google.golang.org/protobuf/encoding/protojson" "go.step.sm/linkedca" - "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/admin" @@ -29,13 +29,67 @@ func (f *fakeLinkedCA) IsLinkedCA() bool { return true } +// testAdminError is an error type that models the expected +// error body returned. +type testAdminError struct { + Type string `json:"type"` + Message string `json:"message"` + Detail string `json:"detail"` +} + +type testX509Policy struct { + Allow *testX509Names `json:"allow,omitempty"` + Deny *testX509Names `json:"deny,omitempty"` + AllowWildcardNames bool `json:"allow_wildcard_names,omitempty"` +} + +type testX509Names struct { + CommonNames []string `json:"commonNames,omitempty"` + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ips,omitempty"` + EmailAddresses []string `json:"emails,omitempty"` + URIDomains []string `json:"uris,omitempty"` +} + +type testSSHPolicy struct { + User *testSSHUserPolicy `json:"user,omitempty"` + Host *testSSHHostPolicy `json:"host,omitempty"` +} + +type testSSHHostPolicy struct { + Allow *testSSHHostNames `json:"allow,omitempty"` + Deny *testSSHHostNames `json:"deny,omitempty"` +} + +type testSSHHostNames struct { + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ips,omitempty"` + Principals []string `json:"principals,omitempty"` +} + +type testSSHUserPolicy struct { + Allow *testSSHUserNames `json:"allow,omitempty"` + Deny *testSSHUserNames `json:"deny,omitempty"` +} + +type testSSHUserNames struct { + EmailAddresses []string `json:"emails,omitempty"` + Principals []string `json:"principals,omitempty"` +} + +// testPolicyResponse models the Policy API JSON response +type testPolicyResponse struct { + X509 *testX509Policy `json:"x509,omitempty"` + SSH *testSSHPolicy `json:"ssh,omitempty"` +} + func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { type test struct { auth adminAuthority adminDB admin.DB ctx context.Context err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -85,7 +139,42 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, + Dns: []string{"*.local"}, + Ips: []string{"10.0.0.0/16"}, + Emails: []string{"@example.com"}, + Uris: []string{"example.com"}, + CommonNames: []string{"test"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"bad.local"}, + Ips: []string{"10.0.0.30"}, + Emails: []string{"bad@example.com"}, + Uris: []string{"notexample.com"}, + CommonNames: []string{"bad"}, + }, + }, + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + Principals: []string{"*"}, + }, + Deny: &linkedca.SSHUserNames{ + Emails: []string{"bad@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.example.com"}, + Ips: []string{"10.10.0.0/16"}, + Principals: []string{"good"}, + }, + Deny: &linkedca.SSHHostNames{ + Dns: []string{"bad@example.com"}, + Ips: []string{"10.10.0.30"}, + Principals: []string{"bad"}, + }, }, }, } @@ -96,7 +185,48 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { return policy, nil }, }, - policy: policy, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"10.0.0.0/16"}, + EmailAddresses: []string{"@example.com"}, + URIDomains: []string{"example.com"}, + CommonNames: []string{"test"}, + }, + Deny: &testX509Names{ + DNSDomains: []string{"bad.local"}, + IPRanges: []string{"10.0.0.30"}, + EmailAddresses: []string{"bad@example.com"}, + URIDomains: []string{"notexample.com"}, + CommonNames: []string{"bad"}, + }, + }, + SSH: &testSSHPolicy{ + User: &testSSHUserPolicy{ + Allow: &testSSHUserNames{ + EmailAddresses: []string{"@example.com"}, + Principals: []string{"*"}, + }, + Deny: &testSSHUserNames{ + EmailAddresses: []string{"bad@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &testSSHHostPolicy{ + Allow: &testSSHHostNames{ + DNSDomains: []string{"*.example.com"}, + IPRanges: []string{"10.10.0.0/16"}, + Principals: []string{"good"}, + }, + Deny: &testSSHHostNames{ + DNSDomains: []string{"bad@example.com"}, + IPRanges: []string{"10.10.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, statusCode: 200, } }, @@ -114,29 +244,31 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { par.GetAuthorityPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + assert.Equal(t, tc.response, p) }) } } @@ -149,7 +281,7 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { ctx context.Context acmeDB acme.DB err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -227,7 +359,7 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -272,7 +404,7 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -315,7 +447,7 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -336,8 +468,14 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { }, nil }, }, - body: body, - policy: policy, + body: body, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + }, + }, + }, statusCode: 201, } }, @@ -355,21 +493,21 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { par.CreateAuthorityPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) // when the error message starts with "proto", we expect it to have // a syntax error (in the tests). If the message doesn't start with "proto", @@ -377,15 +515,18 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { if strings.HasPrefix(tc.err.Message, "proto:") { assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { - assert.Equals(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.Message, ae.Message) } return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + + assert.Equal(t, tc.response, p) }) } @@ -399,7 +540,7 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { ctx context.Context acmeDB acme.DB err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -485,7 +626,7 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -530,7 +671,7 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -573,7 +714,7 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -594,8 +735,14 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { }, nil }, }, - body: body, - policy: policy, + body: body, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + }, + }, + }, statusCode: 200, } }, @@ -613,21 +760,21 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { par.UpdateAuthorityPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) // when the error message starts with "proto", we expect it to have // a syntax error (in the tests). If the message doesn't start with "proto", @@ -635,15 +782,18 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { if strings.HasPrefix(tc.err.Message, "proto:") { assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { - assert.Equals(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.Message, ae.Message) } return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + + assert.Equal(t, tc.response, p) }) } @@ -764,32 +914,32 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { par.DeleteAuthorityPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) return } body, err := io.ReadAll(res.Body) - assert.FatalError(t, err) + assert.NoError(t, err) res.Body.Close() response := DeleteResponse{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) - assert.Equals(t, "ok", response.Status) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equal(t, "ok", response.Status) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) }) } @@ -802,7 +952,7 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { ctx context.Context acmeDB acme.DB err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -832,7 +982,42 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, + Dns: []string{"*.local"}, + Ips: []string{"10.0.0.0/16"}, + Emails: []string{"@example.com"}, + Uris: []string{"example.com"}, + CommonNames: []string{"test"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"bad.local"}, + Ips: []string{"10.0.0.30"}, + Emails: []string{"bad@example.com"}, + Uris: []string{"notexample.com"}, + CommonNames: []string{"bad"}, + }, + }, + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + Principals: []string{"*"}, + }, + Deny: &linkedca.SSHUserNames{ + Emails: []string{"bad@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.example.com"}, + Ips: []string{"10.10.0.0/16"}, + Principals: []string{"good"}, + }, + Deny: &linkedca.SSHHostNames{ + Dns: []string{"bad@example.com"}, + Ips: []string{"10.10.0.30"}, + Principals: []string{"bad"}, + }, }, }, } @@ -841,8 +1026,49 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { } ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) return test{ - ctx: ctx, - policy: policy, + ctx: ctx, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"10.0.0.0/16"}, + EmailAddresses: []string{"@example.com"}, + URIDomains: []string{"example.com"}, + CommonNames: []string{"test"}, + }, + Deny: &testX509Names{ + DNSDomains: []string{"bad.local"}, + IPRanges: []string{"10.0.0.30"}, + EmailAddresses: []string{"bad@example.com"}, + URIDomains: []string{"notexample.com"}, + CommonNames: []string{"bad"}, + }, + }, + SSH: &testSSHPolicy{ + User: &testSSHUserPolicy{ + Allow: &testSSHUserNames{ + EmailAddresses: []string{"@example.com"}, + Principals: []string{"*"}, + }, + Deny: &testSSHUserNames{ + EmailAddresses: []string{"bad@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &testSSHHostPolicy{ + Allow: &testSSHHostNames{ + DNSDomains: []string{"*.example.com"}, + IPRanges: []string{"10.10.0.0/16"}, + Principals: []string{"good"}, + }, + Deny: &testSSHHostNames{ + DNSDomains: []string{"bad@example.com"}, + IPRanges: []string{"10.10.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, statusCode: 200, } }, @@ -860,28 +1086,31 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { par.GetProvisionerPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + + assert.Equal(t, tc.response, p) }) } @@ -894,7 +1123,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { body []byte ctx context.Context err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -964,7 +1193,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -999,7 +1228,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -1032,7 +1261,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -1040,8 +1269,14 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { return nil }, }, - body: body, - policy: policy, + body: body, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + }, + }, + }, statusCode: 201, } }, @@ -1059,21 +1294,21 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { par.CreateProvisionerPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) // when the error message starts with "proto", we expect it to have // a syntax error (in the tests). If the message doesn't start with "proto", @@ -1081,15 +1316,18 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { if strings.HasPrefix(tc.err.Message, "proto:") { assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { - assert.Equals(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.Message, ae.Message) } return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + + assert.Equal(t, tc.response, p) }) } @@ -1102,7 +1340,7 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { adminDB admin.DB ctx context.Context err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -1173,7 +1411,7 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { adminErr := admin.NewError(admin.ErrorBadRequestType, "error updating provisioner policy") adminErr.Message = "error updating provisioner policy: admin lock out" body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -1209,7 +1447,7 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating provisioner policy: force") adminErr.Message = "error updating provisioner policy: force" body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -1243,7 +1481,7 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { ctx := linkedca.NewContextWithAdmin(context.Background(), adm) ctx = linkedca.NewContextWithProvisioner(ctx, prov) body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, auth: &mockAdminAuthority{ @@ -1251,8 +1489,14 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { return nil }, }, - body: body, - policy: policy, + body: body, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + }, + }, + }, statusCode: 200, } }, @@ -1270,21 +1514,21 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { par.UpdateProvisionerPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) // when the error message starts with "proto", we expect it to have // a syntax error (in the tests). If the message doesn't start with "proto", @@ -1292,15 +1536,18 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { if strings.HasPrefix(tc.err.Message, "proto:") { assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { - assert.Equals(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.Message, ae.Message) } return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + + assert.Equal(t, tc.response, p) }) } @@ -1391,32 +1638,32 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { par.DeleteProvisionerPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) return } body, err := io.ReadAll(res.Body) - assert.FatalError(t, err) + assert.NoError(t, err) res.Body.Close() response := DeleteResponse{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) - assert.Equals(t, "ok", response.Status) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equal(t, "ok", response.Status) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) }) } @@ -1428,7 +1675,7 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { acmeDB acme.DB adminDB admin.DB err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -1464,7 +1711,42 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, + Dns: []string{"*.local"}, + Ips: []string{"10.0.0.0/16"}, + Emails: []string{"@example.com"}, + Uris: []string{"example.com"}, + CommonNames: []string{"test"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"bad.local"}, + Ips: []string{"10.0.0.30"}, + Emails: []string{"bad@example.com"}, + Uris: []string{"notexample.com"}, + CommonNames: []string{"bad"}, + }, + }, + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + Principals: []string{"*"}, + }, + Deny: &linkedca.SSHUserNames{ + Emails: []string{"bad@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.example.com"}, + Ips: []string{"10.10.0.0/16"}, + Principals: []string{"good"}, + }, + Deny: &linkedca.SSHHostNames{ + Dns: []string{"bad@example.com"}, + Ips: []string{"10.10.0.30"}, + Principals: []string{"bad"}, + }, }, }, } @@ -1478,8 +1760,49 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) return test{ - ctx: ctx, - policy: policy, + ctx: ctx, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + IPRanges: []string{"10.0.0.0/16"}, + EmailAddresses: []string{"@example.com"}, + URIDomains: []string{"example.com"}, + CommonNames: []string{"test"}, + }, + Deny: &testX509Names{ + DNSDomains: []string{"bad.local"}, + IPRanges: []string{"10.0.0.30"}, + EmailAddresses: []string{"bad@example.com"}, + URIDomains: []string{"notexample.com"}, + CommonNames: []string{"bad"}, + }, + }, + SSH: &testSSHPolicy{ + User: &testSSHUserPolicy{ + Allow: &testSSHUserNames{ + EmailAddresses: []string{"@example.com"}, + Principals: []string{"*"}, + }, + Deny: &testSSHUserNames{ + EmailAddresses: []string{"bad@example.com"}, + Principals: []string{"root"}, + }, + }, + Host: &testSSHHostPolicy{ + Allow: &testSSHHostNames{ + DNSDomains: []string{"*.example.com"}, + IPRanges: []string{"10.10.0.0/16"}, + Principals: []string{"good"}, + }, + Deny: &testSSHHostNames{ + DNSDomains: []string{"bad@example.com"}, + IPRanges: []string{"10.10.0.30"}, + Principals: []string{"bad"}, + }, + }, + }, + }, statusCode: 200, } }, @@ -1497,28 +1820,31 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { par.GetACMEAccountPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + + assert.Equal(t, tc.response, p) }) } @@ -1531,7 +1857,7 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { body []byte ctx context.Context err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -1610,13 +1936,13 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { - assert.Equals(t, "provID", provisionerID) - assert.Equals(t, "eakID", eak.ID) + assert.Equal(t, "provID", provisionerID) + assert.Equal(t, "eakID", eak.ID) return errors.New("force") }, }, @@ -1643,18 +1969,24 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { }, } body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { - assert.Equals(t, "provID", provisionerID) - assert.Equals(t, "eakID", eak.ID) + assert.Equal(t, "provID", provisionerID) + assert.Equal(t, "eakID", eak.ID) return nil }, }, - body: body, - policy: policy, + body: body, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + }, + }, + }, statusCode: 201, } }, @@ -1672,21 +2004,21 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { par.CreateACMEAccountPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) // when the error message starts with "proto", we expect it to have // a syntax error (in the tests). If the message doesn't start with "proto", @@ -1694,15 +2026,18 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { if strings.HasPrefix(tc.err.Message, "proto:") { assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { - assert.Equals(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.Message, ae.Message) } return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + + assert.Equal(t, tc.response, p) }) } @@ -1715,7 +2050,7 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { body []byte ctx context.Context err *admin.Error - policy *linkedca.Policy + response *testPolicyResponse statusCode int } var tests = map[string]func(t *testing.T) test{ @@ -1795,13 +2130,13 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating ACME EAK policy: force") adminErr.Message = "error updating ACME EAK policy: force" body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { - assert.Equals(t, "provID", provisionerID) - assert.Equals(t, "eakID", eak.ID) + assert.Equal(t, "provID", provisionerID) + assert.Equal(t, "eakID", eak.ID) return errors.New("force") }, }, @@ -1829,18 +2164,24 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) body, err := protojson.Marshal(policy) - assert.FatalError(t, err) + assert.NoError(t, err) return test{ ctx: ctx, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { - assert.Equals(t, "provID", provisionerID) - assert.Equals(t, "eakID", eak.ID) + assert.Equal(t, "provID", provisionerID) + assert.Equal(t, "eakID", eak.ID) return nil }, }, - body: body, - policy: policy, + body: body, + response: &testPolicyResponse{ + X509: &testX509Policy{ + Allow: &testX509Names{ + DNSDomains: []string{"*.local"}, + }, + }, + }, statusCode: 200, } }, @@ -1858,21 +2199,21 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { par.UpdateACMEAccountPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) // when the error message starts with "proto", we expect it to have // a syntax error (in the tests). If the message doesn't start with "proto", @@ -1880,15 +2221,18 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { if strings.HasPrefix(tc.err.Message, "proto:") { assert.True(t, strings.Contains(ae.Message, "syntax error")) } else { - assert.Equals(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.Message, ae.Message) } return } - p := &linkedca.Policy{} - assert.FatalError(t, readProtoJSON(res.Body, p)) - assert.Equals(t, tc.policy, p) + p := &testPolicyResponse{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(body, &p)) + + assert.Equal(t, tc.response, p) }) } @@ -1957,8 +2301,8 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { ctx: ctx, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { - assert.Equals(t, "provID", provisionerID) - assert.Equals(t, "eakID", eak.ID) + assert.Equal(t, "provID", provisionerID) + assert.Equal(t, "eakID", eak.ID) return errors.New("force") }, }, @@ -1988,8 +2332,8 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { ctx: ctx, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { - assert.Equals(t, "provID", provisionerID) - assert.Equals(t, "eakID", eak.ID) + assert.Equal(t, "provID", provisionerID) + assert.Equal(t, "eakID", eak.ID) return nil }, }, @@ -2010,32 +2354,32 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { par.DeleteACMEAccountPolicy(w, req) res := w.Result() - assert.Equals(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) res.Body.Close() - assert.FatalError(t, err) + assert.NoError(t, err) - ae := admin.Error{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) - assert.Equals(t, tc.err.Type, ae.Type) - assert.Equals(t, tc.err.Message, ae.Message) - assert.Equals(t, tc.err.StatusCode(), res.StatusCode) - assert.Equals(t, tc.err.Detail, ae.Detail) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.Message, ae.Message) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) return } body, err := io.ReadAll(res.Body) - assert.FatalError(t, err) + assert.NoError(t, err) res.Body.Close() response := DeleteResponse{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) - assert.Equals(t, "ok", response.Status) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equal(t, "ok", response.Status) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) }) } From ed231d29e211619d7f818eb58998c1ca62379025 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 5 May 2022 15:57:47 +0200 Subject: [PATCH 75/78] Update to go.step.sm/linkedca@v0.16.1 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c2dbad15..36e39b4d 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.0 go.step.sm/crypto v0.16.1 - go.step.sm/linkedca v0.16.0 + go.step.sm/linkedca v0.16.1 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20220403103023-749bd193bc2b golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect diff --git a/go.sum b/go.sum index 24acc602..55528b4f 100644 --- a/go.sum +++ b/go.sum @@ -817,6 +817,8 @@ go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk= go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= go.step.sm/linkedca v0.16.0 h1:9xdE150lRTEoBq1gJl+prePpSmNqXRXsez3qzRs3Lic= go.step.sm/linkedca v0.16.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= +go.step.sm/linkedca v0.16.1 h1:CdbMV5SjnlRsgeYTXaaZmQCkYIgJq8BOzpewri57M2k= +go.step.sm/linkedca v0.16.1/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= From 7104299119974b2105bb1c26a984f22feba1ae75 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 6 May 2022 13:12:13 +0200 Subject: [PATCH 76/78] Add full policy validation in API --- authority/admin/api/policy.go | 59 ++++++ authority/admin/api/policy_test.go | 283 +++++++++++++++++++++++++++++ authority/policy.go | 117 +----------- authority/policy/policy.go | 119 ++++++++++++ authority/policy/policy_test.go | 155 ++++++++++++++++ authority/policy_test.go | 136 -------------- 6 files changed, 618 insertions(+), 251 deletions(-) create mode 100644 authority/policy/policy_test.go diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 275b947c..970b8785 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -11,6 +11,7 @@ import ( "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/policy" ) type policyAdminResponderInterface interface { @@ -104,6 +105,11 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r newPolicy.Deduplicate() + if err := validatePolicy(newPolicy); err != nil { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy")) + return + } + adm := linkedca.MustAdminFromContext(ctx) var createdPolicy *linkedca.Policy @@ -149,6 +155,11 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r newPolicy.Deduplicate() + if err := validatePolicy(newPolicy); err != nil { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy")) + return + } + adm := linkedca.MustAdminFromContext(ctx) var updatedPolicy *linkedca.Policy @@ -239,6 +250,11 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, newPolicy.Deduplicate() + if err := validatePolicy(newPolicy); err != nil { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy")) + return + } + prov.Policy = newPolicy if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { @@ -278,6 +294,11 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, newPolicy.Deduplicate() + if err := validatePolicy(newPolicy); err != nil { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy")) + return + } + prov.Policy = newPolicy if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { if isBadRequest(err) { @@ -364,6 +385,11 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, newPolicy.Deduplicate() + if err := validatePolicy(newPolicy); err != nil { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy")) + return + } + eak.Policy = newPolicy acmeEAK := linkedEAKToCertificates(eak) @@ -400,6 +426,11 @@ func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, newPolicy.Deduplicate() + if err := validatePolicy(newPolicy); err != nil { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy")) + return + } + eak.Policy = newPolicy acmeEAK := linkedEAKToCertificates(eak) if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { @@ -455,3 +486,31 @@ func isBadRequest(err error) bool { isPolicyError := errors.As(err, &pe) return isPolicyError && (pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure) } + +func validatePolicy(p *linkedca.Policy) error { + + // convert the policy; return early if nil + options := policy.PolicyToCertificates(p) + if options == nil { + return nil + } + + var err error + + // Initialize a temporary x509 allow/deny policy engine + if _, err = policy.NewX509PolicyEngine(options.GetX509Options()); err != nil { + return err + } + + // Initialize a temporary SSH allow/deny policy engine for host certificates + if _, err = policy.NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil { + return err + } + + // Initialize a temporary SSH allow/deny policy engine for user certificates + if _, err = policy.NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil { + return err + } + + return nil +} diff --git a/authority/admin/api/policy_test.go b/authority/admin/api/policy_test.go index 77879190..1e70db52 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -343,6 +343,32 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { statusCode: 400, } }, + "fail/validatePolicy": func(t *testing.T) test { + ctx := context.Background() + adminErr := admin.NewError(admin.ErrorBadRequestType, "error validating authority policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)") + adminErr.Message = "error validating authority policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)" + body := []byte(` + { + "x509": { + "allow": { + "uris": [ + "https://example.com" + ] + } + } + }`) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorNotFoundType, "not found") + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, "fail/CreateAuthorityPolicy-policy-admin-lockout-error": func(t *testing.T) test { adm := &linkedca.Admin{ Subject: "step", @@ -610,6 +636,39 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { statusCode: 400, } }, + "fail/validatePolicy": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + ctx := context.Background() + adminErr := admin.NewError(admin.ErrorBadRequestType, "error validating authority policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)") + adminErr.Message = "error validating authority policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)" + body := []byte(` + { + "x509": { + "allow": { + "uris": [ + "https://example.com" + ] + } + } + }`) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return policy, nil + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, "fail/UpdateAuthorityPolicy-policy-admin-lockout-error": func(t *testing.T) test { adm := &linkedca.Admin{ Subject: "step", @@ -1174,6 +1233,35 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { statusCode: 400, } }, + "fail/validatePolicy": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "error validating provisioner policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)") + adminErr.Message = "error validating provisioner policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)" + body := []byte(` + { + "x509": { + "allow": { + "uris": [ + "https://example.com" + ] + } + } + }`) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorNotFoundType, "not found") + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, "fail/auth.UpdateProvisioner-policy-admin-lockout-error": func(t *testing.T) test { adm := &linkedca.Admin{ Subject: "step", @@ -1391,6 +1479,43 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { statusCode: 400, } }, + "fail/validatePolicy": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "error validating provisioner policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)") + adminErr.Message = "error validating provisioner policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)" + body := []byte(` + { + "x509": { + "allow": { + "uris": [ + "https://example.com" + ] + } + } + }`) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { + return nil, admin.NewError(admin.ErrorNotFoundType, "not found") + }, + }, + body: body, + err: adminErr, + statusCode: 400, + } + }, "fail/auth.UpdateProvisioner-policy-admin-lockout-error": func(t *testing.T) test { adm := &linkedca.Admin{ Subject: "step", @@ -1916,6 +2041,34 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { statusCode: 400, } }, + "fail/validatePolicy": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + adminErr := admin.NewError(admin.ErrorBadRequestType, "error validating ACME EAK policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)") + adminErr.Message = "error validating ACME EAK policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)" + body := []byte(` + { + "x509": { + "allow": { + "uris": [ + "https://example.com" + ] + } + } + }`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, "fail/acmeDB.UpdateExternalAccountKey-error": func(t *testing.T) test { prov := &linkedca.Provisioner{ Id: "provID", @@ -2109,6 +2262,42 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { statusCode: 400, } }, + "fail/validatePolicy": func(t *testing.T) test { + policy := &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + } + prov := &linkedca.Provisioner{ + Name: "provName", + } + eak := &linkedca.EABKey{ + Id: "eakID", + Policy: policy, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) + adminErr := admin.NewError(admin.ErrorBadRequestType, "error validating ACME EAK policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)") + adminErr.Message = "error validating ACME EAK policy: cannot parse permitted URI domain constraint \"https://example.com\": URI domain constraint \"https://example.com\" contains scheme (not supported yet)" + body := []byte(` + { + "x509": { + "allow": { + "uris": [ + "https://example.com" + ] + } + } + }`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, "fail/acmeDB.UpdateExternalAccountKey-error": func(t *testing.T) test { policy := &linkedca.Policy{ X509: &linkedca.X509Policy{ @@ -2426,3 +2615,97 @@ func Test_isBadRequest(t *testing.T) { }) } } + +func Test_validatePolicy(t *testing.T) { + type args struct { + p *linkedca.Policy + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "nil", + args: args{ + p: nil, + }, + wantErr: false, + }, + { + name: "x509", + args: args{ + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"**.local"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ssh user", + args: args{ + p: &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@@example.com"}, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ssh host", + args: args{ + p: &linkedca.Policy{ + Ssh: &linkedca.SSHPolicy{ + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"**.local"}, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ok", + args: args{ + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + }, + Ssh: &linkedca.SSHPolicy{ + User: &linkedca.SSHUserPolicy{ + Allow: &linkedca.SSHUserNames{ + Emails: []string{"@example.com"}, + }, + }, + Host: &linkedca.SSHHostPolicy{ + Allow: &linkedca.SSHHostNames{ + Dns: []string{"*.local"}, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validatePolicy(tt.args.p); (err != nil) != tt.wantErr { + t.Errorf("validatePolicy() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/authority/policy.go b/authority/policy.go index 063a464c..4afe2535 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -155,7 +155,7 @@ func (a *Authority) checkProvisionerPolicy(ctx context.Context, currentAdmin *li 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) + policyOptions := authPolicy.PolicyToCertificates(p) if policyOptions == nil { return nil } @@ -222,7 +222,7 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { return fmt.Errorf("error getting policy to (re)load policy engines: %w", err) } } - policyOptions = policyToCertificates(linkedPolicy) + policyOptions = authPolicy.PolicyToCertificates(linkedPolicy) } else { policyOptions = a.config.AuthorityConfig.Policy } @@ -256,116 +256,3 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error { return nil } - -func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { - - // return early - if p == nil { - return nil - } - - // return early if x509 nor SSH is set - if p.GetX509() == nil && p.GetSsh() == nil { - return nil - } - - opts := &authPolicy.Options{} - - // fill x509 policy configuration - if x509 := p.GetX509(); x509 != nil { - opts.X509 = &authPolicy.X509PolicyOptions{} - if allow := x509.GetAllow(); allow != nil { - opts.X509.AllowedNames = &authPolicy.X509NameOptions{} - if allow.Dns != nil { - opts.X509.AllowedNames.DNSDomains = allow.Dns - } - if allow.Ips != nil { - opts.X509.AllowedNames.IPRanges = allow.Ips - } - if allow.Emails != nil { - opts.X509.AllowedNames.EmailAddresses = allow.Emails - } - if allow.Uris != nil { - opts.X509.AllowedNames.URIDomains = allow.Uris - } - if allow.CommonNames != nil { - opts.X509.AllowedNames.CommonNames = allow.CommonNames - } - } - if deny := x509.GetDeny(); deny != nil { - opts.X509.DeniedNames = &authPolicy.X509NameOptions{} - if deny.Dns != nil { - opts.X509.DeniedNames.DNSDomains = deny.Dns - } - if deny.Ips != nil { - opts.X509.DeniedNames.IPRanges = deny.Ips - } - if deny.Emails != nil { - opts.X509.DeniedNames.EmailAddresses = deny.Emails - } - if deny.Uris != nil { - opts.X509.DeniedNames.URIDomains = deny.Uris - } - if deny.CommonNames != nil { - opts.X509.DeniedNames.CommonNames = deny.CommonNames - } - } - - opts.X509.AllowWildcardNames = x509.GetAllowWildcardNames() - } - - // fill ssh policy configuration - if ssh := p.GetSsh(); ssh != nil { - opts.SSH = &authPolicy.SSHPolicyOptions{} - if host := ssh.GetHost(); host != nil { - opts.SSH.Host = &authPolicy.SSHHostCertificateOptions{} - if allow := host.GetAllow(); allow != nil { - opts.SSH.Host.AllowedNames = &authPolicy.SSHNameOptions{} - if allow.Dns != nil { - opts.SSH.Host.AllowedNames.DNSDomains = allow.Dns - } - if allow.Ips != nil { - opts.SSH.Host.AllowedNames.IPRanges = allow.Ips - } - if allow.Principals != nil { - opts.SSH.Host.AllowedNames.Principals = allow.Principals - } - } - if deny := host.GetDeny(); deny != nil { - opts.SSH.Host.DeniedNames = &authPolicy.SSHNameOptions{} - if deny.Dns != nil { - opts.SSH.Host.DeniedNames.DNSDomains = deny.Dns - } - if deny.Ips != nil { - opts.SSH.Host.DeniedNames.IPRanges = deny.Ips - } - if deny.Principals != nil { - opts.SSH.Host.DeniedNames.Principals = deny.Principals - } - } - } - if user := ssh.GetUser(); user != nil { - opts.SSH.User = &authPolicy.SSHUserCertificateOptions{} - if allow := user.GetAllow(); allow != nil { - opts.SSH.User.AllowedNames = &authPolicy.SSHNameOptions{} - if allow.Emails != nil { - opts.SSH.User.AllowedNames.EmailAddresses = allow.Emails - } - if allow.Principals != nil { - opts.SSH.User.AllowedNames.Principals = allow.Principals - } - } - if deny := user.GetDeny(); deny != nil { - opts.SSH.User.DeniedNames = &authPolicy.SSHNameOptions{} - if deny.Emails != nil { - opts.SSH.User.DeniedNames.EmailAddresses = deny.Emails - } - if deny.Principals != nil { - opts.SSH.User.DeniedNames.Principals = deny.Principals - } - } - } - } - - return opts -} diff --git a/authority/policy/policy.go b/authority/policy/policy.go index 52297d65..51ad0da4 100644 --- a/authority/policy/policy.go +++ b/authority/policy/policy.go @@ -3,6 +3,8 @@ package policy import ( "fmt" + "go.step.sm/linkedca" + "github.com/smallstep/certificates/policy" ) @@ -52,10 +54,14 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, return nil, nil } + // check if configuration specifies that wildcard names are allowed if policyOptions.AreWildcardNamesAllowed() { options = append(options, policy.WithAllowLiteralWildcardNames()) } + // enable subject common name verification by default + options = append(options, policy.WithSubjectCommonNameVerification()) + return policy.New(options...) } @@ -135,3 +141,116 @@ func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEn return policy.New(options...) } + +func PolicyToCertificates(p *linkedca.Policy) *Options { + + // return early + if p == nil { + return nil + } + + // return early if x509 nor SSH is set + if p.GetX509() == nil && p.GetSsh() == nil { + return nil + } + + opts := &Options{} + + // fill x509 policy configuration + if x509 := p.GetX509(); x509 != nil { + opts.X509 = &X509PolicyOptions{} + if allow := x509.GetAllow(); allow != nil { + opts.X509.AllowedNames = &X509NameOptions{} + if allow.Dns != nil { + opts.X509.AllowedNames.DNSDomains = allow.Dns + } + if allow.Ips != nil { + opts.X509.AllowedNames.IPRanges = allow.Ips + } + if allow.Emails != nil { + opts.X509.AllowedNames.EmailAddresses = allow.Emails + } + if allow.Uris != nil { + opts.X509.AllowedNames.URIDomains = allow.Uris + } + if allow.CommonNames != nil { + opts.X509.AllowedNames.CommonNames = allow.CommonNames + } + } + if deny := x509.GetDeny(); deny != nil { + opts.X509.DeniedNames = &X509NameOptions{} + if deny.Dns != nil { + opts.X509.DeniedNames.DNSDomains = deny.Dns + } + if deny.Ips != nil { + opts.X509.DeniedNames.IPRanges = deny.Ips + } + if deny.Emails != nil { + opts.X509.DeniedNames.EmailAddresses = deny.Emails + } + if deny.Uris != nil { + opts.X509.DeniedNames.URIDomains = deny.Uris + } + if deny.CommonNames != nil { + opts.X509.DeniedNames.CommonNames = deny.CommonNames + } + } + + opts.X509.AllowWildcardNames = x509.GetAllowWildcardNames() + } + + // fill ssh policy configuration + if ssh := p.GetSsh(); ssh != nil { + opts.SSH = &SSHPolicyOptions{} + if host := ssh.GetHost(); host != nil { + opts.SSH.Host = &SSHHostCertificateOptions{} + if allow := host.GetAllow(); allow != nil { + opts.SSH.Host.AllowedNames = &SSHNameOptions{} + if allow.Dns != nil { + opts.SSH.Host.AllowedNames.DNSDomains = allow.Dns + } + if allow.Ips != nil { + opts.SSH.Host.AllowedNames.IPRanges = allow.Ips + } + if allow.Principals != nil { + opts.SSH.Host.AllowedNames.Principals = allow.Principals + } + } + if deny := host.GetDeny(); deny != nil { + opts.SSH.Host.DeniedNames = &SSHNameOptions{} + if deny.Dns != nil { + opts.SSH.Host.DeniedNames.DNSDomains = deny.Dns + } + if deny.Ips != nil { + opts.SSH.Host.DeniedNames.IPRanges = deny.Ips + } + if deny.Principals != nil { + opts.SSH.Host.DeniedNames.Principals = deny.Principals + } + } + } + if user := ssh.GetUser(); user != nil { + opts.SSH.User = &SSHUserCertificateOptions{} + if allow := user.GetAllow(); allow != nil { + opts.SSH.User.AllowedNames = &SSHNameOptions{} + if allow.Emails != nil { + opts.SSH.User.AllowedNames.EmailAddresses = allow.Emails + } + if allow.Principals != nil { + opts.SSH.User.AllowedNames.Principals = allow.Principals + } + } + if deny := user.GetDeny(); deny != nil { + opts.SSH.User.DeniedNames = &SSHNameOptions{} + if deny.Emails != nil { + opts.SSH.User.DeniedNames.EmailAddresses = deny.Emails + } + if deny.Principals != nil { + opts.SSH.User.DeniedNames.Principals = deny.Principals + } + } + } + } + + return opts +} diff --git a/authority/policy/policy_test.go b/authority/policy/policy_test.go new file mode 100644 index 00000000..a241d596 --- /dev/null +++ b/authority/policy/policy_test.go @@ -0,0 +1,155 @@ +package policy + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "go.step.sm/linkedca" +) + +func TestPolicyToCertificates(t *testing.T) { + type args struct { + policy *linkedca.Policy + } + tests := []struct { + name string + args args + want *Options + }{ + { + name: "nil", + args: args{ + policy: nil, + }, + want: nil, + }, + { + name: "no-policy", + args: args{ + &linkedca.Policy{}, + }, + want: nil, + }, + { + name: "partial-policy", + args: args{ + &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"*.local"}, + }, + AllowWildcardNames: false, + }, + }, + }, + want: &Options{ + X509: &X509PolicyOptions{ + AllowedNames: &X509NameOptions{ + DNSDomains: []string{"*.local"}, + }, + AllowWildcardNames: false, + }, + }, + }, + { + name: "full-policy", + args: args{ + &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"}, + CommonNames: []string{"some name"}, + }, + Deny: &linkedca.X509Names{ + Dns: []string{"bad"}, + Ips: []string{"127.0.0.30"}, + Emails: []string{"badhost.example.com"}, + Uris: []string{"https://badhost.local"}, + CommonNames: []string{"another name"}, + }, + AllowWildcardNames: true, + }, + 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: &Options{ + X509: &X509PolicyOptions{ + AllowedNames: &X509NameOptions{ + DNSDomains: []string{"step"}, + IPRanges: []string{"127.0.0.1/24"}, + EmailAddresses: []string{"*.example.com"}, + URIDomains: []string{"https://*.local"}, + CommonNames: []string{"some name"}, + }, + DeniedNames: &X509NameOptions{ + DNSDomains: []string{"bad"}, + IPRanges: []string{"127.0.0.30"}, + EmailAddresses: []string{"badhost.example.com"}, + URIDomains: []string{"https://badhost.local"}, + CommonNames: []string{"another name"}, + }, + AllowWildcardNames: true, + }, + SSH: &SSHPolicyOptions{ + Host: &SSHHostCertificateOptions{ + AllowedNames: &SSHNameOptions{ + DNSDomains: []string{"*.localhost"}, + IPRanges: []string{"127.0.0.1/24"}, + Principals: []string{"user"}, + }, + DeniedNames: &SSHNameOptions{ + DNSDomains: []string{"badhost.localhost"}, + IPRanges: []string{"127.0.0.40"}, + Principals: []string{"root"}, + }, + }, + User: &SSHUserCertificateOptions{ + AllowedNames: &SSHNameOptions{ + EmailAddresses: []string{"@work"}, + Principals: []string{"user"}, + }, + DeniedNames: &SSHNameOptions{ + EmailAddresses: []string{"root@work"}, + Principals: []string{"root"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := PolicyToCertificates(tt.args.policy) + if !cmp.Equal(tt.want, got) { + t.Errorf("policyToCertificates() diff=\n%s", cmp.Diff(tt.want, got)) + } + }) + } +} diff --git a/authority/policy_test.go b/authority/policy_test.go index e64752ec..efeb743b 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -6,7 +6,6 @@ import ( "reflect" "testing" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "go.step.sm/linkedca" @@ -195,141 +194,6 @@ func TestAuthority_checkPolicy(t *testing.T) { } } -func Test_policyToCertificates(t *testing.T) { - tests := []struct { - name string - policy *linkedca.Policy - want *policy.Options - }{ - { - name: "nil", - policy: nil, - want: nil, - }, - { - name: "no-policy", - policy: &linkedca.Policy{}, - want: nil, - }, - { - name: "partial-policy", - policy: &linkedca.Policy{ - X509: &linkedca.X509Policy{ - Allow: &linkedca.X509Names{ - Dns: []string{"*.local"}, - }, - AllowWildcardNames: false, - }, - }, - want: &policy.Options{ - X509: &policy.X509PolicyOptions{ - AllowedNames: &policy.X509NameOptions{ - DNSDomains: []string{"*.local"}, - }, - AllowWildcardNames: false, - }, - }, - }, - { - 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"}, - CommonNames: []string{"some name"}, - }, - Deny: &linkedca.X509Names{ - Dns: []string{"bad"}, - Ips: []string{"127.0.0.30"}, - Emails: []string{"badhost.example.com"}, - Uris: []string{"https://badhost.local"}, - CommonNames: []string{"another name"}, - }, - AllowWildcardNames: true, - }, - 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: &policy.Options{ - X509: &policy.X509PolicyOptions{ - AllowedNames: &policy.X509NameOptions{ - DNSDomains: []string{"step"}, - IPRanges: []string{"127.0.0.1/24"}, - EmailAddresses: []string{"*.example.com"}, - URIDomains: []string{"https://*.local"}, - CommonNames: []string{"some name"}, - }, - DeniedNames: &policy.X509NameOptions{ - DNSDomains: []string{"bad"}, - IPRanges: []string{"127.0.0.30"}, - EmailAddresses: []string{"badhost.example.com"}, - URIDomains: []string{"https://badhost.local"}, - CommonNames: []string{"another name"}, - }, - AllowWildcardNames: true, - }, - SSH: &policy.SSHPolicyOptions{ - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - DNSDomains: []string{"*.localhost"}, - IPRanges: []string{"127.0.0.1/24"}, - Principals: []string{"user"}, - }, - DeniedNames: &policy.SSHNameOptions{ - DNSDomains: []string{"badhost.localhost"}, - IPRanges: []string{"127.0.0.40"}, - Principals: []string{"root"}, - }, - }, - User: &policy.SSHUserCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - EmailAddresses: []string{"@work"}, - Principals: []string{"user"}, - }, - DeniedNames: &policy.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)) - } - }) - } -} - func mustPolicyEngine(t *testing.T, options *policy.Options) *policy.Engine { engine, err := policy.New(options) if err != nil { From 0f4ffa504a81276c7e60bb0959c913a715609ed8 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 6 May 2022 13:23:09 +0200 Subject: [PATCH 77/78] Fix linting issues --- authority/admin/api/policy.go | 51 +++++++++++++++++---------------- authority/policy.go | 4 +-- authority/policy/policy.go | 2 +- authority/policy/policy_test.go | 2 +- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 970b8785..6af1104a 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -61,18 +61,18 @@ func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht return } - policy, err := par.auth.GetAuthorityPolicy(r.Context()) + authorityPolicy, err := par.auth.GetAuthorityPolicy(r.Context()) 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 { + if authorityPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } - render.ProtoJSONStatus(w, policy, http.StatusOK) + render.ProtoJSONStatus(w, authorityPolicy, http.StatusOK) } // CreateAuthorityPolicy handles the POST /admin/authority/policy request @@ -84,14 +84,14 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r } ctx := r.Context() - policy, err := par.auth.GetAuthorityPolicy(ctx) + authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx) if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) { render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy")) return } - if policy != nil { + if authorityPolicy != nil { adminErr := admin.NewError(admin.ErrorConflictType, "authority already has a policy") render.Error(w, adminErr) return @@ -135,14 +135,14 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r } ctx := r.Context() - policy, err := par.auth.GetAuthorityPolicy(ctx) + authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx) if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) { render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy")) return } - if policy == nil { + if authorityPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } @@ -185,14 +185,14 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r } ctx := r.Context() - policy, err := par.auth.GetAuthorityPolicy(ctx) + authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx) 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 { + if authorityPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } @@ -215,13 +215,13 @@ func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r * prov := linkedca.MustProvisionerFromContext(r.Context()) - policy := prov.GetPolicy() - if policy == nil { + provisionerPolicy := prov.GetPolicy() + if provisionerPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } - render.ProtoJSONStatus(w, policy, http.StatusOK) + render.ProtoJSONStatus(w, provisionerPolicy, http.StatusOK) } // CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request @@ -235,8 +235,8 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, ctx := r.Context() prov := linkedca.MustProvisionerFromContext(ctx) - policy := prov.GetPolicy() - if policy != nil { + provisionerPolicy := prov.GetPolicy() + if provisionerPolicy != nil { adminErr := admin.NewError(admin.ErrorConflictType, "provisioner %s already has a policy", prov.Name) render.Error(w, adminErr) return @@ -281,7 +281,8 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, ctx := r.Context() prov := linkedca.MustProvisionerFromContext(ctx) - if prov.Policy == nil { + provisionerPolicy := prov.GetPolicy() + if provisionerPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } @@ -350,13 +351,13 @@ func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r * ctx := r.Context() eak := linkedca.MustExternalAccountKeyFromContext(ctx) - policy := eak.GetPolicy() - if policy == nil { + eakPolicy := eak.GetPolicy() + if eakPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) return } - render.ProtoJSONStatus(w, policy, http.StatusOK) + render.ProtoJSONStatus(w, eakPolicy, http.StatusOK) } func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { @@ -370,8 +371,8 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, prov := linkedca.MustProvisionerFromContext(ctx) eak := linkedca.MustExternalAccountKeyFromContext(ctx) - policy := eak.GetPolicy() - if policy != nil { + eakPolicy := eak.GetPolicy() + if eakPolicy != nil { adminErr := admin.NewError(admin.ErrorConflictType, "ACME EAK %s already has a policy", eak.Id) render.Error(w, adminErr) return @@ -412,8 +413,8 @@ func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, prov := linkedca.MustProvisionerFromContext(ctx) eak := linkedca.MustExternalAccountKeyFromContext(ctx) - policy := eak.GetPolicy() - if policy == nil { + eakPolicy := eak.GetPolicy() + if eakPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) return } @@ -452,8 +453,8 @@ func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, prov := linkedca.MustProvisionerFromContext(ctx) eak := linkedca.MustExternalAccountKeyFromContext(ctx) - policy := eak.GetPolicy() - if policy == nil { + eakPolicy := eak.GetPolicy() + if eakPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) return } @@ -490,7 +491,7 @@ func isBadRequest(err error) bool { func validatePolicy(p *linkedca.Policy) error { // convert the policy; return early if nil - options := policy.PolicyToCertificates(p) + options := policy.LinkedToCertificates(p) if options == nil { return nil } diff --git a/authority/policy.go b/authority/policy.go index 4afe2535..6348c690 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -155,7 +155,7 @@ func (a *Authority) checkProvisionerPolicy(ctx context.Context, currentAdmin *li 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 := authPolicy.PolicyToCertificates(p) + policyOptions := authPolicy.LinkedToCertificates(p) if policyOptions == nil { return nil } @@ -222,7 +222,7 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { return fmt.Errorf("error getting policy to (re)load policy engines: %w", err) } } - policyOptions = authPolicy.PolicyToCertificates(linkedPolicy) + policyOptions = authPolicy.LinkedToCertificates(linkedPolicy) } else { policyOptions = a.config.AuthorityConfig.Policy } diff --git a/authority/policy/policy.go b/authority/policy/policy.go index 51ad0da4..3c53b704 100644 --- a/authority/policy/policy.go +++ b/authority/policy/policy.go @@ -142,7 +142,7 @@ func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEn return policy.New(options...) } -func PolicyToCertificates(p *linkedca.Policy) *Options { +func LinkedToCertificates(p *linkedca.Policy) *Options { // return early if p == nil { diff --git a/authority/policy/policy_test.go b/authority/policy/policy_test.go index a241d596..9210ad90 100644 --- a/authority/policy/policy_test.go +++ b/authority/policy/policy_test.go @@ -146,7 +146,7 @@ func TestPolicyToCertificates(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := PolicyToCertificates(tt.args.policy) + got := LinkedToCertificates(tt.args.policy) if !cmp.Equal(tt.want, got) { t.Errorf("policyToCertificates() diff=\n%s", cmp.Diff(tt.want, got)) } From cc26a0b394b363b02f06f9c9e0e70de150504939 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 6 May 2022 13:58:48 +0200 Subject: [PATCH 78/78] Explicitly disable wildcard Common Name constraint --- policy/engine_test.go | 1 + policy/options.go | 31 ++++++++++++++++++++-- policy/options_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++ policy/validate.go | 4 +++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/policy/engine_test.go b/policy/engine_test.go index fabfebb9..1280d14d 100755 --- a/policy/engine_test.go +++ b/policy/engine_test.go @@ -2492,6 +2492,7 @@ func TestNamePolicyEngine_X509_AllAllowed(t *testing.T) { t.Run(tt.name, func(t *testing.T) { engine, err := New(tt.options...) assert.NoError(t, err) + assert.NotNil(t, engine) gotErr := engine.IsX509CertificateAllowed(tt.cert) wantErr := tt.wantErr != nil diff --git a/policy/options.go b/policy/options.go index 79507f43..f08f9180 100755 --- a/policy/options.go +++ b/policy/options.go @@ -28,14 +28,30 @@ func WithAllowLiteralWildcardNames() NamePolicyOption { func WithPermittedCommonNames(commonNames ...string) NamePolicyOption { return func(g *NamePolicyEngine) error { - g.permittedCommonNames = commonNames + normalizedCommonNames := make([]string, len(commonNames)) + for i, commonName := range commonNames { + normalizedCommonName, err := normalizeAndValidateCommonName(commonName) + if err != nil { + return fmt.Errorf("cannot parse permitted common name constraint %q: %w", commonName, err) + } + normalizedCommonNames[i] = normalizedCommonName + } + g.permittedCommonNames = normalizedCommonNames return nil } } func WithExcludedCommonNames(commonNames ...string) NamePolicyOption { return func(g *NamePolicyEngine) error { - g.excludedCommonNames = commonNames + normalizedCommonNames := make([]string, len(commonNames)) + for i, commonName := range commonNames { + normalizedCommonName, err := normalizeAndValidateCommonName(commonName) + if err != nil { + return fmt.Errorf("cannot parse excluded common name constraint %q: %w", commonName, err) + } + normalizedCommonNames[i] = normalizedCommonName + } + g.excludedCommonNames = normalizedCommonNames return nil } } @@ -242,6 +258,17 @@ func isIPv4(ip net.IP) bool { return ip.To4() != nil } +func normalizeAndValidateCommonName(constraint string) (string, error) { + normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) + if normalizedConstraint == "" { + return "", fmt.Errorf("contraint %q can not be empty or white space string", constraint) + } + if normalizedConstraint == "*" { + return "", fmt.Errorf("wildcard constraint %q is not supported", constraint) + } + return normalizedConstraint, nil +} + func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) { normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint)) if normalizedConstraint == "" { diff --git a/policy/options_test.go b/policy/options_test.go index d1a62a9f..697afecf 100644 --- a/policy/options_test.go +++ b/policy/options_test.go @@ -8,6 +8,46 @@ import ( "github.com/stretchr/testify/assert" ) +func Test_normalizeAndValidateCommonName(t *testing.T) { + tests := []struct { + name string + constraint string + want string + wantErr bool + }{ + { + name: "fail/empty-constraint", + constraint: "", + want: "", + wantErr: true, + }, + { + name: "fail/wildcard", + constraint: "*", + want: "", + wantErr: true, + }, + { + name: "ok", + constraint: "step", + want: "step", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeAndValidateCommonName(tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("normalizeAndValidateCommonName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("normalizeAndValidateCommonName() = %v, want %v", got, tt.want) + } + }) + } +} + func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) { tests := []struct { name string @@ -196,6 +236,24 @@ func TestNew(t *testing.T) { wantErr bool } var tests = map[string]func(t *testing.T) test{ + "fail/with-permitted-common-name": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithPermittedCommonNames("*"), + }, + want: nil, + wantErr: true, + } + }, + "fail/with-excluded-common-name": func(t *testing.T) test { + return test{ + options: []NamePolicyOption{ + WithExcludedCommonNames(""), + }, + want: nil, + wantErr: true, + } + }, "fail/with-permitted-dns-domains": func(t *testing.T) test { return test{ options: []NamePolicyOption{ diff --git a/policy/validate.go b/policy/validate.go index 968e936d..ee6f7e9c 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -639,5 +639,9 @@ func matchPrincipalConstraint(principal, constraint string) (bool, error) { // matchCommonNameConstraint performs a string literal equality check against constraint. func matchCommonNameConstraint(commonName, constraint string) (bool, error) { + // wildcard constraint is (currently) not supported for common names + if constraint == "*" { + return false, nil + } return strings.EqualFold(commonName, constraint), nil }