From 9539729bd9b47f6581d59a7399d5fae8a3cf92f5 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 3 Jan 2022 12:25:24 +0100 Subject: [PATCH 001/241] 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 002/241] 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 | 769 ++++++++++++++++++++++++++++++-- 8 files changed, 1062 insertions(+), 34 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{""}, + }, + cert: &x509.Certificate{ + DNSNames: []string{"example.local"}, + }, + want: true, + wantErr: false, + }, + { + name: "ok/dns-permitted", fields: fields{ permittedDNSDomains: []string{".local"}, }, - csr: &x509.CertificateRequest{ + cert: &x509.Certificate{ DNSNames: []string{"example.local"}, }, want: true, wantErr: false, }, { - name: "ok/ipv4", + 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/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/uri", + 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,297 @@ 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"}, + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.local", + }, + }, + }, + want: true, + wantErr: false, + }, + { + 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"}, @@ -268,12 +978,13 @@ func TestGuard_IsAllowed(t *testing.T) { want: true, wantErr: false, }, - // TODO: more complex uses cases that combine multiple names + // 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 68b980d6892ca9967d71b93e1e86f950367021dd Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Thu, 13 Jan 2022 20:30:54 +0100 Subject: [PATCH 003/241] feat(authority): avoid hardcoded cn in authority csr --- authority/config/config.go | 4 ++++ authority/tls.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/authority/config/config.go b/authority/config/config.go index 75c32994..4e7a7b25 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -64,6 +64,7 @@ type Config struct { TLS *TLSOptions `json:"tls,omitempty"` Password string `json:"password,omitempty"` Templates *templates.Templates `json:"templates,omitempty"` + CommonName string `json:"commonName,omitempty"` } // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer @@ -169,6 +170,9 @@ func (c *Config) Init() { if c.AuthorityConfig == nil { c.AuthorityConfig = &AuthConfig{} } + if c.CommonName == "" { + c.CommonName = "Step Online CA" + } c.AuthorityConfig.init() } diff --git a/authority/tls.go b/authority/tls.go index cc049655..f6cd34c3 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -509,7 +509,7 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { } // Create initial certificate request. - cr, err := x509util.CreateCertificateRequest("Step Online CA", a.config.DNSNames, signer) + cr, err := x509util.CreateCertificateRequest(a.config.CommonName, a.config.DNSNames, signer) if err != nil { return fatal(err) } From 26d7b70957bc368ff77eadff87bf18af170b9dfb Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Thu, 13 Jan 2022 16:23:54 +0100 Subject: [PATCH 004/241] feat(cas): add generic Config parameter to allow more flexible configuration on CAS --- cas/apiv1/options.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index badad7fc..bdad39c6 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -61,6 +61,9 @@ type Options struct { CaPool string `json:"-"` CaPoolTier string `json:"-"` GCSBucket string `json:"-"` + + // Generic structure to configure any CAS + Config map[string]interface{} `json:"config,omitempty"` } // CertificateIssuer contains the properties used to use the StepCAS certificate From 91d51c2b8810f277a1a65805a799b6ae592129df Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 14 Jan 2022 13:06:32 +0100 Subject: [PATCH 005/241] 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 16390694e1750e3fc645e665a0f04f5320a90271 Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Fri, 14 Jan 2022 18:56:17 +0100 Subject: [PATCH 006/241] feat(vault): adding hashicorp vault cas --- cas/apiv1/options.go | 1 + cas/apiv1/services.go | 2 + cas/vaultcas/vaultcas.go | 322 +++++++++++++++++++++++++++++++++ cas/vaultcas/vaultcas_test.go | 235 +++++++++++++++++++++++++ cmd/step-ca/main.go | 1 + go.mod | 6 +- go.sum | 323 +++++++++++++++++++++++++++++++++- 7 files changed, 887 insertions(+), 3 deletions(-) create mode 100644 cas/vaultcas/vaultcas.go create mode 100644 cas/vaultcas/vaultcas_test.go diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index bdad39c6..7c62548b 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -17,6 +17,7 @@ type Options struct { // CertificateAuthority reference: // In StepCAS the value is the CA url, e.g. "https://ca.smallstep.com:9000". // In CloudCAS the format is "projects/*/locations/*/certificateAuthorities/*". + // In VaultCAS the value is the url, e.g. "https://vault.smallstep.com". CertificateAuthority string `json:"certificateAuthority,omitempty"` // CertificateAuthorityFingerprint is the root fingerprint used to diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index cf9a5470..c8a8b0e9 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -45,6 +45,8 @@ const ( CloudCAS = "cloudcas" // StepCAS is a CertificateAuthorityService using another step-ca instance. StepCAS = "stepcas" + // VaultCAS is a CertificateAuthorityService using Hasicorp Vault PKI. + VaultCAS = "vaultcas" ) // String returns a string from the type. It will always return the lower case diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go new file mode 100644 index 00000000..d82ed7d7 --- /dev/null +++ b/cas/vaultcas/vaultcas.go @@ -0,0 +1,322 @@ +package vaultcas + +import ( + "context" + "crypto/x509" + "encoding/pem" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/cas/apiv1" + + vault "github.com/hashicorp/vault/api" + auth "github.com/hashicorp/vault/api/auth/approle" + certutil "github.com/hashicorp/vault/sdk/helper/certutil" + mapstructure "github.com/mitchellh/mapstructure" +) + +func init() { + apiv1.Register(apiv1.VaultCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) { + return New(ctx, opts) + }) +} + +type VaultOptions struct { + PKI string `json:"pki,omitempty"` + PKIRole string `json:"pkiRole,omitempty` + PKIRoleRSA string `json:"pkiRoleRSA,omitempty` + PKIRoleEC string `json:"pkiRoleEC,omitempty` + PKIRoleED25519 string `json:"PKIRoleED25519,omitempty` + RoleID string `json:"roleID,omitempty"` + SecretID string `json:"secretID,omitempty"` + AppRole string `json:"appRole,omitempty"` + IsWrappingToken bool `json:"isWrappingToken,omitempty"` +} + +// VaultCAS implements a Certificate Authority Service using Hashicorp Vault. +type VaultCAS struct { + client *vault.Client + config VaultOptions + fingerprint string +} + +func loadOptions(config map[string]interface{}) (vc VaultOptions, err error) { + err = mapstructure.Decode(config, &vc) + if err != nil { + return vc, err + } + + if vc.PKI == "" { + vc.PKI = "pki" // use default pki vault name + } + + // pkirole or per key type must be defined + if vc.PKIRole == "" && vc.PKIRoleRSA == "" && vc.PKIRoleEC == "" && vc.PKIRoleED25519 == "" { + return vc, errors.New("loadOptions you must define a pki role") + } + + // if pkirole is empty all others keys must be set + if vc.PKIRole == "" && (vc.PKIRoleRSA == "" || vc.PKIRoleEC == "" || vc.PKIRoleED25519 == "") { + return vc, errors.New("loadOptions if 'pkiRole' is empty, PKIRoleRSA, PKIRoleEC and PKIRoleED25519 cannot be empty") + } + + // if pkirole is not empty, use it as default for unset keys + if vc.PKIRole != "" { + if vc.PKIRoleRSA == "" { + vc.PKIRoleRSA = vc.PKIRole + } + if vc.PKIRoleEC == "" { + vc.PKIRoleEC = vc.PKIRole + } + if vc.PKIRoleED25519 == "" { + vc.PKIRoleED25519 = vc.PKIRole + } + } + + if vc.RoleID == "" { + return vc, errors.New("loadOptions 'roleID' cannot be empty") + } + + if vc.SecretID == "" { + return vc, errors.New("loadOptions 'secretID' cannot be empty") + } + + if vc.AppRole == "" { + vc.AppRole = "auth/approle" + } + + return vc, nil +} + +func getCertificateAndChain(certb certutil.CertBundle) (*x509.Certificate, []*x509.Certificate, error) { + cert, err := parseCertificate(certb.Certificate) + if err != nil { + return nil, nil, err + } + chain := make([]*x509.Certificate, len(certb.CAChain)) + for i := range certb.CAChain { + chain[i], err = parseCertificate(certb.CAChain[i]) + if err != nil { + return nil, nil, err + } + } + return cert, chain, nil +} + +func parseCertificate(pemCert string) (*x509.Certificate, error) { + block, _ := pem.Decode([]byte(pemCert)) + if block == nil { + return nil, errors.Errorf("parseCertificate: error decoding certificate: not a valid PEM encoded block '%v'", pemCert) + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "parseCertificate: error parsing certificate") + } + return cert, nil +} + +func parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) { + block, _ := pem.Decode([]byte(pemCsr)) + if block == nil { + return nil, errors.New("error decoding certificate request: not a valid PEM encoded block") + } + cr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing certificate request") + } + return cr, nil +} + +func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.Duration) (*x509.Certificate, []*x509.Certificate, error) { + sans := make([]string, 0, len(cr.DNSNames)+len(cr.EmailAddresses)+len(cr.IPAddresses)+len(cr.URIs)) + sans = append(sans, cr.DNSNames...) + sans = append(sans, cr.EmailAddresses...) + for _, ip := range cr.IPAddresses { + sans = append(sans, ip.String()) + } + for _, u := range cr.URIs { + sans = append(sans, u.String()) + } + + commonName := cr.Subject.CommonName + if commonName == "" && len(sans) > 0 { + commonName = sans[0] + } + + var vaultPKIRole string + csr := api.CertificateRequest{CertificateRequest: cr} + + switch { + case csr.PublicKeyAlgorithm == x509.RSA: + vaultPKIRole = v.config.PKIRoleRSA + case csr.PublicKeyAlgorithm == x509.ECDSA: + vaultPKIRole = v.config.PKIRoleEC + case csr.PublicKeyAlgorithm == x509.Ed25519: + vaultPKIRole = v.config.PKIRoleED25519 + default: + return nil, nil, errors.Errorf("createCertificate: Unsupported public key algorithm '%v'", csr.PublicKeyAlgorithm) + } + + certPemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw}) + if certPemBytes == nil { + return nil, nil, errors.Errorf("createCertificate: Failed to encode pem '%v'", csr.Raw) + } + + y := map[string]interface{}{ + "csr": string(certPemBytes), + "format": "pem_bundle", + "ttl": lifetime.Seconds(), + } + + secret, err := v.client.Logical().Write(v.config.PKI+"/sign/"+vaultPKIRole, y) + if err != nil { + return nil, nil, errors.Wrapf(err, "createCertificate: unable to sign certificate %v", y) + } + if secret == nil { + return nil, nil, errors.New("createCertificate: secret sign is empty") + } + + var certBundle certutil.CertBundle + + err = mapstructure.Decode(secret.Data, &certBundle) + if err != nil { + return nil, nil, err + } + + // Return certificate and certificate chain + return getCertificateAndChain(certBundle) +} + +// New creates a new CertificateAuthorityService implementation +// using Hashicorp Vault +func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { + if opts.CertificateAuthority == "" { + return nil, errors.New("vaultCAS 'certificateAuthority' cannot be empty") + } + + if opts.CertificateAuthorityFingerprint == "" { + return nil, errors.New("vaultCAS 'certificateAuthorityFingerprint' cannot be empty") + } + + vc, err := loadOptions(opts.Config) + if err != nil { + return nil, err + } + + config := vault.DefaultConfig() + config.Address = opts.CertificateAuthority + + client, err := vault.NewClient(config) + if err != nil { + return nil, errors.Wrap(err, "unable to initialize vault client") + } + + var appRoleAuth *auth.AppRoleAuth + if vc.IsWrappingToken == true { + appRoleAuth, err = auth.NewAppRoleAuth( + vc.RoleID, + &auth.SecretID{FromString: vc.SecretID}, + auth.WithWrappingToken(), + auth.WithMountPath(vc.AppRole), + ) + } else { + appRoleAuth, err = auth.NewAppRoleAuth( + vc.RoleID, + &auth.SecretID{FromString: vc.SecretID}, + auth.WithMountPath(vc.AppRole), + ) + } + if err != nil { + return nil, errors.Wrap(err, "unable to initialize AppRole auth method") + } + + authInfo, err := client.Auth().Login(ctx, appRoleAuth) + if err != nil { + return nil, errors.Wrap(err, "unable to login to AppRole auth method") + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } + + return &VaultCAS{ + client: client, + config: vc, + fingerprint: opts.CertificateAuthorityFingerprint, + }, nil +} + +// CreateCertificate signs a new certificate using Hashicorp Vault. +func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { + switch { + case req.CSR == nil: + return nil, errors.New("CreateCertificate: `CSR` cannot be nil") + case req.Lifetime == 0: + return nil, errors.New("CreateCertificate: `LIFETIME` cannot be 0") + } + + cert, chain, err := v.createCertificate(req.CSR, req.Lifetime) + if err != nil { + return nil, err + } + + return &apiv1.CreateCertificateResponse{ + Certificate: cert, + CertificateChain: chain, + }, nil +} + +func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { + secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca") + if err != nil { + return nil, errors.Wrap(err, "GetCertificateAuthority: unable to read root") + } + if secret == nil { + return nil, errors.New("GetCertificateAuthority: secret root is empty") + } + + var certBundle certutil.CertBundle + + err = mapstructure.Decode(secret.Data, &certBundle) + if err != nil { + return nil, err + } + + cert, _, err := getCertificateAndChain(certBundle) + if err != nil { + return nil, err + } + return &apiv1.GetCertificateAuthorityResponse{ + RootCertificate: cert, + }, nil +} + +// RenewCertificate will always return a non-implemented error as renewals +// are not supported yet. +func (v *VaultCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { + return nil, apiv1.ErrNotImplemented{Message: "vaultCAS does not support renewals"} +} + +func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { + if req.SerialNumber == "" && req.Certificate == nil { + return nil, errors.New("RevokeCertificate `serialNumber` or `certificate` are required") + } + + serialNumber := req.SerialNumber + if req.Certificate != nil { + serialNumber = req.Certificate.SerialNumber.String() + } + + serialNumberDash := strings.ReplaceAll(serialNumber, ":", "-") + + _, err := v.client.Logical().Write(v.config.PKI+"/revoke/"+serialNumberDash, nil) + if err != nil { + return nil, errors.Wrap(err, "RevokeCertificate unable to revoke certificate") + } + + return &apiv1.RevokeCertificateResponse{ + Certificate: req.Certificate, + CertificateChain: nil, + }, nil +} diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go new file mode 100644 index 00000000..b91e29ae --- /dev/null +++ b/cas/vaultcas/vaultcas_test.go @@ -0,0 +1,235 @@ +package vaultcas + +import ( + "context" + "crypto/x509" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + "time" + + vault "github.com/hashicorp/vault/api" + "github.com/smallstep/certificates/cas/apiv1" +) + +var ( + testCertificateSigned = `-----BEGIN CERTIFICATE----- +MIIB/DCCAaKgAwIBAgIQHHFuGMz0cClfde5kqP5prTAKBggqhkjOPQQDAjAqMSgw +JgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx +NTAwMDQ0M1oXDTMwMDkxMzAwMDQ0MFowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0 +ZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMqNCiXMvbn74LsHzRv+8 +17m9vEzH6RHrg3m82e0uEc36+fZWV/zJ9SKuONmnl5VP79LsjL5SVH0RDj73U2XO +DKOBtjCBszAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMB0GA1UdDgQWBBRTA2cTs7PCNjnps/+T0dS8diqv0DAfBgNVHSMEGDAW +gBRIOVqyLDSlErJLuWWEvRm5UU1r1TBCBgwrBgEEAYKkZMYoQAIEMjAwEwhjbG91 +ZGNhcxMkZDhkMThhNjgtNTI5Ni00YWYzLWFlNGItMmY4NzdkYTNmYmQ5MAoGCCqG +SM49BAMCA0gAMEUCIGxl+pqJ50WYWUqK2l4V1FHoXSi0Nht5kwTxFxnWZu1xAiEA +zemu3bhWLFaGg3s8i+HTEhw4RqkHP74vF7AVYp88bAw= +-----END CERTIFICATE-----` + testCertificateCsrEc = `-----BEGIN CERTIFICATE REQUEST----- +MIHoMIGPAgEAMA0xCzAJBgNVBAMTAkVDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAEUVVVZGD6eUrB20T/qrjKZoYzseQ18AIm9jtUNpQn5hIClpdk2zKy5bja3iUa +nmqRKCIz/B/MU55zuNDeckqqX6AgMB4GCSqGSIb3DQEJDjERMA8wDQYDVR0RBAYw +BIICRUMwCgYIKoZIzj0EAwIDSAAwRQIhAJxpWyH7cctbzcnK1JBWDAmc/G61bq9y +otHrQDfYvS8bAiBVGQz2cfO2SqhvkkQbOqWUFjk1wHzISvlTjyc3IJ7FLw== +-----END CERTIFICATE REQUEST-----` + testCertificateCsrRsa = `-----BEGIN CERTIFICATE REQUEST----- +MIICdDCCAVwCAQAwDjEMMAoGA1UEAxMDUlNBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAxe5XLSZrTCzzH0FJCXvZwghAY5XztzjseSRcm0jL8Q7nvNWi +Vpu1n7EmfVU9b8sbvtVYqMQV+hMdj2C/NIw4Yal4Wg+BgunYOrRqfY7oDm4csG0R +g5v0h2yQw14kqVrftNyojX0Nv/CPboCGl64PA9zsEXQTB3Y1AUWrUGPiBWNACYIH +mjv70Ay9JKBBAqov38I7nka/RgYAl5DCHzU2vvODriBYFWagnzycA4Ni5EKTz93W +SPdDEhkWi3ugUqal3SvgHl8re+8d7ghLn85Y3TFuyU2nSMDPHaymsiNFw1mRwOw3 +lAseidHJkPQs7q6FiYXaeqetf1j/gw0n23ZogwIDAQABoCEwHwYJKoZIhvcNAQkO +MRIwEDAOBgNVHREEBzAFggNSU0EwDQYJKoZIhvcNAQELBQADggEBALnO5vcDkgGO +GQoSINa2NmNFxAtYQGYHok5KXYX+S+etmOmDrmrhsl/pSjN3GPCPlThFlbLStB70 +oJw67nEjGf0hPEBVlm+qFUsYQ1KGRZFAWDSMQ//pU225XFDCmlzHfV7gZjSkP9GN +Gc5VECOzx6hAFR+IEL/l/1GG5HHkPPrr/8OvuIfm2V5ofYmhsXMVVYH52qPofMAV +B8UdNnZK3nyLdUqVd+PYUUJmN4bJ8YfxofKKgbLkhvkKp4OZ9vkwUi2+61NdHTf2 +wIauOyxEoTlJpU6oA/sxu/2Ht2DP+8y6mognLBuKklE/VH3/2iqQWyg1NV5hyg3b +loVSdLsIh5Y= +-----END CERTIFICATE REQUEST-----` + testCertificateCsrEd25519 = `-----BEGIN CERTIFICATE REQUEST----- +MIGuMGICAQAwDjEMMAoGA1UEAxMDT0tQMCowBQYDK2VwAyEAopc6daK4zYR6BDAM +pV/v53oR/ewbtrkHZQkN/amFMLagITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQH +MAWCA09LUDAFBgMrZXADQQDJi47MAgl/WKAz+V/kDu1k/zbKk1nrHHAUonbofHUW +M6ihSD43+awq3BPeyPbToeH5orSH9l3MuTfbxPb5BVEH +-----END CERTIFICATE REQUEST-----` + testRootFingerprint = `e7678acf0d8de731262bce2fe792c48f19547285f5976805125a40867c77464e` +) + +func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate { + t.Helper() + crt, err := parseCertificate(pemCert) + if err != nil { + t.Fatal(err) + } + return crt +} + +func mustParseCertificateRequest(t *testing.T, pemCert string) *x509.CertificateRequest { + t.Helper() + crt, err := parseCertificateRequest(pemCert) + if err != nil { + t.Fatal(err) + } + return crt +} + +func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { + t.Helper() + + writeJSON := func(w http.ResponseWriter, v interface{}) { + _ = json.NewEncoder(w).Encode(v) + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.RequestURI == "/v1/auth/auth/approle/login": + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "auth": { + "client_token": "98a4c7ab-b1fe-361b-ba0b-e307aacfd587" + } + }`) + case r.RequestURI == "/v1/pki/sign/ec": + w.WriteHeader(http.StatusOK) + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} + writeJSON(w, cert) + return + case r.RequestURI == "/v1/pki/sign/rsa": + w.WriteHeader(http.StatusOK) + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} + writeJSON(w, cert) + return + case r.RequestURI == "/v1/pki/sign/ed25519": + w.WriteHeader(http.StatusOK) + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} + writeJSON(w, cert) + return + default: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, `{"error":"not found"}`) + } + })) + t.Cleanup(func() { + srv.Close() + }) + u, err := url.Parse(srv.URL) + if err != nil { + srv.Close() + t.Fatal(err) + } + + config := vault.DefaultConfig() + config.Address = srv.URL + + client, err := vault.NewClient(config) + if err != nil { + srv.Close() + t.Fatal(err) + } + + return u, client +} + +func TestNew_register(t *testing.T) { + caURL, _ := testCAHelper(t) + + fn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.VaultCAS) + if !ok { + t.Errorf("apiv1.Register() ok = %v, want true", ok) + return + } + _, err := fn(context.Background(), apiv1.Options{ + CertificateAuthority: caURL.String(), + CertificateAuthorityFingerprint: testRootFingerprint, + Config: map[string]interface{}{ + "PKI": "pki", + "PKIRole": "pki-role", + "RoleID": "roleID", + "SecretID": "secretID", + "IsWrappingToken": false, + }, + }) + + if err != nil { + t.Errorf("New() error = %v", err) + return + } +} + +func TestVaultCAS_CreateCertificate(t *testing.T) { + _, client := testCAHelper(t) + + options := VaultOptions{ + PKI: "pki", + PKIRole: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleED25519: "ed25519", + RoleID: "roleID", + SecretID: "secretID", + AppRole: "approle", + IsWrappingToken: false, + } + + type fields struct { + client *vault.Client + options VaultOptions + } + + type args struct { + req *apiv1.CreateCertificateRequest + } + + tests := []struct { + name string + fields fields + args args + want *apiv1.CreateCertificateResponse + wantErr bool + }{ + {"ok ec", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: mustParseCertificateRequest(t, testCertificateCsrEc), + Lifetime: time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: mustParseCertificate(t, testCertificateSigned), + CertificateChain: []*x509.Certificate{}, + }, false}, + {"ok rsa", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: mustParseCertificateRequest(t, testCertificateCsrRsa), + Lifetime: time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: mustParseCertificate(t, testCertificateSigned), + CertificateChain: []*x509.Certificate{}, + }, false}, + {"ok ed25519", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: mustParseCertificateRequest(t, testCertificateCsrEd25519), + Lifetime: time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: mustParseCertificate(t, testCertificateSigned), + CertificateChain: []*x509.Certificate{}, + }, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &VaultCAS{ + client: tt.fields.client, + config: tt.fields.options, + } + got, err := c.CreateCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.CreateCertificate() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index f40ddf5f..9b02d892 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -40,6 +40,7 @@ import ( _ "github.com/smallstep/certificates/cas/cloudcas" _ "github.com/smallstep/certificates/cas/softcas" _ "github.com/smallstep/certificates/cas/stepcas" + _ "github.com/smallstep/certificates/cas/vaultcas" ) // commit and buildTime are filled in during build by the Makefile diff --git a/go.mod b/go.mod index 16633d1c..d0cbcb46 100644 --- a/go.mod +++ b/go.mod @@ -21,9 +21,13 @@ require ( github.com/google/go-cmp v0.5.6 github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.0.5 + github.com/hashicorp/vault/api v1.3.1 + github.com/hashicorp/vault/api/auth/approle v0.1.1 + github.com/hashicorp/vault/sdk v0.3.0 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/mitchellh/mapstructure v1.4.2 github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.9.1 github.com/rs/xid v1.2.1 @@ -40,7 +44,7 @@ require ( golang.org/x/net v0.0.0-20211216030914-fe4d6282115f google.golang.org/api v0.47.0 google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 - google.golang.org/grpc v1.39.0 + google.golang.org/grpc v1.41.0 google.golang.org/protobuf v1.27.1 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index f66f7934..d42da957 100644 --- a/go.sum +++ b/go.sum @@ -25,18 +25,23 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -67,8 +72,13 @@ github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -81,61 +91,87 @@ github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmy github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= 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-v2 v0.18.0 h1:qZ+woO4SamnH/eEbjM2IDLhRNwIwND/RQyVlBLp3Jqg= 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= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -143,18 +179,34 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158 h1:CevA8fI91PAnP8vpnXuB8ZYAZ5wqY86nAbxfgK8tWO4= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= @@ -162,6 +214,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -174,6 +229,7 @@ github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70d github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= @@ -184,9 +240,13 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -196,26 +256,49 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs= +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 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= +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/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 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= 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/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= 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= +github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= +github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap/v3 v3.1.10 h1:7WsKqasmPThNvdl0Q5GPpbTDD/ZD98CfuawrMIuh7qQ= +github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= @@ -228,11 +311,16 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB 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= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 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= @@ -268,9 +356,11 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -285,11 +375,15 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -302,7 +396,9 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22 h1:ub2sxhs2A0HRa2dWHavvmWxiVGXNfE9wI+gcTMwED8A= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -312,110 +408,215 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 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= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda h1:5ikpG9mYCMFiZX0nkxoV6aU2IpCHPdws3gCNgdZeEV0= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.3.0 h1:HXNYlRkkM/t+Y/Yhxtwcy02dlYwIaoxzvxPnS+cqy78= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0 h1:UOxjlb4xVNF93jak1mzzoBatyFju9nrkxpVwIp/QqxQ= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs= +github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/password v0.1.1 h1:6JzmBqXprakgFEHwBgdchsjaA9x3GyjdI568bXKxa60= +github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1 h1:Yc026VyMyIpq1UWRnakHRG01U8fJm+nEfEmjoAb00n8= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= +github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO2aRM= +github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= +github.com/hashicorp/vault/api/auth/approle v0.1.1 h1:R5yA+xcNvw1ix6bDuWOaLOq2L4L77zDCVsethNw97xQ= +github.com/hashicorp/vault/api/auth/approle v0.1.1/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag= +github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY= +github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0 h1:0U6+BtN6LhaYuTnIJq4Wyq5cpn6O2kWrxAtcqBmYY6w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +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/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/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 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= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU= 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 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= 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/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= 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= @@ -423,77 +624,121 @@ github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HK github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f h1:8dM0ilqKL0Uzl42GABzzC4Oqlc3kGRILz0vgoff7nwg= 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/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= 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= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= +github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -501,11 +746,15 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -514,7 +763,13 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -525,6 +780,7 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/slackhq/nebula v1.5.2 h1:wuIOHsOnrNw3rQx8yPxXiGu8wAtAxxtUI/K8W7Vj7EI= github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIpR52WlE= @@ -533,29 +789,42 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/nosql v0.3.9 h1:YPy5PR3PXClqmpFaVv0wfXDXDc7NXGBE1auyU2c87dc= github.com/smallstep/nosql v0.3.9/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a h1:AhmOdSHeswKHBjhsLs/7+1voOxT+LLrSk/Nxvk35fug= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -566,27 +835,37 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/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 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= 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= @@ -601,6 +880,7 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds= go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= @@ -611,10 +891,15 @@ go.step.sm/linkedca v0.9.0 h1:xKXZoRXy4B7LeGBZozq62IQ0p3v8dT33O9UOMpVtRtI= go.step.sm/linkedca v0.9.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.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -630,6 +915,7 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= @@ -643,8 +929,10 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -657,8 +945,10 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -668,8 +958,10 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -742,6 +1034,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -767,6 +1060,7 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -809,6 +1103,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -833,6 +1128,8 @@ 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/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= @@ -890,13 +1187,16 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/windows v0.5.1 h1:OnYw96PF+CsIMrqWo5QP3Q59q5hY1rFErk/yN3cS+JQ= golang.zx2c4.com/wireguard/windows v0.5.1/go.mod h1:EApyTk/ZNrkbZjurHL1nleDYnsPpJYBO7LZEBCyDAHk= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -931,6 +1231,7 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -976,6 +1277,7 @@ google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 h1:7yQQsvnwjfEahbNNEKcBHv3mR+HnB1ctGY/z1JXzx8M= google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 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= @@ -1003,8 +1305,10 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= 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= @@ -1020,20 +1324,29 @@ 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= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1054,9 +1367,15 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= From 6bc301339ff86a2e1c40a82ad6ebd36fef46175d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 17 Jan 2022 22:49:47 +0100 Subject: [PATCH 007/241] Improve test case and code coverage --- authority/provisioner/policy.go | 10 +- policy/x509/options.go | 275 +++-- policy/x509/options_test.go | 1339 ++++++++++++++++++++++++ policy/x509/x509.go | 204 +++- policy/x509/x509_test.go | 1737 +++++++++++++++++++++++++------ 5 files changed, 3099 insertions(+), 466 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) +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) + } + 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 "", fmt.Errorf("cannot parse URI domain constraint %q", constraint) } - return nil + 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: 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? In x509 there's a limit on the number of comparisons + // TODO: set limit on total of all names validated? In x509 there's a limit on the number of comparisons // that protects the CA from a DoS (i.e. many heavy comparisons). The x509 implementation takes // 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"}, @@ -76,68 +640,73 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/dns-excluded", - fields: fields{ - excludedDNSDomains: []string{"example.com"}, + name: "fail/dns-permitted-dot-domain", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ - DNSNames: []string{"www.example.com"}, + DNSNames: []string{ + ".local", + }, }, want: false, wantErr: true, }, { - name: "fail/dns-excluded-single-host", - fields: fields{ - excludedDNSDomains: []string{"example.com"}, + name: "fail/dns-permitted-wildcard-multiple-subdomains", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ - DNSNames: []string{"example.com"}, + DNSNames: []string{ + "sub.example.local", + }, }, 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), - }, - }, + name: "fail/dns-permitted-wildcard-literal", + options: []NamePolicyOption{ + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, + DNSNames: []string{ + "*.local", + }, }, 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), + 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("127.0.0.1")}, + IPAddresses: []net.IP{net.ParseIP("1.1.1.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), + 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")}, @@ -146,64 +715,67 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { 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), - }, - }, + name: "fail/mail-permitted-wildcard", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, + EmailAddresses: []string{ + "test@local.com", + }, }, want: false, wantErr: true, }, { - name: "fail/mail-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, + name: "fail/mail-permitted-wildcard-x509", + options: []NamePolicyOption{ + AddPermittedEmailAddress("example.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.com"}, + EmailAddresses: []string{ + "test@local.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 + name: "fail/mail-permitted-specific-mailbox", + options: []NamePolicyOption{ + AddPermittedEmailAddress("test@local.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, + EmailAddresses: []string{ + "root@local.com", + }, }, want: false, wantErr: true, }, { - name: "fail/mail-excluded", - fields: fields{ - excludedEmailAddresses: []string{"example.local"}, + name: "fail/mail-permitted-wildcard-subdomain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, + EmailAddresses: []string{ + "test@sub.example.com", + }, }, want: false, wantErr: true, }, { - name: "fail/uri-permitted", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "fail/permitted-uri-domain-wildcard", + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "www.example.local", + Host: "example.com", }, }, }, @@ -211,15 +783,15 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/uri-permitted-period-host", - fields: fields{ - permittedURIDomains: []string{".example.local"}, + name: "fail/permitted-uri", + options: []NamePolicyOption{ + AddPermittedURIDomain("test.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "example.local", + Host: "bad.local", }, }, }, @@ -227,90 +799,113 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: true, }, { - name: "fail/uri-permitted-period-host-certificate", - fields: fields{ - permittedURIDomains: []string{".example.local"}, + 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: ".example.local", + Host: "*.local", }, }, }, want: false, wantErr: true, }, + // SINGLE SAN TYPE EXCLUDED FAILURE TESTS { - name: "fail/uri-permitted-empty-host", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "fail/dns-excluded", + options: []NamePolicyOption{ + AddExcludedDNSDomain("*.example.com"), }, cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "", - }, - }, + DNSNames: []string{"www.example.com"}, }, want: false, wantErr: true, }, { - name: "fail/uri-permitted-port-missing", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "fail/dns-excluded-single-host", + options: []NamePolicyOption{ + AddExcludedDNSDomain("host.example.com"), }, cert: &x509.Certificate{ - URIs: []*url.URL{ - { - Scheme: "https", - Host: "example.local::", - }, - }, + DNSNames: []string{"host.example.com"}, }, want: false, wantErr: true, }, { - name: "fail/uri-permitted-ip", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "fail/ipv4-excluded", + options: []NamePolicyOption{ + AddExcludedIPRanges( + []*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: "127.0.0.1", + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/ipv6-excluded", + 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")}, + }, + want: false, + wantErr: true, + }, + { + name: "fail/mail-excluded", + options: []NamePolicyOption{ + AddExcludedEmailAddress("@example.com"), + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"mail@example.com"}, }, 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,167 +1284,219 @@ 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"}, + IPAddresses: []net.IP{ + net.ParseIP("127.0.0.1"), + }, }, want: true, wantErr: false, }, { - name: "ok/dns-permitted", - fields: fields{ - permittedDNSDomains: []string{".local"}, - }, + name: "ok/ipv6-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, + IPAddresses: []net.IP{ + net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + }, }, want: true, wantErr: false, }, { - name: "ok/dns-excluded", - fields: fields{ - excludedDNSDomains: []string{".notlocal"}, - }, + name: "ok/mail-no-constraints", + options: []NamePolicyOption{}, cert: &x509.Certificate{ - DNSNames: []string{"example.local"}, + EmailAddresses: []string{"mail@smallstep.com"}, }, want: true, wantErr: false, }, { - name: "ok/ipv4-permitted", - fields: fields{ - permittedIPRanges: []*net.IPNet{ + name: "ok/uri-no-constraints", + options: []NamePolicyOption{}, + cert: &x509.Certificate{ + 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/subject-no-constraints", + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + }, cert: &x509.Certificate{ - IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, + Subject: pkix.Name{ + CommonName: "www.example.com", + }, }, 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/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{ - IPAddresses: []net.IP{net.ParseIP("10.10.10.10")}, + DNSNames: []string{"example.local"}, }, 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), - }, + 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{ - IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, + DNSNames: []string{"example.local"}, }, 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), - }, + 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{ - IPAddresses: []net.IP{net.ParseIP("2003:0db8:85a3:0000:0000:8a2e:0370:7334")}, + DNSNames: []string{ + "example.local", + "example.x509local", + "host.example.com", + }, }, want: true, wantErr: false, }, { - name: "ok/mail-permitted", - fields: fields{ - permittedEmailAddresses: []string{"example.local"}, + name: "ok/ipv4-permitted", + options: []NamePolicyOption{ + AddPermittedCIDR("127.0.0.1/24"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.20")}, }, want: true, wantErr: false, }, { - name: "ok/mail-permitted-with-period-domain", - fields: fields{ - permittedEmailAddresses: []string{".example.local"}, + name: "ok/ipv6-permitted", + options: []NamePolicyOption{ + AddPermittedCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/120"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@somehost.example.local"}, + IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7339")}, }, want: true, wantErr: false, }, { - name: "ok/mail-permitted-with-multiple-labels", - fields: fields{ - permittedEmailAddresses: []string{".example.local"}, + name: "ok/mail-permitted-wildcard", + options: []NamePolicyOption{ + AddPermittedEmailAddress("@example.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@sub.www.example.local"}, + EmailAddresses: []string{ + "test@example.com", + }, }, want: true, wantErr: false, }, { - name: "ok/mail-excluded", - fields: fields{ - excludedEmailAddresses: []string{"example.notlocal"}, + name: "ok/mail-permitted-plain-domain", + options: []NamePolicyOption{ + AddPermittedEmailAddress("example.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@example.local"}, + EmailAddresses: []string{ + "test@example.com", + }, }, want: true, wantErr: false, }, { - name: "ok/mail-excluded-with-period-domain", - fields: fields{ - excludedEmailAddresses: []string{".example.notlocal"}, + name: "ok/mail-permitted-specific-mailbox", + options: []NamePolicyOption{ + AddPermittedEmailAddress("test@local.com"), }, cert: &x509.Certificate{ - EmailAddresses: []string{"mail@somehost.example.local"}, + EmailAddresses: []string{ + "test@local.com", + }, }, want: true, wantErr: false, }, { - name: "ok/uri-permitted", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "ok/uri-permitted-domain-wildcard", + options: []NamePolicyOption{ + AddPermittedURIDomain("*.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "www.example.com", + Host: "example.local", }, }, }, @@ -674,15 +1504,15 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/uri-permitted-with-port", - fields: fields{ - permittedURIDomains: []string{".example.com"}, + name: "ok/uri-permitted-specific-uri", + options: []NamePolicyOption{ + AddPermittedURIDomain("test.local"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "www.example.com:8080", + Host: "test.local", }, }, }, @@ -690,25 +1520,88 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { wantErr: false, }, { - name: "ok/uri-sub-permitted", - fields: fields{ - permittedURIDomains: []string{"example.com"}, + name: "ok/uri-permitted-with-port", + options: []NamePolicyOption{ + AddPermittedURIDomain(".example.com"), }, cert: &x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", - Host: "sub.host.example.com", + Host: "www.example.com:8080", }, }, }, want: true, wantErr: false, }, + // SINGLE SAN TYPE EXCLUDED SUCCESS TESTS + { + name: "ok/dns-excluded", + options: []NamePolicyOption{ + WithExcludedDNSDomain("*.notlocal"), + }, + cert: &x509.Certificate{ + 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{ @@ -721,11 +1614,12 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { want: true, wantErr: false, }, + // SUBJECT SUCCESS TESTS { name: "ok/subject-empty", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -738,9 +1632,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-dns-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedDNSDomains: []string{".local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedDNSDomain("*.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -752,9 +1646,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-dns-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedDNSDomains: []string{".notlocal"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedDNSDomain("*.notlocal"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -766,14 +1660,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { 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), + 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{ @@ -785,14 +1681,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { 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), + 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{ @@ -804,14 +1702,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { 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), + 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{ @@ -823,14 +1723,16 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { 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), + 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{ @@ -842,9 +1744,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-email-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedEmailAddresses: []string{"example.local"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedEmailAddress("@example.local"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -856,9 +1758,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-email-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedEmailAddresses: []string{"example.notlocal"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedEmailAddress("@example.notlocal"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -870,9 +1772,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-uri-permitted", - fields: fields{ - verifySubjectCommonName: true, - permittedURIDomains: []string{".example.com"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddPermittedURIDomain("*.example.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -884,9 +1786,9 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { }, { name: "ok/subject-uri-excluded", - fields: fields{ - verifySubjectCommonName: true, - excludedURIDomains: []string{".google.com"}, + options: []NamePolicyOption{ + WithSubjectCommonNameVerification(), + AddExcludedURIDomain("*.smallstep.com"), }, cert: &x509.Certificate{ Subject: pkix.Name{ @@ -896,21 +1798,185 @@ func TestNamePolicyEngine_AreCertificateNamesAllowed(t *testing.T) { 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{ + { + Scheme: "https", + Host: "www.example.com", + }, + }, + }, + want: true, + wantErr: false, + }, + { + 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{ + DNSNames: []string{"test.local"}, + }, + want: true, + wantErr: false, + }, + { + 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{ + EmailAddresses: []string{"mail@example.com"}, + }, + want: true, + wantErr: false, + }, + { + 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{ + URIs: []*url.URL{ + { + 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{ + 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/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, + }, + // 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 008/241] 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 009/241] 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 010/241] 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 d957a57e24500c0b8fc84a6bcc5ca6c13e670bbd Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Thu, 20 Jan 2022 10:16:47 +0100 Subject: [PATCH 011/241] fix: apply mariano suggestions and fixes * use json.RawMessage to remote mapstructure in options * use vault secretid structure to support multiple source aka string, file and env * remove log prefix * return raw cert on error on newline for cert and csr * clean sans, commonName in createCertificate (bad copy/paste from StepCAS) * verify authority fingerprint * convert serial on revoke to bigint, bytes and vault dashed representation --- cas/apiv1/options.go | 3 +- cas/vaultcas/vaultcas.go | 116 +++++++++++-------- cas/vaultcas/vaultcas_test.go | 17 +-- go.mod | 1 - go.sum | 210 ---------------------------------- 5 files changed, 77 insertions(+), 270 deletions(-) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 7c62548b..a39b4115 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -3,6 +3,7 @@ package apiv1 import ( "crypto" "crypto/x509" + "encoding/json" "github.com/pkg/errors" "github.com/smallstep/certificates/kms" @@ -64,7 +65,7 @@ type Options struct { GCSBucket string `json:"-"` // Generic structure to configure any CAS - Config map[string]interface{} `json:"config,omitempty"` + Config json.RawMessage `json:"config,omitempty"` } // CertificateIssuer contains the properties used to use the StepCAS certificate diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index d82ed7d7..721b58ba 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -2,9 +2,11 @@ package vaultcas import ( "context" + "crypto/sha256" "crypto/x509" + "encoding/json" "encoding/pem" - "strings" + "math/big" "time" "github.com/pkg/errors" @@ -14,7 +16,6 @@ import ( vault "github.com/hashicorp/vault/api" auth "github.com/hashicorp/vault/api/auth/approle" certutil "github.com/hashicorp/vault/sdk/helper/certutil" - mapstructure "github.com/mitchellh/mapstructure" ) func init() { @@ -24,15 +25,15 @@ func init() { } type VaultOptions struct { - PKI string `json:"pki,omitempty"` - PKIRole string `json:"pkiRole,omitempty` - PKIRoleRSA string `json:"pkiRoleRSA,omitempty` - PKIRoleEC string `json:"pkiRoleEC,omitempty` - PKIRoleED25519 string `json:"PKIRoleED25519,omitempty` - RoleID string `json:"roleID,omitempty"` - SecretID string `json:"secretID,omitempty"` - AppRole string `json:"appRole,omitempty"` - IsWrappingToken bool `json:"isWrappingToken,omitempty"` + PKI string `json:"pki,omitempty"` + PKIRole string `json:"pkiRole,omitempty"` + PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` + PKIRoleEC string `json:"pkiRoleEC,omitempty"` + PKIRoleED25519 string `json:"PKIRoleED25519,omitempty"` + RoleID string `json:"roleID,omitempty"` + SecretID auth.SecretID `json:"secretID,omitempty"` + AppRole string `json:"appRole,omitempty"` + IsWrappingToken bool `json:"isWrappingToken,omitempty"` } // VaultCAS implements a Certificate Authority Service using Hashicorp Vault. @@ -42,10 +43,10 @@ type VaultCAS struct { fingerprint string } -func loadOptions(config map[string]interface{}) (vc VaultOptions, err error) { - err = mapstructure.Decode(config, &vc) +func loadOptions(config json.RawMessage) (vc VaultOptions, err error) { + err = json.Unmarshal(config, &vc) if err != nil { - return vc, err + return vc, errors.Wrap(err, "error decoding vaultCAS config") } if vc.PKI == "" { @@ -54,12 +55,12 @@ func loadOptions(config map[string]interface{}) (vc VaultOptions, err error) { // pkirole or per key type must be defined if vc.PKIRole == "" && vc.PKIRoleRSA == "" && vc.PKIRoleEC == "" && vc.PKIRoleED25519 == "" { - return vc, errors.New("loadOptions you must define a pki role") + return vc, errors.New("vaultCAS config options must define `pkiRole`") } // if pkirole is empty all others keys must be set if vc.PKIRole == "" && (vc.PKIRoleRSA == "" || vc.PKIRoleEC == "" || vc.PKIRoleED25519 == "") { - return vc, errors.New("loadOptions if 'pkiRole' is empty, PKIRoleRSA, PKIRoleEC and PKIRoleED25519 cannot be empty") + return vc, errors.New("vaultCAS config options must include a `pkiRole` or `pkiRoleRSA`, `pkiRoleEC` and `pkiRoleEd25519`") } // if pkirole is not empty, use it as default for unset keys @@ -76,11 +77,15 @@ func loadOptions(config map[string]interface{}) (vc VaultOptions, err error) { } if vc.RoleID == "" { - return vc, errors.New("loadOptions 'roleID' cannot be empty") + return vc, errors.New("vaultCAS config options must define `roleID`") } - if vc.SecretID == "" { - return vc, errors.New("loadOptions 'secretID' cannot be empty") + if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" { + return vc, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") + } + + if vc.PKI == "" { + vc.PKI = "pki" // use default pki vault name } if vc.AppRole == "" { @@ -108,11 +113,11 @@ func getCertificateAndChain(certb certutil.CertBundle) (*x509.Certificate, []*x5 func parseCertificate(pemCert string) (*x509.Certificate, error) { block, _ := pem.Decode([]byte(pemCert)) if block == nil { - return nil, errors.Errorf("parseCertificate: error decoding certificate: not a valid PEM encoded block '%v'", pemCert) + return nil, errors.Errorf("error decoding certificate: not a valid PEM encoded block, please verify\r\n%v", pemCert) } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return nil, errors.Wrap(err, "parseCertificate: error parsing certificate") + return nil, errors.Wrap(err, "error parsing certificate") } return cert, nil } @@ -120,7 +125,7 @@ func parseCertificate(pemCert string) (*x509.Certificate, error) { func parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) { block, _ := pem.Decode([]byte(pemCsr)) if block == nil { - return nil, errors.New("error decoding certificate request: not a valid PEM encoded block") + return nil, errors.Errorf("error decoding certificate request: not a valid PEM encoded block, please verify\r\n%v", pemCsr) } cr, err := x509.ParseCertificateRequest(block.Bytes) if err != nil { @@ -130,21 +135,6 @@ func parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) { } func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.Duration) (*x509.Certificate, []*x509.Certificate, error) { - sans := make([]string, 0, len(cr.DNSNames)+len(cr.EmailAddresses)+len(cr.IPAddresses)+len(cr.URIs)) - sans = append(sans, cr.DNSNames...) - sans = append(sans, cr.EmailAddresses...) - for _, ip := range cr.IPAddresses { - sans = append(sans, ip.String()) - } - for _, u := range cr.URIs { - sans = append(sans, u.String()) - } - - commonName := cr.Subject.CommonName - if commonName == "" && len(sans) > 0 { - commonName = sans[0] - } - var vaultPKIRole string csr := api.CertificateRequest{CertificateRequest: cr} @@ -180,7 +170,12 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time. var certBundle certutil.CertBundle - err = mapstructure.Decode(secret.Data, &certBundle) + secretData, err := json.Marshal(secret.Data) + if err != nil { + return nil, nil, err + } + + err = json.Unmarshal(secretData, &certBundle) if err != nil { return nil, nil, err } @@ -214,17 +209,17 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { } var appRoleAuth *auth.AppRoleAuth - if vc.IsWrappingToken == true { + if vc.IsWrappingToken { appRoleAuth, err = auth.NewAppRoleAuth( vc.RoleID, - &auth.SecretID{FromString: vc.SecretID}, + &vc.SecretID, auth.WithWrappingToken(), auth.WithMountPath(vc.AppRole), ) } else { appRoleAuth, err = auth.NewAppRoleAuth( vc.RoleID, - &auth.SecretID{FromString: vc.SecretID}, + &vc.SecretID, auth.WithMountPath(vc.AppRole), ) } @@ -270,15 +265,20 @@ func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca") if err != nil { - return nil, errors.Wrap(err, "GetCertificateAuthority: unable to read root") + return nil, errors.Wrap(err, "unable to read root") } if secret == nil { - return nil, errors.New("GetCertificateAuthority: secret root is empty") + return nil, errors.New("secret root is empty") } var certBundle certutil.CertBundle - err = mapstructure.Decode(secret.Data, &certBundle) + secretData, err := json.Marshal(secret.Data) + if err != nil { + return nil, err + } + + err = json.Unmarshal(secretData, &certBundle) if err != nil { return nil, err } @@ -287,6 +287,13 @@ func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityReq if err != nil { return nil, err } + + sha256Sum := sha256.Sum256(cert.Raw) + expectedSum := certutil.GetHexFormatted(sha256Sum[:], "") + if expectedSum != v.fingerprint { + return nil, errors.Errorf("Vault Root CA fingerprint `%s` doesn't match config fingerprint `%v`", expectedSum, v.fingerprint) + } + return &apiv1.GetCertificateAuthorityResponse{ RootCertificate: cert, }, nil @@ -300,19 +307,28 @@ func (v *VaultCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1. func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { if req.SerialNumber == "" && req.Certificate == nil { - return nil, errors.New("RevokeCertificate `serialNumber` or `certificate` are required") + return nil, errors.New("`serialNumber` or `certificate` are required") } - serialNumber := req.SerialNumber - if req.Certificate != nil { - serialNumber = req.Certificate.SerialNumber.String() + var serialNumber []byte + if req.SerialNumber != "" { + // req.SerialNumber is a big.Int string representation + n := new(big.Int) + n, ok := n.SetString(req.SerialNumber, 10) + if !ok { + return nil, errors.Errorf("serialNumber `%v` can't be convert to big.Int", req.SerialNumber) + } + serialNumber = n.Bytes() + } else { + // req.Certificate.SerialNumber is a big.Int + serialNumber = req.Certificate.SerialNumber.Bytes() } - serialNumberDash := strings.ReplaceAll(serialNumber, ":", "-") + serialNumberDash := certutil.GetHexFormatted(serialNumber, "-") _, err := v.client.Logical().Write(v.config.PKI+"/revoke/"+serialNumberDash, nil) if err != nil { - return nil, errors.Wrap(err, "RevokeCertificate unable to revoke certificate") + return nil, errors.Wrap(err, "unable to revoke certificate") } return &apiv1.RevokeCertificateResponse{ diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index b91e29ae..1d814554 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -13,6 +13,7 @@ import ( "time" vault "github.com/hashicorp/vault/api" + auth "github.com/hashicorp/vault/api/auth/approle" "github.com/smallstep/certificates/cas/apiv1" ) @@ -148,13 +149,13 @@ func TestNew_register(t *testing.T) { _, err := fn(context.Background(), apiv1.Options{ CertificateAuthority: caURL.String(), CertificateAuthorityFingerprint: testRootFingerprint, - Config: map[string]interface{}{ - "PKI": "pki", - "PKIRole": "pki-role", - "RoleID": "roleID", - "SecretID": "secretID", - "IsWrappingToken": false, - }, + Config: json.RawMessage(`{ + "PKI": "pki", + "PKIRole": "pki-role", + "RoleID": "roleID", + "SecretID": {"FromString": "secretID"}, + "IsWrappingToken": false + }`), }) if err != nil { @@ -173,7 +174,7 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { PKIRoleEC: "ec", PKIRoleED25519: "ed25519", RoleID: "roleID", - SecretID: "secretID", + SecretID: auth.SecretID{FromString: "secretID"}, AppRole: "approle", IsWrappingToken: false, } diff --git a/go.mod b/go.mod index d0cbcb46..5307e95b 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( 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/mitchellh/mapstructure v1.4.2 github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.9.1 github.com/rs/xid v1.2.1 diff --git a/go.sum b/go.sum index d42da957..f0d9f2fa 100644 --- a/go.sum +++ b/go.sum @@ -25,23 +25,18 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -72,13 +67,9 @@ github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -91,46 +82,35 @@ github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmy github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -141,37 +121,28 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= 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-v2 v0.18.0 h1:qZ+woO4SamnH/eEbjM2IDLhRNwIwND/RQyVlBLp3Jqg= 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= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -179,34 +150,21 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158 h1:CevA8fI91PAnP8vpnXuB8ZYAZ5wqY86nAbxfgK8tWO4= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= @@ -214,9 +172,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -229,7 +185,6 @@ github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70d github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= @@ -240,13 +195,9 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -256,48 +207,35 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs= 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 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= 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/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 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= 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/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= 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= github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.1.10 h1:7WsKqasmPThNvdl0Q5GPpbTDD/ZD98CfuawrMIuh7qQ= github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -313,14 +251,11 @@ 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= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 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= @@ -360,7 +295,6 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -375,15 +309,11 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -396,9 +326,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22 h1:ub2sxhs2A0HRa2dWHavvmWxiVGXNfE9wI+gcTMwED8A= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -408,29 +336,19 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 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= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda h1:5ikpG9mYCMFiZX0nkxoV6aU2IpCHPdws3gCNgdZeEV0= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.3.0 h1:HXNYlRkkM/t+Y/Yhxtwcy02dlYwIaoxzvxPnS+cqy78= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.3.0 h1:UOxjlb4xVNF93jak1mzzoBatyFju9nrkxpVwIp/QqxQ= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -446,9 +364,7 @@ github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= -github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -461,22 +377,18 @@ github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ= github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/password v0.1.1 h1:6JzmBqXprakgFEHwBgdchsjaA9x3GyjdI568bXKxa60= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= -github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1 h1:Yc026VyMyIpq1UWRnakHRG01U8fJm+nEfEmjoAb00n8= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -484,7 +396,6 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -492,13 +403,9 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO2aRM= @@ -509,84 +416,60 @@ github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZdu github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/hudl/fargo v1.3.0 h1:0U6+BtN6LhaYuTnIJq4Wyq5cpn6O2kWrxAtcqBmYY6w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 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/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/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 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= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= -github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= -github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= @@ -603,20 +486,16 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU= 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 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= 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/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= 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= @@ -627,11 +506,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -642,66 +518,43 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f h1:8dM0ilqKL0Uzl42GABzzC4Oqlc3kGRILz0vgoff7nwg= 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/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= 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= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -711,11 +564,9 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -723,14 +574,12 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -738,7 +587,6 @@ github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt2 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -746,15 +594,11 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -763,13 +607,10 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -780,7 +621,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/slackhq/nebula v1.5.2 h1:wuIOHsOnrNw3rQx8yPxXiGu8wAtAxxtUI/K8W7Vj7EI= github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIpR52WlE= @@ -789,39 +629,27 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/nosql v0.3.9 h1:YPy5PR3PXClqmpFaVv0wfXDXDc7NXGBE1auyU2c87dc= github.com/smallstep/nosql v0.3.9/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= -github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a h1:AhmOdSHeswKHBjhsLs/7+1voOxT+LLrSk/Nxvk35fug= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= @@ -835,37 +663,28 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/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 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= 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= @@ -880,7 +699,6 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds= go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= @@ -894,12 +712,9 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -929,10 +744,8 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -945,10 +758,8 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -958,7 +769,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1034,7 +844,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1187,16 +996,13 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard/windows v0.5.1 h1:OnYw96PF+CsIMrqWo5QP3Q59q5hY1rFErk/yN3cS+JQ= golang.zx2c4.com/wireguard/windows v0.5.1/go.mod h1:EApyTk/ZNrkbZjurHL1nleDYnsPpJYBO7LZEBCyDAHk= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -1308,7 +1114,6 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= 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= @@ -1324,29 +1129,20 @@ 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= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1367,15 +1163,9 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= From 066bf320866de37f509ee647466b5c9a6666c8ff Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 25 Jan 2022 14:59:55 +0100 Subject: [PATCH 012/241] 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 -// 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 +// HasNames checks if the SSHNameOptions 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 013/241] 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 8ef3abf6d9b449c2e1d8a5b4fa069cecc346dae3 Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Wed, 26 Jan 2022 11:29:21 +0100 Subject: [PATCH 014/241] fix: minus d on Ed --- cas/vaultcas/vaultcas.go | 14 +++++++------- cas/vaultcas/vaultcas_test.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 721b58ba..fce1bdaf 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -29,7 +29,7 @@ type VaultOptions struct { PKIRole string `json:"pkiRole,omitempty"` PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"` - PKIRoleED25519 string `json:"PKIRoleED25519,omitempty"` + PKIRoleEd25519 string `json:"PKIRoleEd25519,omitempty"` RoleID string `json:"roleID,omitempty"` SecretID auth.SecretID `json:"secretID,omitempty"` AppRole string `json:"appRole,omitempty"` @@ -54,13 +54,13 @@ func loadOptions(config json.RawMessage) (vc VaultOptions, err error) { } // pkirole or per key type must be defined - if vc.PKIRole == "" && vc.PKIRoleRSA == "" && vc.PKIRoleEC == "" && vc.PKIRoleED25519 == "" { + if vc.PKIRole == "" && vc.PKIRoleRSA == "" && vc.PKIRoleEC == "" && vc.PKIRoleEd25519 == "" { return vc, errors.New("vaultCAS config options must define `pkiRole`") } // if pkirole is empty all others keys must be set - if vc.PKIRole == "" && (vc.PKIRoleRSA == "" || vc.PKIRoleEC == "" || vc.PKIRoleED25519 == "") { - return vc, errors.New("vaultCAS config options must include a `pkiRole` or `pkiRoleRSA`, `pkiRoleEC` and `pkiRoleEd25519`") + if vc.PKIRole == "" && (vc.PKIRoleRSA == "" || vc.PKIRoleEC == "" || vc.PKIRoleEd25519 == "") { + return vc, errors.New("vaultCAS config options must include a `pkiRole` or `pkiRoleRSA`, `pkiRoleEC` and `PKIRoleEd25519`") } // if pkirole is not empty, use it as default for unset keys @@ -71,8 +71,8 @@ func loadOptions(config json.RawMessage) (vc VaultOptions, err error) { if vc.PKIRoleEC == "" { vc.PKIRoleEC = vc.PKIRole } - if vc.PKIRoleED25519 == "" { - vc.PKIRoleED25519 = vc.PKIRole + if vc.PKIRoleEd25519 == "" { + vc.PKIRoleEd25519 = vc.PKIRole } } @@ -144,7 +144,7 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time. case csr.PublicKeyAlgorithm == x509.ECDSA: vaultPKIRole = v.config.PKIRoleEC case csr.PublicKeyAlgorithm == x509.Ed25519: - vaultPKIRole = v.config.PKIRoleED25519 + vaultPKIRole = v.config.PKIRoleEd25519 default: return nil, nil, errors.Errorf("createCertificate: Unsupported public key algorithm '%v'", csr.PublicKeyAlgorithm) } diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 1d814554..73725b43 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -172,7 +172,7 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { PKIRole: "role", PKIRoleRSA: "rsa", PKIRoleEC: "ec", - PKIRoleED25519: "ed25519", + PKIRoleEd25519: "ed25519", RoleID: "roleID", SecretID: auth.SecretID{FromString: "secretID"}, AppRole: "approle", From b49ac2501bafd77e9ae983f5d608beafdad32904 Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Thu, 27 Jan 2022 11:14:19 +0100 Subject: [PATCH 015/241] feat: enhance options and fix revoke plus more tests --- cas/vaultcas/vaultcas.go | 49 ++-- cas/vaultcas/vaultcas_test.go | 447 +++++++++++++++++++++++++++++++++- 2 files changed, 467 insertions(+), 29 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index fce1bdaf..e503ab28 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -26,7 +26,7 @@ func init() { type VaultOptions struct { PKI string `json:"pki,omitempty"` - PKIRole string `json:"pkiRole,omitempty"` + PKIRoleDefault string `json:"PKIRoleDefault,omitempty"` PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"` PKIRoleEd25519 string `json:"PKIRoleEd25519,omitempty"` @@ -43,45 +43,38 @@ type VaultCAS struct { fingerprint string } -func loadOptions(config json.RawMessage) (vc VaultOptions, err error) { - err = json.Unmarshal(config, &vc) +func loadOptions(config json.RawMessage) (*VaultOptions, error) { + var vc *VaultOptions + + err := json.Unmarshal(config, &vc) if err != nil { - return vc, errors.Wrap(err, "error decoding vaultCAS config") + return nil, errors.Wrap(err, "error decoding vaultCAS config") } if vc.PKI == "" { vc.PKI = "pki" // use default pki vault name } - // pkirole or per key type must be defined - if vc.PKIRole == "" && vc.PKIRoleRSA == "" && vc.PKIRoleEC == "" && vc.PKIRoleEd25519 == "" { - return vc, errors.New("vaultCAS config options must define `pkiRole`") + if vc.PKIRoleDefault == "" { + vc.PKIRoleDefault = "default" // use default pki role name } - // if pkirole is empty all others keys must be set - if vc.PKIRole == "" && (vc.PKIRoleRSA == "" || vc.PKIRoleEC == "" || vc.PKIRoleEd25519 == "") { - return vc, errors.New("vaultCAS config options must include a `pkiRole` or `pkiRoleRSA`, `pkiRoleEC` and `PKIRoleEd25519`") + if vc.PKIRoleRSA == "" { + vc.PKIRoleRSA = vc.PKIRoleDefault } - - // if pkirole is not empty, use it as default for unset keys - if vc.PKIRole != "" { - if vc.PKIRoleRSA == "" { - vc.PKIRoleRSA = vc.PKIRole - } - if vc.PKIRoleEC == "" { - vc.PKIRoleEC = vc.PKIRole - } - if vc.PKIRoleEd25519 == "" { - vc.PKIRoleEd25519 = vc.PKIRole - } + if vc.PKIRoleEC == "" { + vc.PKIRoleEC = vc.PKIRoleDefault + } + if vc.PKIRoleEd25519 == "" { + vc.PKIRoleEd25519 = vc.PKIRoleDefault } if vc.RoleID == "" { - return vc, errors.New("vaultCAS config options must define `roleID`") + return nil, errors.New("vaultCAS config options must define `roleID`") } if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" { - return vc, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") + return nil, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") } if vc.PKI == "" { @@ -237,7 +230,7 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { return &VaultCAS{ client: client, - config: vc, + config: *vc, fingerprint: opts.CertificateAuthorityFingerprint, }, nil } @@ -326,7 +319,11 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv serialNumberDash := certutil.GetHexFormatted(serialNumber, "-") - _, err := v.client.Logical().Write(v.config.PKI+"/revoke/"+serialNumberDash, nil) + y := map[string]interface{}{ + "serial_number": serialNumberDash, + } + + _, err := v.client.Logical().Write(v.config.PKI+"/revoke/", y) if err != nil { return nil, errors.Wrap(err, "unable to revoke certificate") } diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 73725b43..3bf06347 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -1,6 +1,7 @@ package vaultcas import ( + "bytes" "context" "crypto/x509" "encoding/json" @@ -60,7 +61,17 @@ pV/v53oR/ewbtrkHZQkN/amFMLagITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQH MAWCA09LUDAFBgMrZXADQQDJi47MAgl/WKAz+V/kDu1k/zbKk1nrHHAUonbofHUW M6ihSD43+awq3BPeyPbToeH5orSH9l3MuTfbxPb5BVEH -----END CERTIFICATE REQUEST-----` - testRootFingerprint = `e7678acf0d8de731262bce2fe792c48f19547285f5976805125a40867c77464e` + testRootCertificate = `-----BEGIN CERTIFICATE----- +MIIBeDCCAR+gAwIBAgIQcXWWjtSZ/PAyH8D1Ou4L9jAKBggqhkjOPQQDAjAbMRkw +FwYDVQQDExBDbG91ZENBUyBSb290IENBMB4XDTIwMTAyNzIyNTM1NFoXDTMwMTAy +NzIyNTM1NFowGzEZMBcGA1UEAxMQQ2xvdWRDQVMgUm9vdCBDQTBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABIySHA4b78Yu4LuGhZIlv/PhNwXz4ZoV1OUZQ0LrK3vj +B13O12DLZC5uj1z3kxdQzXUttSbtRv49clMpBiTpsZKjRTBDMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSZ+t9RMHbFTl5BatM3 +5bJlHPOu3DAKBggqhkjOPQQDAgNHADBEAiASah6gg0tVM3WI0meCQ4SEKk7Mjhbv ++SmhuZHWV1QlXQIgRXNyWcpVUrAoG6Uy1KQg07LDpF5dFeK9InrDxSJAkVo= +-----END CERTIFICATE-----` + testRootFingerprint = `62e816cbac5c501b7705e18415503852798dfbcd67062f06bcb4af67c290e3c8` ) func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate { @@ -112,6 +123,30 @@ func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} writeJSON(w, cert) return + case r.RequestURI == "/v1/pki/cert/ca": + w.WriteHeader(http.StatusOK) + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testRootCertificate}} + writeJSON(w, cert) + return + case r.RequestURI == "/v1/pki/revoke": + buf := new(bytes.Buffer) + buf.ReadFrom(r.Body) + m := make(map[string]string) + json.Unmarshal(buf.Bytes(), &m) + switch { + case m["serial_number"] == "1c-71-6e-18-cc-f4-70-29-5f-75-ee-64-a8-fe-69-ad": + w.WriteHeader(http.StatusOK) + return + case m["serial_number"] == "01-e2-40": + w.WriteHeader(http.StatusOK) + return + // both + case m["serial_number"] == "01-34-3e": + w.WriteHeader(http.StatusOK) + return + default: + w.WriteHeader(http.StatusNotFound) + } default: w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, `{"error":"not found"}`) @@ -151,7 +186,7 @@ func TestNew_register(t *testing.T) { CertificateAuthorityFingerprint: testRootFingerprint, Config: json.RawMessage(`{ "PKI": "pki", - "PKIRole": "pki-role", + "PKIRoleDefault": "pki-role", "RoleID": "roleID", "SecretID": {"FromString": "secretID"}, "IsWrappingToken": false @@ -169,7 +204,7 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { options := VaultOptions{ PKI: "pki", - PKIRole: "role", + PKIRoleDefault: "role", PKIRoleRSA: "rsa", PKIRoleEC: "ec", PKIRoleEd25519: "ed25519", @@ -216,6 +251,14 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { Certificate: mustParseCertificate(t, testCertificateSigned), CertificateChain: []*x509.Certificate{}, }, false}, + {"fail CSR", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: nil, + Lifetime: time.Hour, + }}, nil, true}, + {"fail lifetime", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: mustParseCertificateRequest(t, testCertificateCsrEc), + Lifetime: 0, + }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -234,3 +277,401 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { }) } } + +func TestVaultCAS_GetCertificateAuthority(t *testing.T) { + caURL, client := testCAHelper(t) + + type fields struct { + client *vault.Client + options VaultOptions + fingerprint string + } + + type args struct { + req *apiv1.GetCertificateAuthorityRequest + } + + options := VaultOptions{ + PKI: "pki", + } + + rootCert, _ := parseCertificate(testRootCertificate) + + tests := []struct { + name string + fields fields + args args + want *apiv1.GetCertificateAuthorityResponse + wantErr bool + }{ + {"ok", fields{client, options, testRootFingerprint}, args{&apiv1.GetCertificateAuthorityRequest{ + Name: caURL.String(), + }}, &apiv1.GetCertificateAuthorityResponse{ + RootCertificate: rootCert, + }, false}, + {"fail fingerprint", fields{client, options, "fail"}, args{&apiv1.GetCertificateAuthorityRequest{ + Name: caURL.String(), + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &VaultCAS{ + client: tt.fields.client, + fingerprint: tt.fields.fingerprint, + config: tt.fields.options, + } + got, err := s.GetCertificateAuthority(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.GetCertificateAuthority() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.GetCertificateAuthority() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVaultCAS_RevokeCertificate(t *testing.T) { + _, client := testCAHelper(t) + + options := VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "approle", + IsWrappingToken: false, + } + + type fields struct { + client *vault.Client + options VaultOptions + } + + type args struct { + req *apiv1.RevokeCertificateRequest + } + + testCrt, _ := parseCertificate(testCertificateSigned) + + tests := []struct { + name string + fields fields + args args + want *apiv1.RevokeCertificateResponse + wantErr bool + }{ + {"ok serial number", fields{client, options}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "123456", + Certificate: nil, + }}, &apiv1.RevokeCertificateResponse{}, false}, + {"ok certificate", fields{client, options}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "", + Certificate: testCrt, + }}, &apiv1.RevokeCertificateResponse{ + Certificate: testCrt, + }, false}, + {"ok both", fields{client, options}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "78910", + Certificate: testCrt, + }}, &apiv1.RevokeCertificateResponse{ + Certificate: testCrt, + }, false}, + {"fail serial string", fields{client, options}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "fail", + Certificate: nil, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &VaultCAS{ + client: tt.fields.client, + config: tt.fields.options, + } + got, err := s.RevokeCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.RevokeCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVaultCAS_RenewCertificate(t *testing.T) { + _, client := testCAHelper(t) + + options := VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "approle", + IsWrappingToken: false, + } + + type fields struct { + client *vault.Client + options VaultOptions + } + + type args struct { + req *apiv1.RenewCertificateRequest + } + + tests := []struct { + name string + fields fields + args args + want *apiv1.RenewCertificateResponse + wantErr bool + }{ + {"not implemented", fields{client, options}, args{&apiv1.RenewCertificateRequest{ + CSR: mustParseCertificateRequest(t, testCertificateCsrEc), + Lifetime: time.Hour, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &VaultCAS{ + client: tt.fields.client, + config: tt.fields.options, + } + got, err := s.RenewCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.RenewCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVaultCAS_loadOptions(t *testing.T) { + tests := []struct { + name string + raw string + want *VaultOptions + wantErr bool + }{ + { + "ok mandatory with SecretID FromString", + `{"RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory with SecretID FromFile", + `{"RoleID": "roleID", "SecretID": {"FromFile": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromFile: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory with SecretID FromEnv", + `{"RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRole PKIRoleEd25519", + `{"PKIRoleDefault": "role", "PKIRoleEd25519": "ed25519" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "role", + PKIRoleEC: "role", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRole PKIRoleEC", + `{"PKIRoleDefault": "role", "PKIRoleEC": "ec" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "role", + PKIRoleEC: "ec", + PKIRoleEd25519: "role", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRole PKIRoleRSA", + `{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "role", + PKIRoleEd25519: "role", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519", + `{"PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519", "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519 with useless PKIRoleDefault", + `{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519", "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory with AppRole", + `{"AppRole": "test", "RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "test", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory with IsWrappingToken", + `{"IsWrappingToken": true, "RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: true, + }, + false, + }, + { + "fail with SecretID FromFail", + `{"RoleID": "roleID", "SecretID": {"FromFail": "secretID"}}`, + nil, + true, + }, + { + "fail with SecretID empty FromEnv", + `{"RoleID": "roleID", "SecretID": {"FromEnv": ""}}`, + nil, + true, + }, + { + "fail with SecretID empty FromFile", + `{"RoleID": "roleID", "SecretID": {"FromFile": ""}}`, + nil, + true, + }, + { + "fail with SecretID empty FromString", + `{"RoleID": "roleID", "SecretID": {"FromString": ""}}`, + nil, + true, + }, + { + "fail mandatory with SecretID FromFail", + `{"RoleID": "roleID", "SecretID": {"FromFail": "secretID"}}`, + nil, + true, + }, + { + "fail missing RoleID", + `{"SecretID": {"FromString": "secretID"}}`, + nil, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := loadOptions(json.RawMessage(tt.raw)) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.loadOptions() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.loadOptions() = %v, want %v", got, tt.want) + } + }) + } +} From 782ff769636568644b505601dcd8621adf35df72 Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Thu, 27 Jan 2022 11:19:31 +0100 Subject: [PATCH 016/241] fix: apply suggestion to use cr only --- cas/vaultcas/vaultcas.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index e503ab28..234ea820 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -10,7 +10,6 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/cas/apiv1" vault "github.com/hashicorp/vault/api" @@ -129,22 +128,21 @@ func parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) { func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.Duration) (*x509.Certificate, []*x509.Certificate, error) { var vaultPKIRole string - csr := api.CertificateRequest{CertificateRequest: cr} switch { - case csr.PublicKeyAlgorithm == x509.RSA: + case cr.PublicKeyAlgorithm == x509.RSA: vaultPKIRole = v.config.PKIRoleRSA - case csr.PublicKeyAlgorithm == x509.ECDSA: + case cr.PublicKeyAlgorithm == x509.ECDSA: vaultPKIRole = v.config.PKIRoleEC - case csr.PublicKeyAlgorithm == x509.Ed25519: + case cr.PublicKeyAlgorithm == x509.Ed25519: vaultPKIRole = v.config.PKIRoleEd25519 default: - return nil, nil, errors.Errorf("createCertificate: Unsupported public key algorithm '%v'", csr.PublicKeyAlgorithm) + return nil, nil, errors.Errorf("createCertificate: Unsupported public key algorithm '%v'", cr.PublicKeyAlgorithm) } - certPemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw}) + certPemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: cr.Raw}) if certPemBytes == nil { - return nil, nil, errors.Errorf("createCertificate: Failed to encode pem '%v'", csr.Raw) + return nil, nil, errors.Errorf("createCertificate: Failed to encode pem '%v'", cr.Raw) } y := map[string]interface{}{ From 9617edf0c2b160f1fbd3e2835e2a40855ee5644c Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 27 Jan 2022 17:18:33 +0100 Subject: [PATCH 017/241] 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 + } + 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, 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,32 +16,38 @@ 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/empty-label", - constraint: "..local", + name: "fail/wildcard-partial-label", + constraint: "*xxxx.local", want: "", wantErr: true, }, { - name: "fail/empty-reverse", - constraint: ".", + name: "fail/wildcard-in-the-middle", + constraint: "x.*.local", want: "", wantErr: true, }, { - name: "false/idna-internationalized-domain-name", - constraint: ".例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/ + name: "fail/empty-label", + constraint: "..local", 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/empty-reverse", + constraint: ".", + want: "", + wantErr: true, + }, + { + 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 018/241] 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 019/241] 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 ab5197500c2545d74283d5896e1c57488ea250a9 Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Sun, 6 Feb 2022 23:29:49 +0100 Subject: [PATCH 020/241] fix: a certificat must excldue the root and you should use verified chained intermediate --- cas/vaultcas/vaultcas.go | 120 +++++++++++++++---- cas/vaultcas/vaultcas_test.go | 30 +++-- go.sum | 210 ++++++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+), 38 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 234ea820..a076acbf 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -1,6 +1,7 @@ package vaultcas import ( + "bytes" "context" "crypto/sha256" "crypto/x509" @@ -42,6 +43,12 @@ type VaultCAS struct { fingerprint string } +type Certificate struct { + leaf *x509.Certificate + intermediates []*x509.Certificate + root *x509.Certificate +} + func loadOptions(config json.RawMessage) (*VaultOptions, error) { var vc *VaultOptions @@ -87,31 +94,94 @@ func loadOptions(config json.RawMessage) (*VaultOptions, error) { return vc, nil } -func getCertificateAndChain(certb certutil.CertBundle) (*x509.Certificate, []*x509.Certificate, error) { - cert, err := parseCertificate(certb.Certificate) - if err != nil { - return nil, nil, err +func certificateSort(n []*x509.Certificate) bool { + // sort all cert using bubble sort + isSorted := false + s := 0 + maxSwap := len(n) * (len(n) - 1) / 2 + for s <= maxSwap && !isSorted { + isSorted = true + var i = 0 + for i < len(n)-1 { + if !isSignedBy(n[i], n[i+1]) { + // swap + n[i], n[i+1] = n[i+1], n[i] + isSorted = false + } + i++ + } + s++ + } + return isSorted +} + +func isSignedBy(i *x509.Certificate, j *x509.Certificate) bool { + signer := x509.NewCertPool() + signer.AddCert(j) + + opts := x509.VerifyOptions{ + Roots: signer, + Intermediates: x509.NewCertPool(), // set empty to avoid using system CA } - chain := make([]*x509.Certificate, len(certb.CAChain)) - for i := range certb.CAChain { - chain[i], err = parseCertificate(certb.CAChain[i]) + _, err := i.Verify(opts) + return err == nil +} + +func parseCertificates(pemCert string) []*x509.Certificate { + var certs []*x509.Certificate + rest := []byte(pemCert) + var block *pem.Block + for { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return nil, nil, err + break } + certs = append(certs, cert) } - return cert, chain, nil + return certs } -func parseCertificate(pemCert string) (*x509.Certificate, error) { - block, _ := pem.Decode([]byte(pemCert)) - if block == nil { - return nil, errors.Errorf("error decoding certificate: not a valid PEM encoded block, please verify\r\n%v", pemCert) +func getCertificateAndChain(certb certutil.CertBundle) (*Certificate, error) { + // certutil.CertBundle contains CAChain and Certificate. + // Both could have a common part or different and we are not sure + // how user define their chain inside vault. + // We will create an array of certificate with all parsed certificates + // then sort the array to create a consistent chain + var root *x509.Certificate + var leaf *x509.Certificate + intermediates := make([]*x509.Certificate, 0) + used := make(map[string]bool) // ensure that intermediate are uniq + chains := append(certb.CAChain, []string{certb.Certificate}...) + for _, chain := range chains { + for _, cert := range parseCertificates(chain) { + if used[cert.SerialNumber.String()] == true { + continue + } + used[cert.SerialNumber.String()] = true + if cert.IsCA && bytes.Equal(cert.RawIssuer, cert.RawSubject) { + root = cert + } else if !cert.IsCA { + leaf = cert + } else { + intermediates = append(intermediates, cert) + } + } } - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, errors.Wrap(err, "error parsing certificate") + if ok := certificateSort(intermediates); !ok { + return nil, errors.Errorf("failed to sort certificate, probably one of cert is not part of the chain") } - return cert, nil + + certificate := &Certificate{ + root: root, + leaf: leaf, + intermediates: intermediates, + } + + return certificate, nil } func parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) { @@ -119,6 +189,7 @@ func parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) { if block == nil { return nil, errors.Errorf("error decoding certificate request: not a valid PEM encoded block, please verify\r\n%v", pemCsr) } + cr, err := x509.ParseCertificateRequest(block.Bytes) if err != nil { return nil, errors.Wrap(err, "error parsing certificate request") @@ -171,8 +242,13 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time. return nil, nil, err } + cert, err := getCertificateAndChain(certBundle) + if err != nil { + return nil, nil, err + } + // Return certificate and certificate chain - return getCertificateAndChain(certBundle) + return cert.leaf, cert.intermediates, nil } // New creates a new CertificateAuthorityService implementation @@ -254,7 +330,7 @@ func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv } func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { - secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca") + secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca_chain") if err != nil { return nil, errors.Wrap(err, "unable to read root") } @@ -274,19 +350,19 @@ func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityReq return nil, err } - cert, _, err := getCertificateAndChain(certBundle) + cert, err := getCertificateAndChain(certBundle) if err != nil { return nil, err } - sha256Sum := sha256.Sum256(cert.Raw) + sha256Sum := sha256.Sum256(cert.root.Raw) expectedSum := certutil.GetHexFormatted(sha256Sum[:], "") if expectedSum != v.fingerprint { return nil, errors.Errorf("Vault Root CA fingerprint `%s` doesn't match config fingerprint `%v`", expectedSum, v.fingerprint) } return &apiv1.GetCertificateAuthorityResponse{ - RootCertificate: cert, + RootCertificate: cert.root, }, nil } diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 3bf06347..049b68d1 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -76,10 +76,7 @@ AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSZ+t9RMHbFTl5BatM3 func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate { t.Helper() - crt, err := parseCertificate(pemCert) - if err != nil { - t.Fatal(err) - } + crt := parseCertificates(pemCert)[0] return crt } @@ -123,9 +120,9 @@ func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} writeJSON(w, cert) return - case r.RequestURI == "/v1/pki/cert/ca": + case r.RequestURI == "/v1/pki/cert/ca_chain": w.WriteHeader(http.StatusOK) - cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testRootCertificate}} + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned + "\n" + testRootCertificate}} writeJSON(w, cert) return case r.RequestURI == "/v1/pki/revoke": @@ -251,14 +248,14 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { Certificate: mustParseCertificate(t, testCertificateSigned), CertificateChain: []*x509.Certificate{}, }, false}, - {"fail CSR", fields{client, options}, args{&apiv1.CreateCertificateRequest{ - CSR: nil, - Lifetime: time.Hour, - }}, nil, true}, - {"fail lifetime", fields{client, options}, args{&apiv1.CreateCertificateRequest{ - CSR: mustParseCertificateRequest(t, testCertificateCsrEc), - Lifetime: 0, - }}, nil, true}, + // {"fail CSR", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + // CSR: nil, + // Lifetime: time.Hour, + // }}, nil, true}, + // {"fail lifetime", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + // CSR: mustParseCertificateRequest(t, testCertificateCsrEc), + // Lifetime: 0, + // }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -271,6 +268,7 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { t.Errorf("VaultCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("VaultCAS.CreateCertificate() = %v, want %v", got, tt.want) } @@ -295,7 +293,7 @@ func TestVaultCAS_GetCertificateAuthority(t *testing.T) { PKI: "pki", } - rootCert, _ := parseCertificate(testRootCertificate) + rootCert := parseCertificates(testRootCertificate)[0] tests := []struct { name string @@ -356,7 +354,7 @@ func TestVaultCAS_RevokeCertificate(t *testing.T) { req *apiv1.RevokeCertificateRequest } - testCrt, _ := parseCertificate(testCertificateSigned) + testCrt := parseCertificates(testCertificateSigned)[0] tests := []struct { name string diff --git a/go.sum b/go.sum index f0d9f2fa..d42da957 100644 --- a/go.sum +++ b/go.sum @@ -25,18 +25,23 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -67,9 +72,13 @@ github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -82,35 +91,46 @@ github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmy github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -121,28 +141,37 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= 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-v2 v0.18.0 h1:qZ+woO4SamnH/eEbjM2IDLhRNwIwND/RQyVlBLp3Jqg= 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= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -150,21 +179,34 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158 h1:CevA8fI91PAnP8vpnXuB8ZYAZ5wqY86nAbxfgK8tWO4= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= @@ -172,7 +214,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -185,6 +229,7 @@ github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70d github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= @@ -195,9 +240,13 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -207,35 +256,48 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs= 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 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= 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/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 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= 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/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= 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= github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap/v3 v3.1.10 h1:7WsKqasmPThNvdl0Q5GPpbTDD/ZD98CfuawrMIuh7qQ= github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -251,11 +313,14 @@ 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= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 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= @@ -295,6 +360,7 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -309,11 +375,15 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -326,7 +396,9 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22 h1:ub2sxhs2A0HRa2dWHavvmWxiVGXNfE9wI+gcTMwED8A= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -336,19 +408,29 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 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= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda h1:5ikpG9mYCMFiZX0nkxoV6aU2IpCHPdws3gCNgdZeEV0= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.3.0 h1:HXNYlRkkM/t+Y/Yhxtwcy02dlYwIaoxzvxPnS+cqy78= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0 h1:UOxjlb4xVNF93jak1mzzoBatyFju9nrkxpVwIp/QqxQ= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -364,7 +446,9 @@ github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -377,18 +461,22 @@ github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ= github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/password v0.1.1 h1:6JzmBqXprakgFEHwBgdchsjaA9x3GyjdI568bXKxa60= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1 h1:Yc026VyMyIpq1UWRnakHRG01U8fJm+nEfEmjoAb00n8= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -396,6 +484,7 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -403,9 +492,13 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO2aRM= @@ -416,60 +509,84 @@ github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZdu github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0 h1:0U6+BtN6LhaYuTnIJq4Wyq5cpn6O2kWrxAtcqBmYY6w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 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/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/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 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= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= @@ -486,16 +603,20 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU= 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 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= 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/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= 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= @@ -506,8 +627,11 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -518,43 +642,66 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f h1:8dM0ilqKL0Uzl42GABzzC4Oqlc3kGRILz0vgoff7nwg= 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/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= 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= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -564,9 +711,11 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -574,12 +723,14 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -587,6 +738,7 @@ github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt2 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -594,11 +746,15 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -607,10 +763,13 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -621,6 +780,7 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/slackhq/nebula v1.5.2 h1:wuIOHsOnrNw3rQx8yPxXiGu8wAtAxxtUI/K8W7Vj7EI= github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIpR52WlE= @@ -629,27 +789,39 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/nosql v0.3.9 h1:YPy5PR3PXClqmpFaVv0wfXDXDc7NXGBE1auyU2c87dc= github.com/smallstep/nosql v0.3.9/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a h1:AhmOdSHeswKHBjhsLs/7+1voOxT+LLrSk/Nxvk35fug= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= @@ -663,28 +835,37 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/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 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= 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= @@ -699,6 +880,7 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds= go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= @@ -712,9 +894,12 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -744,8 +929,10 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -758,8 +945,10 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -769,6 +958,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -844,6 +1034,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -996,13 +1187,16 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/windows v0.5.1 h1:OnYw96PF+CsIMrqWo5QP3Q59q5hY1rFErk/yN3cS+JQ= golang.zx2c4.com/wireguard/windows v0.5.1/go.mod h1:EApyTk/ZNrkbZjurHL1nleDYnsPpJYBO7LZEBCyDAHk= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -1114,6 +1308,7 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= 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= @@ -1129,20 +1324,29 @@ 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= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1163,9 +1367,15 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= From a9550a746fbc3895d859b7d9ed6949407af65c70 Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Sun, 6 Feb 2022 23:35:17 +0100 Subject: [PATCH 021/241] fix: add back commented tests --- cas/vaultcas/vaultcas_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 049b68d1..1febf1ce 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -248,14 +248,14 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { Certificate: mustParseCertificate(t, testCertificateSigned), CertificateChain: []*x509.Certificate{}, }, false}, - // {"fail CSR", fields{client, options}, args{&apiv1.CreateCertificateRequest{ - // CSR: nil, - // Lifetime: time.Hour, - // }}, nil, true}, - // {"fail lifetime", fields{client, options}, args{&apiv1.CreateCertificateRequest{ - // CSR: mustParseCertificateRequest(t, testCertificateCsrEc), - // Lifetime: 0, - // }}, nil, true}, + {"fail CSR", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: nil, + Lifetime: time.Hour, + }}, nil, true}, + {"fail lifetime", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: mustParseCertificateRequest(t, testCertificateCsrEc), + Lifetime: 0, + }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From b3316c4a5601ba7ca271bc4abdbf85837ffa0614 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Feb 2022 17:17:32 -0800 Subject: [PATCH 022/241] Refactor json Marshal+Unmarshal in one function. --- cas/vaultcas/vaultcas.go | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index a076acbf..4b01c486 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -158,7 +158,7 @@ func getCertificateAndChain(certb certutil.CertBundle) (*Certificate, error) { chains := append(certb.CAChain, []string{certb.Certificate}...) for _, chain := range chains { for _, cert := range parseCertificates(chain) { - if used[cert.SerialNumber.String()] == true { + if used[cert.SerialNumber.String()] { continue } used[cert.SerialNumber.String()] = true @@ -231,15 +231,8 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time. } var certBundle certutil.CertBundle - - secretData, err := json.Marshal(secret.Data) - if err != nil { - return nil, nil, err - } - - err = json.Unmarshal(secretData, &certBundle) - if err != nil { - return nil, nil, err + if err := unmarshalMap(secret.Data, &certBundle); err != nil { + return nil, nil, errors.Wrap(err, "error unmarshaling cert bundle") } cert, err := getCertificateAndChain(certBundle) @@ -339,15 +332,8 @@ func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityReq } var certBundle certutil.CertBundle - - secretData, err := json.Marshal(secret.Data) - if err != nil { - return nil, err - } - - err = json.Unmarshal(secretData, &certBundle) - if err != nil { - return nil, err + if err := unmarshalMap(secret.Data, &certBundle); err != nil { + return nil, errors.Wrap(err, "error unmarshaling cert bundle") } cert, err := getCertificateAndChain(certBundle) @@ -407,3 +393,12 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv CertificateChain: nil, }, nil } + +func unmarshalMap(m map[string]interface{}, v interface{}) error { + b, err := json.Marshal(m) + if err != nil { + return err + } + + return json.Unmarshal(b, v) +} From d42415920030ee9522dee2e8d716a5e334efef46 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Feb 2022 18:09:20 -0800 Subject: [PATCH 023/241] Fix certificate type identification --- cas/vaultcas/vaultcas.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 4b01c486..1cebcc8c 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -1,7 +1,6 @@ package vaultcas import ( - "bytes" "context" "crypto/sha256" "crypto/x509" @@ -162,12 +161,12 @@ func getCertificateAndChain(certb certutil.CertBundle) (*Certificate, error) { continue } used[cert.SerialNumber.String()] = true - if cert.IsCA && bytes.Equal(cert.RawIssuer, cert.RawSubject) { + if isRoot(cert) { root = cert - } else if !cert.IsCA { - leaf = cert - } else { + } else if cert.BasicConstraintsValid && cert.IsCA { intermediates = append(intermediates, cert) + } else { + leaf = cert } } } @@ -402,3 +401,11 @@ func unmarshalMap(m map[string]interface{}, v interface{}) error { return json.Unmarshal(b, v) } + +// isRoot returns true if the given certificate is a root certificate. +func isRoot(cert *x509.Certificate) bool { + if cert.BasicConstraintsValid && cert.IsCA { + return cert.CheckSignatureFrom(cert) == nil + } + return false +} From ae7b41a12c48aa16d1250f8ff260d68cd84710bb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Feb 2022 18:33:33 -0800 Subject: [PATCH 024/241] Fix linter errors. --- cas/vaultcas/vaultcas.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 1cebcc8c..4d7e220d 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -114,7 +114,7 @@ func certificateSort(n []*x509.Certificate) bool { return isSorted } -func isSignedBy(i *x509.Certificate, j *x509.Certificate) bool { +func isSignedBy(i, j *x509.Certificate) bool { signer := x509.NewCertPool() signer.AddCert(j) @@ -154,18 +154,18 @@ func getCertificateAndChain(certb certutil.CertBundle) (*Certificate, error) { var leaf *x509.Certificate intermediates := make([]*x509.Certificate, 0) used := make(map[string]bool) // ensure that intermediate are uniq - chains := append(certb.CAChain, []string{certb.Certificate}...) - for _, chain := range chains { + for _, chain := range append(certb.CAChain, certb.Certificate) { for _, cert := range parseCertificates(chain) { if used[cert.SerialNumber.String()] { continue } used[cert.SerialNumber.String()] = true - if isRoot(cert) { + switch { + case isRoot(cert): root = cert - } else if cert.BasicConstraintsValid && cert.IsCA { + case cert.BasicConstraintsValid && cert.IsCA: intermediates = append(intermediates, cert) - } else { + default: leaf = cert } } From 7c541888ad281e0c6f669ce3963c0f305ac84a62 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 8 Mar 2022 13:26:07 +0100 Subject: [PATCH 025/241] 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 | 80 +++++----- authority/provisioner/x5c.go | 13 +- authority/ssh.go | 40 +++++ authority/tls.go | 19 +++ 21 files changed, 509 insertions(+), 287 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,63 +37,63 @@ 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"` -} - -// 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"` + Host *policy.SSHHostCertificateOptions } -// 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"` +// 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 } -// HasTemplate returns true if a template is defined in the provisioner options. -func (o *SSHOptions) HasTemplate() bool { - return o != nil && (o.Template != "" || o.TemplateFile != "") +// 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 } -// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the -// names that a provisioner is authorized to sign SSH certificates for. -func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions { +// GetAllowedHostNameOptions returns the SSHNameOptions that are +// allowed when SSH host certificates are requested. +func (o *SSHOptions) GetAllowedHostNameOptions() *policy.SSHNameOptions { if o == nil { return nil } - return o.AllowedNames + if o.Host == nil { + return nil + } + return o.Host.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 { +// GetDeniedHostNameOptions returns the SSHNameOptions that are +// denied when SSH host certificates are requested. +func (o *SSHOptions) GetDeniedHostNameOptions() *policy.SSHNameOptions { if o == nil { return nil } - return o.DeniedNames + if o.Host == nil { + return nil + } + return o.Host.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 +// HasTemplate returns true if a template is defined in the provisioner options. +func (o *SSHOptions) HasTemplate() bool { + return o != nil && (o.Template != "" || o.TemplateFile != "") } // TemplateSSHOptions generates a SSHCertificateOptions with the template and 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 026/241] 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 027/241] 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 028/241] 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 + } + + if err := par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil { + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy")) return } - newPolicy, err := par.auth.GetAuthorityPolicy(ctx) + 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" - "github.com/smallstep/certificates/authority/admin" - "github.com/smallstep/certificates/authority/policy" "go.step.sm/linkedca" + + "github.com/smallstep/certificates/authority/admin" + 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 b401376829199e2196181c84781a4f72e0acd9dc Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Mar 2022 19:21:40 -0700 Subject: [PATCH 029/241] Add current provisioner to AuthorizeSign SignOptions. The original provisioner cannot be retrieved from a certificate if a linked ra is used. --- authority/provisioner/acme.go | 1 + authority/provisioner/aws.go | 1 + authority/provisioner/azure.go | 1 + authority/provisioner/gcp.go | 1 + authority/provisioner/jwk.go | 1 + authority/provisioner/k8sSA.go | 1 + authority/provisioner/nebula.go | 1 + authority/provisioner/noop.go | 2 +- authority/provisioner/oidc.go | 1 + authority/provisioner/scep.go | 1 + authority/provisioner/x5c.go | 1 + 11 files changed, 11 insertions(+), 1 deletion(-) diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 913d0ace..b5d806ab 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -89,6 +89,7 @@ func (p *ACME) Init(config Config) (err error) { // on the resulting certificate. func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { return []SignOption{ + p, // modifiers / withOptions newProvisionerExtensionOption(TypeACME, p.Name, ""), newForceCNOption(p.ForceCN), diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 5f79d7d0..9d27e016 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -467,6 +467,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } return append(so, + p, templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID), diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index d9654566..58ce47b3 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -349,6 +349,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } return append(so, + p, templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID), diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 6070b640..69d909a2 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -262,6 +262,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } return append(so, + p, templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName), diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index c014bec0..3c5032fb 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -170,6 +170,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } return []SignOption{ + p, templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID), diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 557d571a..083773e0 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -231,6 +231,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } return []SignOption{ + p, templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeK8sSA, p.Name, ""), diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index 1a6eee3e..4216e997 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -144,6 +144,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } return []SignOption{ + p, templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeNebula, p.Name, ""), diff --git a/authority/provisioner/noop.go b/authority/provisioner/noop.go index 1709fbca..39661e54 100644 --- a/authority/provisioner/noop.go +++ b/authority/provisioner/noop.go @@ -38,7 +38,7 @@ func (p *noop) Init(config Config) error { } func (p *noop) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { - return []SignOption{}, nil + return []SignOption{p}, nil } func (p *noop) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 1fc9bb4b..3a9398a2 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -345,6 +345,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e } return []SignOption{ + o, templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID), diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index f4cffd78..9dc1edd8 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -121,6 +121,7 @@ func (s *SCEP) Init(config Config) (err error) { // on the resulting certificate. func (s *SCEP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { return []SignOption{ + s, // modifiers / withOptions newProvisionerExtensionOption(TypeSCEP, s.Name, ""), newForceCNOption(s.ForceCN), diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 6f534c76..4f3e5899 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -218,6 +218,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } return []SignOption{ + p, templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeX5C, p.Name, ""), From 9d027c17d0a6154c3784df8b5b5fa8b425fc6f73 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Mar 2022 19:24:05 -0700 Subject: [PATCH 030/241] Send current provisioner on PostCertificate --- authority/linkedca.go | 15 ++++++++++++++- authority/tls.go | 24 +++++++++++++++++++----- go.mod | 2 +- go.sum | 5 ++--- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/authority/linkedca.go b/authority/linkedca.go index b568dcbb..00d5ceef 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -15,6 +15,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" @@ -228,12 +229,13 @@ func (c *linkedCaClient) DeleteAdmin(ctx context.Context, id string) error { return errors.Wrap(err, "error deleting admin") } -func (c *linkedCaClient) StoreCertificateChain(fullchain ...*x509.Certificate) error { +func (c *linkedCaClient) StoreCertificateChain(prov provisioner.Interface, fullchain ...*x509.Certificate) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _, err := c.client.PostCertificate(ctx, &linkedca.CertificateRequest{ PemCertificate: serializeCertificateChain(fullchain[0]), PemCertificateChain: serializeCertificateChain(fullchain[1:]...), + Provisioner: createProvisionerIdentity(prov), }) return errors.Wrap(err, "error posting certificate") } @@ -310,6 +312,17 @@ func (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) { return resp.Status != linkedca.RevocationStatus_ACTIVE, nil } +func createProvisionerIdentity(prov provisioner.Interface) *linkedca.ProvisionerIdentity { + if prov == nil { + return nil + } + return &linkedca.ProvisionerIdentity{ + Id: prov.GetID(), + Type: linkedca.Provisioner_Type(prov.GetType()), + Name: prov.GetName(), + } +} + func serializeCertificate(crt *x509.Certificate) string { if crt == nil { return "" diff --git a/authority/tls.go b/authority/tls.go index 58a1247c..93bc0408 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -89,8 +89,13 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Set backdate with the configured value signOpts.Backdate = a.config.AuthorityConfig.Backdate.Duration + var prov provisioner.Interface for _, op := range extraOpts { switch k := op.(type) { + // Capture current provisioner + case provisioner.Interface: + prov = k + // Adds new options to NewCertificate case provisioner.CertificateOptions: certOptions = append(certOptions, k.Options(signOpts)...) @@ -204,7 +209,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } fullchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...) - if err = a.storeCertificate(fullchain); err != nil { + if err = a.storeCertificate(prov, fullchain); err != nil { if err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error storing certificate in db", opts...) @@ -325,19 +330,28 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 // TODO: at some point we should replace the db.AuthDB interface to implement // `StoreCertificate(...*x509.Certificate) error` instead of just // `StoreCertificate(*x509.Certificate) error`. -func (a *Authority) storeCertificate(fullchain []*x509.Certificate) error { +func (a *Authority) storeCertificate(prov provisioner.Interface, fullchain []*x509.Certificate) error { + type linkedChainStorer interface { + StoreCertificateChain(provisioner.Interface, ...*x509.Certificate) error + } type certificateChainStorer interface { StoreCertificateChain(...*x509.Certificate) error } // Store certificate in linkedca - if s, ok := a.adminDB.(certificateChainStorer); ok { + switch s := a.adminDB.(type) { + case linkedChainStorer: + return s.StoreCertificateChain(prov, fullchain...) + case certificateChainStorer: return s.StoreCertificateChain(fullchain...) } + // Store certificate in local db - if s, ok := a.db.(certificateChainStorer); ok { + switch s := a.db.(type) { + case certificateChainStorer: return s.StoreCertificateChain(fullchain...) + default: + return a.db.StoreCertificate(fullchain[0]) } - return a.db.StoreCertificate(fullchain[0]) } // storeRenewedCertificate allows to use an extension of the db.AuthDB interface diff --git a/go.mod b/go.mod index 6033d05e..aad8e124 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 c7a18aad..f124df98 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= @@ -685,8 +686,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.3 h1:f3GMl+aCydt294BZRjTYwpaXRqwwndvoTY2NLN4wu10= go.step.sm/crypto v0.15.3/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.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 6b620c8e9c844f66d4f41cd5a1796c48e38086aa Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 24 Mar 2022 10:54:45 +0100 Subject: [PATCH 031/241] 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 032/241] 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 033/241] 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 034/241] 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 17d7fd70cd16c06dc523033f98eda1ffdb7fb9d1 Mon Sep 17 00:00:00 2001 From: Panagiotis Siatras Date: Tue, 22 Mar 2022 14:31:18 +0200 Subject: [PATCH 035/241] api/log: initial implementation of the package (#859) * api/log: initial implementation of the package * api: refactored to support api/log * scep/api: refactored to support api/log * api/log: documented the package * api: moved log-related tests to api/log --- api/errors.go | 3 ++- api/log/log.go | 47 +++++++++++++++++++++++++++++++++++++ api/log/log_test.go | 44 +++++++++++++++++++++++++++++++++++ api/utils.go | 56 ++++++++++----------------------------------- api/utils_test.go | 35 ---------------------------- scep/api/api.go | 12 +++++----- 6 files changed, 111 insertions(+), 86 deletions(-) create mode 100644 api/log/log.go create mode 100644 api/log/log_test.go diff --git a/api/errors.go b/api/errors.go index 522fa955..49efd486 100644 --- a/api/errors.go +++ b/api/errors.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/api/log" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" @@ -60,6 +61,6 @@ func WriteError(w http.ResponseWriter, err error) { } if err := json.NewEncoder(w).Encode(err); err != nil { - LogError(w, err) + log.Error(w, err) } } diff --git a/api/log/log.go b/api/log/log.go new file mode 100644 index 00000000..78dae506 --- /dev/null +++ b/api/log/log.go @@ -0,0 +1,47 @@ +// Package log implements API-related logging helpers. +package log + +import ( + "log" + "net/http" + + "github.com/smallstep/certificates/logging" +) + +// Error adds to the response writer the given error if it implements +// logging.ResponseLogger. If it does not implement it, then writes the error +// using the log package. +func Error(rw http.ResponseWriter, err error) { + if rl, ok := rw.(logging.ResponseLogger); ok { + rl.WithFields(map[string]interface{}{ + "error": err, + }) + } else { + log.Println(err) + } +} + +// EnabledResponse log the response object if it implements the EnableLogger +// interface. +func EnabledResponse(rw http.ResponseWriter, v interface{}) { + type enableLogger interface { + ToLog() (interface{}, error) + } + + if el, ok := v.(enableLogger); ok { + out, err := el.ToLog() + if err != nil { + Error(rw, err) + + return + } + + if rl, ok := rw.(logging.ResponseLogger); ok { + rl.WithFields(map[string]interface{}{ + "response": out, + }) + } else { + log.Println(out) + } + } +} diff --git a/api/log/log_test.go b/api/log/log_test.go new file mode 100644 index 00000000..fcd3ea2b --- /dev/null +++ b/api/log/log_test.go @@ -0,0 +1,44 @@ +package log + +import ( + "errors" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/smallstep/certificates/logging" +) + +func TestError(t *testing.T) { + theError := errors.New("the error") + + type args struct { + rw http.ResponseWriter + err error + } + tests := []struct { + name string + args args + withFields bool + }{ + {"normalLogger", args{httptest.NewRecorder(), theError}, false}, + {"responseLogger", args{logging.NewResponseLogger(httptest.NewRecorder()), theError}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Error(tt.args.rw, tt.args.err) + if tt.withFields { + if rl, ok := tt.args.rw.(logging.ResponseLogger); ok { + fields := rl.Fields() + if !reflect.DeepEqual(fields["error"], theError) { + t.Errorf("ResponseLogger[\"error\"] = %s, wants %s", fields["error"], theError) + } + } else { + t.Error("ResponseWriter does not implement logging.ResponseLogger") + } + } + }) + } +} diff --git a/api/utils.go b/api/utils.go index 9daa0cd2..e3fcc9c4 100644 --- a/api/utils.go +++ b/api/utils.go @@ -2,52 +2,14 @@ package api import ( "encoding/json" - "log" "net/http" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - "github.com/smallstep/certificates/logging" + "github.com/smallstep/certificates/api/log" ) -// EnableLogger is an interface that enables response logging for an object. -type EnableLogger interface { - ToLog() (interface{}, error) -} - -// LogError adds to the response writer the given error if it implements -// logging.ResponseLogger. If it does not implement it, then writes the error -// using the log package. -func LogError(rw http.ResponseWriter, err error) { - if rl, ok := rw.(logging.ResponseLogger); ok { - rl.WithFields(map[string]interface{}{ - "error": err, - }) - } else { - log.Println(err) - } -} - -// LogEnabledResponse log the response object if it implements the EnableLogger -// interface. -func LogEnabledResponse(rw http.ResponseWriter, v interface{}) { - if el, ok := v.(EnableLogger); ok { - out, err := el.ToLog() - if err != nil { - LogError(rw, err) - return - } - if rl, ok := rw.(logging.ResponseLogger); ok { - rl.WithFields(map[string]interface{}{ - "response": out, - }) - } else { - log.Println(out) - } - } -} - // JSON writes the passed value into the http.ResponseWriter. func JSON(w http.ResponseWriter, v interface{}) { JSONStatus(w, v, http.StatusOK) @@ -59,10 +21,12 @@ func JSONStatus(w http.ResponseWriter, v interface{}, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { - LogError(w, err) + log.Error(w, err) + return } - LogEnabledResponse(w, v) + + log.EnabledResponse(w, v) } // ProtoJSON writes the passed value into the http.ResponseWriter. @@ -78,12 +42,16 @@ func ProtoJSONStatus(w http.ResponseWriter, m proto.Message, status int) { b, err := protojson.Marshal(m) if err != nil { - LogError(w, err) + log.Error(w, err) + return } + if _, err := w.Write(b); err != nil { - LogError(w, err) + log.Error(w, err) + return } - //LogEnabledResponse(w, v) + + // log.EnabledResponse(w, v) } diff --git a/api/utils_test.go b/api/utils_test.go index 12350c97..f5e1e1cb 100644 --- a/api/utils_test.go +++ b/api/utils_test.go @@ -3,46 +3,11 @@ package api import ( "net/http" "net/http/httptest" - "reflect" "testing" - "github.com/pkg/errors" - "github.com/smallstep/certificates/logging" ) -func TestLogError(t *testing.T) { - theError := errors.New("the error") - type args struct { - rw http.ResponseWriter - err error - } - tests := []struct { - name string - args args - withFields bool - }{ - {"normalLogger", args{httptest.NewRecorder(), theError}, false}, - {"responseLogger", args{logging.NewResponseLogger(httptest.NewRecorder()), theError}, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - LogError(tt.args.rw, tt.args.err) - if tt.withFields { - if rl, ok := tt.args.rw.(logging.ResponseLogger); ok { - fields := rl.Fields() - if !reflect.DeepEqual(fields["error"], theError) { - t.Errorf("ResponseLogger[\"error\"] = %s, wants %s", fields["error"], theError) - } - } else { - t.Error("ResponseWriter does not implement logging.ResponseLogger") - } - } - }) - } -} - func TestJSON(t *testing.T) { type args struct { rw http.ResponseWriter diff --git a/scep/api/api.go b/scep/api/api.go index 77c683ee..a326ea92 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -10,14 +10,14 @@ import ( "strings" "github.com/go-chi/chi" + microscep "github.com/micromdm/scep/v2/scep" + "github.com/pkg/errors" + "go.mozilla.org/pkcs7" + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/api/log" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/scep" - "go.mozilla.org/pkcs7" - - "github.com/pkg/errors" - - microscep "github.com/micromdm/scep/v2/scep" ) const ( @@ -337,7 +337,7 @@ func formatCapabilities(caps []string) []byte { func writeSCEPResponse(w http.ResponseWriter, response SCEPResponse) { if response.Error != nil { - api.LogError(w, response.Error) + log.Error(w, response.Error) } if response.Certificate != nil { From 76e53479238705a85986576ecffb1f330c141f2e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 23 Mar 2022 23:14:04 +0100 Subject: [PATCH 036/241] Add armv5 build to GoReleaser configuration --- .goreleaser.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index 207c75bd..cd4826c9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -19,6 +19,7 @@ builds: - linux_386 - linux_amd64 - linux_arm64 + - linux_arm_5 - linux_arm_6 - linux_arm_7 - windows_amd64 From c50800eb0166c9a7ca7ec09c85ab5aa6f3ac0b9e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 24 Mar 2022 00:04:59 +0100 Subject: [PATCH 037/241] Add armv5 build for (cloud|aws)kms --- .goreleaser.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index cd4826c9..441d5785 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -40,6 +40,7 @@ builds: - linux_386 - linux_amd64 - linux_arm64 + - linux_arm_5 - linux_arm_6 - linux_arm_7 - windows_amd64 @@ -60,6 +61,7 @@ builds: - linux_386 - linux_amd64 - linux_arm64 + - linux_arm_5 - linux_arm_6 - linux_arm_7 - windows_amd64 From 161a4b28bedd5c89aad9134853a000ea3b7287ab Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Mar 2022 16:22:26 -0700 Subject: [PATCH 038/241] Change go version to 1.17 and 1.18 --- .github/workflows/release.yml | 8 ++++---- .github/workflows/test.yml | 6 +++--- CHANGELOG.md | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d0416ef..2ab7084d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: [ '1.15', '1.16', '1.17' ] + go: [ '1.17', '1.18' ] outputs: is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} steps: @@ -33,7 +33,7 @@ jobs: uses: golangci/golangci-lint-action@v2 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: 'v1.44.0' + version: 'v1.45.0' # Optional: working directory, useful for monorepos # working-directory: somedir @@ -106,7 +106,7 @@ jobs: name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.17 + go-version: 1.18 - name: APT Install id: aptInstall @@ -159,7 +159,7 @@ jobs: name: Setup Go uses: actions/setup-go@v2 with: - go-version: '1.17' + go-version: '1.18' - name: Install cosign uses: sigstore/cosign-installer@v1.1.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f36e78ef..64cb64cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: [ '1.16', '1.17' ] + go: [ '1.17', '1.18' ] steps: - name: Checkout @@ -33,7 +33,7 @@ jobs: uses: golangci/golangci-lint-action@v2 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: 'v1.44.0' + version: 'v1.45.0' # Optional: working directory, useful for monorepos # working-directory: somedir @@ -58,7 +58,7 @@ jobs: run: V=1 make ci - name: Codecov - if: matrix.go == '1.17' + if: matrix.go == '1.18' uses: codecov/codecov-action@v1.2.1 with: file: ./coverage.out # optional diff --git a/CHANGELOG.md b/CHANGELOG.md index fc25c0ed..3164b3b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added support for renew after expiry using the claim `allowRenewAfterExpiry`. ### Changed - Made SCEP CA URL paths dynamic +- Support two latest versions of golang (1.17, 1.18) ### Deprecated ### Removed ### Fixed From 5ab79f53be3ab1c3ffa4b2a9db08831a6780b670 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Mar 2022 16:53:57 -0700 Subject: [PATCH 039/241] Fix linter errors --- authority/provisioner/x5c.go | 4 +++- authority/provisioner/x5c_test.go | 2 ++ ca/ca.go | 3 --- ca/identity/client_test.go | 23 ++++++++++++++++++++++- ca/identity/identity_test.go | 2 ++ ca/tls.go | 2 -- ca/tls_options_test.go | 1 + 7 files changed, 30 insertions(+), 7 deletions(-) diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 4f3e5899..295d81fb 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -100,6 +100,7 @@ func (p *X5C) Init(config Config) (err error) { var ( block *pem.Block rest = p.Roots + count int ) for rest != nil { block, rest = pem.Decode(rest) @@ -110,11 +111,12 @@ func (p *X5C) Init(config Config) (err error) { if err != nil { return errors.Wrap(err, "error parsing x509 certificate from PEM block") } + count++ p.rootPool.AddCert(cert) } // Verify that at least one root was found. - if len(p.rootPool.Subjects()) == 0 { + if count == 0 { return errors.Errorf("no x509 certificates found in roots attribute for provisioner '%s'", p.GetName()) } diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 84e29b48..7932d045 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -118,6 +118,8 @@ M46l92gdOozT return ProvisionerValidateTest{ p: p, extraValid: func(p *X5C) error { + // nolint:staticcheck // We don't have a different way to + // check the number of certificates in the pool. numCerts := len(p.rootPool.Subjects()) if numCerts != 2 { return errors.Errorf("unexpected number of certs: want 2, but got %d", numCerts) diff --git a/ca/ca.go b/ca/ca.go index c95ba22f..dfb82731 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -450,9 +450,6 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) { tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven tlsConfig.ClientCAs = certPool - // Use server's most preferred ciphersuite - tlsConfig.PreferServerCipherSuites = true - return tlsConfig, nil } diff --git a/ca/identity/client_test.go b/ca/identity/client_test.go index 0f1234e9..9660a3bd 100644 --- a/ca/identity/client_test.go +++ b/ca/identity/client_test.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "reflect" + "sort" "testing" ) @@ -196,7 +197,7 @@ func TestLoadClient(t *testing.T) { switch { case gotTransport.TLSClientConfig.GetClientCertificate == nil: t.Error("LoadClient() transport does not define GetClientCertificate") - case !reflect.DeepEqual(got.CaURL, tt.want.CaURL) || !reflect.DeepEqual(gotTransport.TLSClientConfig.RootCAs.Subjects(), wantTransport.TLSClientConfig.RootCAs.Subjects()): + case !reflect.DeepEqual(got.CaURL, tt.want.CaURL) || !equalPools(gotTransport.TLSClientConfig.RootCAs, wantTransport.TLSClientConfig.RootCAs): t.Errorf("LoadClient() = %#v, want %#v", got, tt.want) default: crt, err := gotTransport.TLSClientConfig.GetClientCertificate(nil) @@ -238,3 +239,23 @@ func Test_defaultsConfig_Validate(t *testing.T) { }) } } + +// nolint:staticcheck,gocritic +func equalPools(a, b *x509.CertPool) bool { + if reflect.DeepEqual(a, b) { + return true + } + subjects := a.Subjects() + sA := make([]string, len(subjects)) + for i := range subjects { + sA[i] = string(subjects[i]) + } + subjects = b.Subjects() + sB := make([]string, len(subjects)) + for i := range subjects { + sB[i] = string(subjects[i]) + } + sort.Strings(sA) + sort.Strings(sB) + return reflect.DeepEqual(sA, sB) +} diff --git a/ca/identity/identity_test.go b/ca/identity/identity_test.go index d3b1d541..55fc60fd 100644 --- a/ca/identity/identity_test.go +++ b/ca/identity/identity_test.go @@ -346,6 +346,8 @@ func TestIdentity_GetCertPool(t *testing.T) { return } if got != nil { + // nolint:staticcheck // we don't have a different way to check + // the certificates in the pool. subjects := got.Subjects() if !reflect.DeepEqual(subjects, tt.wantSubjects) { t.Errorf("Identity.GetCertPool() = %x, want %x", subjects, tt.wantSubjects) diff --git a/ca/tls.go b/ca/tls.go index 0738d0e0..7954cbdf 100644 --- a/ca/tls.go +++ b/ca/tls.go @@ -95,7 +95,6 @@ func (c *Client) getClientTLSConfig(ctx context.Context, sign *api.SignResponse, // Note that with GetClientCertificate tlsConfig.Certificates is not used. // Without tlsConfig.Certificates there's not need to use tlsConfig.BuildNameToCertificate() tlsConfig.GetClientCertificate = renewer.GetClientCertificate - tlsConfig.PreferServerCipherSuites = true // Apply options and initialize mutable tls.Config tlsCtx := newTLSOptionCtx(c, tlsConfig, sign) @@ -137,7 +136,6 @@ func (c *Client) GetServerTLSConfig(ctx context.Context, sign *api.SignResponse, // Without tlsConfig.Certificates there's not need to use tlsConfig.BuildNameToCertificate() tlsConfig.GetCertificate = renewer.GetCertificate tlsConfig.GetClientCertificate = renewer.GetClientCertificate - tlsConfig.PreferServerCipherSuites = true tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert // Apply options and initialize mutable tls.Config diff --git a/ca/tls_options_test.go b/ca/tls_options_test.go index 7d94926b..ca5f80b8 100644 --- a/ca/tls_options_test.go +++ b/ca/tls_options_test.go @@ -542,6 +542,7 @@ func TestAddFederationToCAs(t *testing.T) { } } +// nolint:staticcheck,gocritic func equalPools(a, b *x509.CertPool) bool { if reflect.DeepEqual(a, b) { return true From 76ea1635a77e35885abb2502f00ea56837319007 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Mar 2022 17:59:15 -0700 Subject: [PATCH 040/241] Change golang to Go --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3164b3b6..73c338f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added support for renew after expiry using the claim `allowRenewAfterExpiry`. ### Changed - Made SCEP CA URL paths dynamic -- Support two latest versions of golang (1.17, 1.18) +- Support two latest versions of Go (1.17, 1.18) ### Deprecated ### Removed ### Fixed From 7b605b2d16b8853fdad6739a9d61fa2f289bfe30 Mon Sep 17 00:00:00 2001 From: vijayjt <2975049+vijayjt@users.noreply.github.com> Date: Mon, 7 Mar 2022 11:24:58 +0000 Subject: [PATCH 041/241] Support Azure tokens from managed identities not associated with a VM --- authority/provisioner/azure.go | 14 +++++++++++--- authority/provisioner/azure_test.go | 22 +++++++++++----------- authority/provisioner/utils_test.go | 12 +++++++++--- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 58ce47b3..eabc6efc 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -30,7 +30,7 @@ const azureDefaultAudience = "https://management.azure.com/" // azureXMSMirIDRegExp is the regular expression used to parse the xms_mirid claim. // Using case insensitive as resourceGroups appears as resourcegroups. -var azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachines/([^/]+)$`) +var azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.(Compute/virtualMachines|ManagedIdentity/userAssignedIdentities)/([^/]+)$`) type azureConfig struct { oidcDiscoveryURL string @@ -260,11 +260,19 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, str } re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID) - if len(re) != 4 { + if len(re) != 5 { return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID) } + + var subscription, group, name string identityObjectID := claims.ObjectID - subscription, group, name := re[1], re[2], re[3] + + if strings.Contains(claims.XMSMirID, "virtualMachines") { + subscription, group, name = re[1], re[2], re[4] + } else { + // This is not a VM resource ID so we don't have the VM name so set that to the empty string + subscription, group, name = re[1], re[2], "" + } return &claims, name, group, subscription, identityObjectID, nil } diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index c05685b7..40bb4698 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -95,7 +95,7 @@ func TestAzure_GetIdentityToken(t *testing.T) { assert.FatalError(t, err) t1, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience, - p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) @@ -237,7 +237,7 @@ func TestAzure_authorizeToken(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience, - p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), jwk) assert.FatalError(t, err) return test{ @@ -252,7 +252,7 @@ func TestAzure_authorizeToken(t *testing.T) { assert.FatalError(t, err) defer srv.Close() tok, err := generateAzureToken("subject", "bad-issuer", azureDefaultAudience, - p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), &p.keyStore.keySet.Keys[0]) assert.FatalError(t, err) return test{ @@ -267,7 +267,7 @@ func TestAzure_authorizeToken(t *testing.T) { assert.FatalError(t, err) defer srv.Close() tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience, - "foo", "subscriptionID", "resourceGroup", "virtualMachine", + "foo", "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), &p.keyStore.keySet.Keys[0]) assert.FatalError(t, err) return test{ @@ -321,7 +321,7 @@ func TestAzure_authorizeToken(t *testing.T) { assert.FatalError(t, err) defer srv.Close() tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience, - p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), &p.keyStore.keySet.Keys[0]) assert.FatalError(t, err) return test{ @@ -437,28 +437,28 @@ func TestAzure_AuthorizeSign(t *testing.T) { assert.FatalError(t, err) t11, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience, - p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) failIssuer, err := generateAzureToken("subject", "bad-issuer", azureDefaultAudience, - p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) failAudience, err := generateAzureToken("subject", p1.oidcConfig.Issuer, "bad-audience", - p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) failExp, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience, - p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now().Add(-360*time.Second), &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) failNbf, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience, - p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now().Add(360*time.Second), &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) failKey, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience, - p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), badKey) assert.FatalError(t, err) diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 669693d6..7e783c2a 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -656,7 +656,7 @@ func generateAzureWithServer() (*Azure, *httptest.Server, error) { w.Header().Add("Cache-Control", "max-age=5") writeJSON(w, getPublic(az.keyStore.keySet)) case "/metadata/identity/oauth2/token": - tok, err := generateAzureToken("subject", issuer, "https://management.azure.com/", az.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", time.Now(), &az.keyStore.keySet.Keys[0]) + tok, err := generateAzureToken("subject", issuer, "https://management.azure.com/", az.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", "vm", time.Now(), &az.keyStore.keySet.Keys[0]) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } else { @@ -994,7 +994,7 @@ func generateAWSToken(p *AWS, sub, iss, aud, accountID, instanceID, privateIP, r return jose.Signed(sig).Claims(claims).CompactSerialize() } -func generateAzureToken(sub, iss, aud, tenantID, subscriptionID, resourceGroup, virtualMachine string, iat time.Time, jwk *jose.JSONWebKey) (string, error) { +func generateAzureToken(sub, iss, aud, tenantID, subscriptionID, resourceGroup, resourceName string, resourceType string, iat time.Time, jwk *jose.JSONWebKey) (string, error) { sig, err := jose.NewSigner( jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID), @@ -1002,6 +1002,12 @@ func generateAzureToken(sub, iss, aud, tenantID, subscriptionID, resourceGroup, if err != nil { return "", err } + var xmsMirID string + if resourceType == "vm" { + xmsMirID = fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", subscriptionID, resourceGroup, resourceName) + } else if resourceType == "uai" { + xmsMirID = fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ManagedIdentity/userAssignedIdentities/%s", subscriptionID, resourceGroup, resourceName) + } claims := azurePayload{ Claims: jose.Claims{ @@ -1019,7 +1025,7 @@ func generateAzureToken(sub, iss, aud, tenantID, subscriptionID, resourceGroup, ObjectID: "the-oid", TenantID: tenantID, Version: "the-version", - XMSMirID: fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", subscriptionID, resourceGroup, virtualMachine), + XMSMirID: xmsMirID, } return jose.Signed(sig).Claims(claims).CompactSerialize() } From 7e47c70af2eded79e4e94d3356d85abb337f1553 Mon Sep 17 00:00:00 2001 From: vijayjt <2975049+vijayjt@users.noreply.github.com> Date: Mon, 7 Mar 2022 12:07:48 +0000 Subject: [PATCH 042/241] Remove redundant parameter type declaration --- authority/provisioner/utils_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 7e783c2a..c55c58d2 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -994,7 +994,7 @@ func generateAWSToken(p *AWS, sub, iss, aud, accountID, instanceID, privateIP, r return jose.Signed(sig).Claims(claims).CompactSerialize() } -func generateAzureToken(sub, iss, aud, tenantID, subscriptionID, resourceGroup, resourceName string, resourceType string, iat time.Time, jwk *jose.JSONWebKey) (string, error) { +func generateAzureToken(sub, iss, aud, tenantID, subscriptionID, resourceGroup, resourceName, resourceType string, iat time.Time, jwk *jose.JSONWebKey) (string, error) { sig, err := jose.NewSigner( jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID), From 37207793f9368a5e2a13e06ad36b150167713979 Mon Sep 17 00:00:00 2001 From: vijayjt <2975049+vijayjt@users.noreply.github.com> Date: Tue, 22 Mar 2022 00:10:43 +0000 Subject: [PATCH 043/241] Pass in the resource name regardless of if its a VM or managed identity --- authority/provisioner/azure.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index eabc6efc..e6323e9f 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -266,13 +266,8 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, str var subscription, group, name string identityObjectID := claims.ObjectID + subscription, group, name = re[1], re[2], re[4] - if strings.Contains(claims.XMSMirID, "virtualMachines") { - subscription, group, name = re[1], re[2], re[4] - } else { - // This is not a VM resource ID so we don't have the VM name so set that to the empty string - subscription, group, name = re[1], re[2], "" - } return &claims, name, group, subscription, identityObjectID, nil } From bca74cb6a734794284439a63219d6ece301b9a82 Mon Sep 17 00:00:00 2001 From: Panagiotis Siatras Date: Thu, 24 Mar 2022 14:58:50 +0200 Subject: [PATCH 044/241] scep: minor cleanup (#867) * api, scep: removed scep.Error * scep/api: replaced nextHTTP with http.HandlerFunc * scep/api: renamed writeSCEPResponse to writeResponse * scep/api: renamed decodeSCEPRequest to decodeRequest * scep/api: renamed writeError to fail * scep/api: replaced pkg/errors with errors * scep/api: formatted imports * scep/api: do not export SCEPRequest & SCEPResponse * scep/api: do not export Handler * api: flush errors better --- api/errors.go | 24 +++----- scep/api/api.go | 158 +++++++++++++++++++++++------------------------- scep/errors.go | 12 ---- 3 files changed, 86 insertions(+), 108 deletions(-) delete mode 100644 scep/errors.go diff --git a/api/errors.go b/api/errors.go index 49efd486..680e6578 100644 --- a/api/errors.go +++ b/api/errors.go @@ -13,7 +13,6 @@ import ( "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" - "github.com/smallstep/certificates/scep" ) // WriteError writes to w a JSON representation of the given error. @@ -25,22 +24,9 @@ func WriteError(w http.ResponseWriter, err error) { case *admin.Error: admin.WriteError(w, k) return - case *scep.Error: - w.Header().Set("Content-Type", "text/plain") - default: - w.Header().Set("Content-Type", "application/json") } cause := errors.Cause(err) - if sc, ok := err.(errs.StatusCoder); ok { - w.WriteHeader(sc.StatusCode()) - } else { - if sc, ok := cause.(errs.StatusCoder); ok { - w.WriteHeader(sc.StatusCode()) - } else { - w.WriteHeader(http.StatusInternalServerError) - } - } // Write errors in the response writer if rl, ok := w.(logging.ResponseLogger); ok { @@ -60,6 +46,16 @@ func WriteError(w http.ResponseWriter, err error) { } } + code := http.StatusInternalServerError + if sc, ok := err.(errs.StatusCoder); ok { + code = sc.StatusCode() + } else if sc, ok := cause.(errs.StatusCoder); ok { + code = sc.StatusCode() + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + if err := json.NewEncoder(w).Encode(err); err != nil { log.Error(w, err) } diff --git a/scep/api/api.go b/scep/api/api.go index a326ea92..91d337fe 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -4,6 +4,8 @@ import ( "context" "crypto/x509" "encoding/base64" + "errors" + "fmt" "io" "net/http" "net/url" @@ -11,7 +13,6 @@ import ( "github.com/go-chi/chi" microscep "github.com/micromdm/scep/v2/scep" - "github.com/pkg/errors" "go.mozilla.org/pkcs7" "github.com/smallstep/certificates/api" @@ -30,22 +31,20 @@ const ( const maxPayloadSize = 2 << 20 -type nextHTTP = func(http.ResponseWriter, *http.Request) - const ( certChainHeader = "application/x-x509-ca-ra-cert" leafHeader = "application/x-x509-ca-cert" pkiOperationHeader = "application/x-pki-message" ) -// SCEPRequest is a SCEP server request. -type SCEPRequest struct { +// request is a SCEP server request. +type request struct { Operation string Message []byte } -// SCEPResponse is a SCEP server response. -type SCEPResponse struct { +// response is a SCEP server response. +type response struct { Operation string CACertNum int Data []byte @@ -53,18 +52,18 @@ type SCEPResponse struct { Error error } -// Handler is the SCEP request handler. -type Handler struct { +// handler is the SCEP request handler. +type handler struct { Auth scep.Interface } // New returns a new SCEP API router. func New(scepAuth scep.Interface) api.RouterHandler { - return &Handler{scepAuth} + return &handler{scepAuth} } // Route traffic and implement the Router interface. -func (h *Handler) Route(r api.Router) { +func (h *handler) Route(r api.Router) { getLink := h.Auth.GetLinkExplicit r.MethodFunc(http.MethodGet, getLink("{provisionerName}/*", false, nil), h.lookupProvisioner(h.Get)) r.MethodFunc(http.MethodGet, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Get)) @@ -73,64 +72,64 @@ func (h *Handler) Route(r api.Router) { } // Get handles all SCEP GET requests -func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { +func (h *handler) Get(w http.ResponseWriter, r *http.Request) { - request, err := decodeSCEPRequest(r) + req, err := decodeRequest(r) if err != nil { - writeError(w, errors.Wrap(err, "invalid scep get request")) + fail(w, fmt.Errorf("invalid scep get request: %w", err)) return } ctx := r.Context() - var response SCEPResponse + var res response - switch request.Operation { + switch req.Operation { case opnGetCACert: - response, err = h.GetCACert(ctx) + res, err = h.GetCACert(ctx) case opnGetCACaps: - response, err = h.GetCACaps(ctx) + res, err = h.GetCACaps(ctx) case opnPKIOperation: // TODO: implement the GET for PKI operation? Default CACAPS doesn't specify this is in use, though default: - err = errors.Errorf("unknown operation: %s", request.Operation) + err = fmt.Errorf("unknown operation: %s", req.Operation) } if err != nil { - writeError(w, errors.Wrap(err, "scep get request failed")) + fail(w, fmt.Errorf("scep get request failed: %w", err)) return } - writeSCEPResponse(w, response) + writeResponse(w, res) } // Post handles all SCEP POST requests -func (h *Handler) Post(w http.ResponseWriter, r *http.Request) { +func (h *handler) Post(w http.ResponseWriter, r *http.Request) { - request, err := decodeSCEPRequest(r) + req, err := decodeRequest(r) if err != nil { - writeError(w, errors.Wrap(err, "invalid scep post request")) + fail(w, fmt.Errorf("invalid scep post request: %w", err)) return } ctx := r.Context() - var response SCEPResponse + var res response - switch request.Operation { + switch req.Operation { case opnPKIOperation: - response, err = h.PKIOperation(ctx, request) + res, err = h.PKIOperation(ctx, req) default: - err = errors.Errorf("unknown operation: %s", request.Operation) + err = fmt.Errorf("unknown operation: %s", req.Operation) } if err != nil { - writeError(w, errors.Wrap(err, "scep post request failed")) + fail(w, fmt.Errorf("scep post request failed: %w", err)) return } - writeSCEPResponse(w, response) + writeResponse(w, res) } -func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) { +func decodeRequest(r *http.Request) (request, error) { defer r.Body.Close() @@ -146,7 +145,7 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) { case http.MethodGet: switch operation { case opnGetCACert, opnGetCACaps: - return SCEPRequest{ + return request{ Operation: operation, Message: []byte{}, }, nil @@ -158,50 +157,50 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) { // TODO: verify this; it seems like it should be StdEncoding instead of URLEncoding decodedMessage, err := base64.URLEncoding.DecodeString(message) if err != nil { - return SCEPRequest{}, err + return request{}, err } - return SCEPRequest{ + return request{ Operation: operation, Message: decodedMessage, }, nil default: - return SCEPRequest{}, errors.Errorf("unsupported operation: %s", operation) + return request{}, fmt.Errorf("unsupported operation: %s", operation) } case http.MethodPost: body, err := io.ReadAll(io.LimitReader(r.Body, maxPayloadSize)) if err != nil { - return SCEPRequest{}, err + return request{}, err } - return SCEPRequest{ + return request{ Operation: operation, Message: body, }, nil default: - return SCEPRequest{}, errors.Errorf("unsupported method: %s", method) + return request{}, fmt.Errorf("unsupported method: %s", method) } } // lookupProvisioner loads the provisioner associated with the request. // Responds 404 if the provisioner does not exist. -func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { +func (h *handler) lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "provisionerName") provisionerName, err := url.PathUnescape(name) if err != nil { - api.WriteError(w, errors.Errorf("error url unescaping provisioner name '%s'", name)) + fail(w, fmt.Errorf("error url unescaping provisioner name '%s'", name)) return } p, err := h.Auth.LoadProvisionerByName(provisionerName) if err != nil { - api.WriteError(w, err) + fail(w, err) return } prov, ok := p.(*provisioner.SCEP) if !ok { - api.WriteError(w, errors.New("provisioner must be of type SCEP")) + fail(w, errors.New("provisioner must be of type SCEP")) return } @@ -212,59 +211,59 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { } // GetCACert returns the CA certificates in a SCEP response -func (h *Handler) GetCACert(ctx context.Context) (SCEPResponse, error) { +func (h *handler) GetCACert(ctx context.Context) (response, error) { certs, err := h.Auth.GetCACertificates(ctx) if err != nil { - return SCEPResponse{}, err + return response{}, err } if len(certs) == 0 { - return SCEPResponse{}, errors.New("missing CA cert") + return response{}, errors.New("missing CA cert") } - response := SCEPResponse{ + res := response{ Operation: opnGetCACert, CACertNum: len(certs), } if len(certs) == 1 { - response.Data = certs[0].Raw + res.Data = certs[0].Raw } else { // create degenerate pkcs7 certificate structure, according to // https://tools.ietf.org/html/rfc8894#section-4.2.1.2, because // not signed or encrypted data has to be returned. data, err := microscep.DegenerateCertificates(certs) if err != nil { - return SCEPResponse{}, err + return response{}, err } - response.Data = data + res.Data = data } - return response, nil + return res, nil } // GetCACaps returns the CA capabilities in a SCEP response -func (h *Handler) GetCACaps(ctx context.Context) (SCEPResponse, error) { +func (h *handler) GetCACaps(ctx context.Context) (response, error) { caps := h.Auth.GetCACaps(ctx) - response := SCEPResponse{ + res := response{ Operation: opnGetCACaps, Data: formatCapabilities(caps), } - return response, nil + return res, nil } // PKIOperation performs PKI operations and returns a SCEP response -func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPResponse, error) { +func (h *handler) PKIOperation(ctx context.Context, req request) (response, error) { // parse the message using microscep implementation - microMsg, err := microscep.ParsePKIMessage(request.Message) + microMsg, err := microscep.ParsePKIMessage(req.Message) if err != nil { // return the error, because we can't use the msg for creating a CertRep - return SCEPResponse{}, err + return response{}, err } // this is essentially doing the same as microscep.ParsePKIMessage, but @@ -272,7 +271,7 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe // wrapper for the microscep implementation. p7, err := pkcs7.Parse(microMsg.Raw) if err != nil { - return SCEPResponse{}, err + return response{}, err } // copy over properties to our internal PKIMessage @@ -285,7 +284,7 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe } if err := h.Auth.DecryptPKIEnvelope(ctx, msg); err != nil { - return SCEPResponse{}, err + return response{}, err } // NOTE: at this point we have sufficient information for returning nicely signed CertReps @@ -317,61 +316,56 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe certRep, err := h.Auth.SignCSR(ctx, csr, msg) if err != nil { - return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.Wrap(err, "error when signing new certificate")) + return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err)) } - response := SCEPResponse{ + res := response{ Operation: opnPKIOperation, Data: certRep.Raw, Certificate: certRep.Certificate, } - return response, nil + return res, nil } func formatCapabilities(caps []string) []byte { return []byte(strings.Join(caps, "\r\n")) } -// writeSCEPResponse writes a SCEP response back to the SCEP client. -func writeSCEPResponse(w http.ResponseWriter, response SCEPResponse) { +// writeResponse writes a SCEP response back to the SCEP client. +func writeResponse(w http.ResponseWriter, res response) { - if response.Error != nil { - log.Error(w, response.Error) + if res.Error != nil { + log.Error(w, res.Error) } - if response.Certificate != nil { - api.LogCertificate(w, response.Certificate) + if res.Certificate != nil { + api.LogCertificate(w, res.Certificate) } - w.Header().Set("Content-Type", contentHeader(response)) - _, err := w.Write(response.Data) - if err != nil { - writeError(w, errors.Wrap(err, "error when writing scep response")) // This could end up as an error again - } + w.Header().Set("Content-Type", contentHeader(res)) + _, _ = w.Write(res.Data) } -func writeError(w http.ResponseWriter, err error) { - scepError := &scep.Error{ - Message: err.Error(), - Status: http.StatusInternalServerError, // TODO: make this a param? - } - api.WriteError(w, scepError) +func fail(w http.ResponseWriter, err error) { + log.Error(w, err) + + http.Error(w, err.Error(), http.StatusInternalServerError) } -func (h *Handler) createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (SCEPResponse, error) { +func (h *handler) createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (response, error) { certRepMsg, err := h.Auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error()) if err != nil { - return SCEPResponse{}, err + return response{}, err } - return SCEPResponse{ + return response{ Operation: opnPKIOperation, Data: certRepMsg.Raw, Error: failError, }, nil } -func contentHeader(r SCEPResponse) string { +func contentHeader(r response) string { switch r.Operation { case opnGetCACert: if r.CACertNum > 1 { diff --git a/scep/errors.go b/scep/errors.go deleted file mode 100644 index 4287403b..00000000 --- a/scep/errors.go +++ /dev/null @@ -1,12 +0,0 @@ -package scep - -// Error is an SCEP error type -type Error struct { - Message string `json:"message"` - Status int `json:"-"` -} - -// Error implements the error interface. -func (e *Error) Error() string { - return e.Message -} From a8522237172c4337076a4d1354a5ea94c0d09d47 Mon Sep 17 00:00:00 2001 From: Panagiotis Siatras Date: Thu, 24 Mar 2022 17:08:23 +0200 Subject: [PATCH 045/241] scep: remove Interface and the dependency to pkg/errors (#872) * scep: documented the package * scep/api: removed some top level constants * scep: removed dependency to pkg/errors * scep/api: documented the package --- scep/api/api.go | 41 +++++++++++++++++++---------------------- scep/authority.go | 40 +++++++++++++--------------------------- scep/scep.go | 4 +--- 3 files changed, 33 insertions(+), 52 deletions(-) diff --git a/scep/api/api.go b/scep/api/api.go index 91d337fe..31f0f10d 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -1,3 +1,4 @@ +// Package api implements a SCEP HTTP server. package api import ( @@ -31,12 +32,6 @@ const ( const maxPayloadSize = 2 << 20 -const ( - certChainHeader = "application/x-x509-ca-ra-cert" - leafHeader = "application/x-x509-ca-cert" - pkiOperationHeader = "application/x-pki-message" -) - // request is a SCEP server request. type request struct { Operation string @@ -54,17 +49,19 @@ type response struct { // handler is the SCEP request handler. type handler struct { - Auth scep.Interface + auth *scep.Authority } // New returns a new SCEP API router. -func New(scepAuth scep.Interface) api.RouterHandler { - return &handler{scepAuth} +func New(auth *scep.Authority) api.RouterHandler { + return &handler{ + auth: auth, + } } // Route traffic and implement the Router interface. func (h *handler) Route(r api.Router) { - getLink := h.Auth.GetLinkExplicit + getLink := h.auth.GetLinkExplicit r.MethodFunc(http.MethodGet, getLink("{provisionerName}/*", false, nil), h.lookupProvisioner(h.Get)) r.MethodFunc(http.MethodGet, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Get)) r.MethodFunc(http.MethodPost, getLink("{provisionerName}/*", false, nil), h.lookupProvisioner(h.Post)) @@ -192,7 +189,7 @@ func (h *handler) lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { return } - p, err := h.Auth.LoadProvisionerByName(provisionerName) + p, err := h.auth.LoadProvisionerByName(provisionerName) if err != nil { fail(w, err) return @@ -213,7 +210,7 @@ func (h *handler) lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { // GetCACert returns the CA certificates in a SCEP response func (h *handler) GetCACert(ctx context.Context) (response, error) { - certs, err := h.Auth.GetCACertificates(ctx) + certs, err := h.auth.GetCACertificates(ctx) if err != nil { return response{}, err } @@ -246,7 +243,7 @@ func (h *handler) GetCACert(ctx context.Context) (response, error) { // GetCACaps returns the CA capabilities in a SCEP response func (h *handler) GetCACaps(ctx context.Context) (response, error) { - caps := h.Auth.GetCACaps(ctx) + caps := h.auth.GetCACaps(ctx) res := response{ Operation: opnGetCACaps, @@ -283,7 +280,7 @@ func (h *handler) PKIOperation(ctx context.Context, req request) (response, erro P7: p7, } - if err := h.Auth.DecryptPKIEnvelope(ctx, msg); err != nil { + if err := h.auth.DecryptPKIEnvelope(ctx, msg); err != nil { return response{}, err } @@ -296,7 +293,7 @@ func (h *handler) PKIOperation(ctx context.Context, req request) (response, erro // a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients. // We'll have to see how it works out. if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq { - challengeMatches, err := h.Auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword) + challengeMatches, err := h.auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword) if err != nil { return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("error when checking password")) } @@ -314,7 +311,7 @@ func (h *handler) PKIOperation(ctx context.Context, req request) (response, erro // Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification // of the client cert is not. - certRep, err := h.Auth.SignCSR(ctx, csr, msg) + certRep, err := h.auth.SignCSR(ctx, csr, msg) if err != nil { return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err)) } @@ -354,7 +351,7 @@ func fail(w http.ResponseWriter, err error) { } func (h *handler) createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (response, error) { - certRepMsg, err := h.Auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error()) + certRepMsg, err := h.auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error()) if err != nil { return response{}, err } @@ -367,14 +364,14 @@ func (h *handler) createFailureResponse(ctx context.Context, csr *x509.Certifica func contentHeader(r response) string { switch r.Operation { + default: + return "text/plain" case opnGetCACert: if r.CACertNum > 1 { - return certChainHeader + return "application/x-x509-ca-ra-cert" } - return leafHeader + return "application/x-x509-ca-cert" case opnPKIOperation: - return pkiOperationHeader - default: - return "text/plain" + return "application/x-pki-message" } } diff --git a/scep/authority.go b/scep/authority.go index 269e3ae1..71f92152 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -4,32 +4,18 @@ import ( "context" "crypto/subtle" "crypto/x509" + "errors" + "fmt" "net/url" - "github.com/smallstep/certificates/authority/provisioner" - microx509util "github.com/micromdm/scep/v2/cryptoutil/x509util" microscep "github.com/micromdm/scep/v2/scep" - - "github.com/pkg/errors" - "go.mozilla.org/pkcs7" "go.step.sm/crypto/x509util" -) -// Interface is the SCEP authority interface. -type Interface interface { - LoadProvisionerByName(string) (provisioner.Interface, error) - GetLinkExplicit(provName string, absoluteLink bool, baseURL *url.URL, inputs ...string) string - - GetCACertificates(ctx context.Context) ([]*x509.Certificate, error) - DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error - SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error) - CreateFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) - MatchChallengePassword(ctx context.Context, password string) (bool, error) - GetCACaps(ctx context.Context) []string -} + "github.com/smallstep/certificates/authority/provisioner" +) // Authority is the layer that handles all SCEP interactions. type Authority struct { @@ -180,12 +166,12 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err p7c, err := pkcs7.Parse(msg.P7.Content) if err != nil { - return errors.Wrap(err, "error parsing pkcs7 content") + return fmt.Errorf("error parsing pkcs7 content: %w", err) } envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter) if err != nil { - return errors.Wrap(err, "error decrypting encrypted pkcs7 content") + return fmt.Errorf("error decrypting encrypted pkcs7 content: %w", err) } msg.pkiEnvelope = envelope @@ -194,19 +180,19 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err case microscep.CertRep: certs, err := microscep.CACerts(msg.pkiEnvelope) if err != nil { - return errors.Wrap(err, "error extracting CA certs from pkcs7 degenerate data") + return fmt.Errorf("error extracting CA certs from pkcs7 degenerate data: %w", err) } msg.CertRepMessage.Certificate = certs[0] return nil case microscep.PKCSReq, microscep.UpdateReq, microscep.RenewalReq: csr, err := x509.ParseCertificateRequest(msg.pkiEnvelope) if err != nil { - return errors.Wrap(err, "parse CSR from pkiEnvelope") + return fmt.Errorf("parse CSR from pkiEnvelope: %w", err) } // check for challengePassword cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope) if err != nil { - return errors.Wrap(err, "parse challenge password in pkiEnvelope") + return fmt.Errorf("parse challenge password in pkiEnvelope: %w", err) } msg.CSRReqMessage = µscep.CSRReqMessage{ RawDecrypted: msg.pkiEnvelope, @@ -215,7 +201,7 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err } return nil case microscep.GetCRL, microscep.GetCert, microscep.CertPoll: - return errors.Errorf("not implemented") + return errors.New("not implemented") } return nil @@ -274,19 +260,19 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) signOps, err := p.AuthorizeSign(ctx, "") if err != nil { - return nil, errors.Wrap(err, "error retrieving authorization options from SCEP provisioner") + return nil, fmt.Errorf("error retrieving authorization options from SCEP provisioner: %w", err) } opts := provisioner.SignOptions{} templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data) if err != nil { - return nil, errors.Wrap(err, "error creating template options from SCEP provisioner") + return nil, fmt.Errorf("error creating template options from SCEP provisioner: %w", err) } signOps = append(signOps, templateOptions) certChain, err := a.signAuth.Sign(csr, opts, signOps...) if err != nil { - return nil, errors.Wrap(err, "error generating certificate for order") + return nil, fmt.Errorf("error generating certificate for order: %w", err) } // take the issued certificate (only); https://tools.ietf.org/html/rfc8894#section-3.3.2 diff --git a/scep/scep.go b/scep/scep.go index afabf368..372a5436 100644 --- a/scep/scep.go +++ b/scep/scep.go @@ -1,3 +1,4 @@ +// Package scep implements Simple Certificate Enrollment Protocol related functionality. package scep import ( @@ -5,9 +6,6 @@ import ( "encoding/asn1" microscep "github.com/micromdm/scep/v2/scep" - - //"github.com/smallstep/certificates/scep/pkcs7" - "go.mozilla.org/pkcs7" ) From 27c1d0afc33c5036ca2543454ea0ffaad58ed654 Mon Sep 17 00:00:00 2001 From: Panagiotis Siatras Date: Thu, 24 Mar 2022 18:18:51 +0200 Subject: [PATCH 046/241] add --context flag to step-ca command (#851) * added the --context flag * apply the context and allow for different ca.json * amended usage for consistency * added an extra example * added an extra example * reordered and reworded examples --- cmd/step-ca/main.go | 24 ++++++++++++++++++++++-- commands/app.go | 26 ++++++++++++++++++++------ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index fba5b792..96e7fbd5 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -117,7 +117,7 @@ func main() { app.HelpName = "step-ca" app.Version = step.Version() app.Usage = "an online certificate authority for secure automated certificate management" - app.UsageText = `**step-ca** [**--password-file**=] + app.UsageText = `**step-ca** [config] [**--context**=] [**--password-file**=] [**--ssh-host-password-file**=] [**--ssh-user-password-file**=] [**--issuer-password-file**=] [**--resolver**=] [**--help**] [**--version**]` app.Description = `**step-ca** runs the Step Online Certificate Authority @@ -133,6 +133,7 @@ This command will run indefinitely on success and return \>0 if any error occurs These examples assume that you have already initialized your PKI by running 'step ca init'. If you have not completed this step please see the 'Getting Started' section of the README. + Run the Step CA and prompt for password: ''' $ step-ca $STEPPATH/config/ca.json @@ -141,7 +142,26 @@ Run the Step CA and read the password from a file - this is useful for automating deployment: ''' $ step-ca $STEPPATH/config/ca.json --password-file ./password.txt -'''` +''' +Run the Step CA for the context selected with step and a custom password file: +''' +$ step context select ssh +$ step-ca --password-file ./password.txt +''' +Run the Step CA for the context named _mybiz_ and prompt for password: +''' +$ step-ca --context=mybiz +''' +Run the Step CA for the context named _mybiz_ and an alternate ca.json file: +''' +$ step-ca --context=mybiz other-ca.json +''' +Run the Step CA for the context named _mybiz_ and read the password from a file - this is useful for +automating deployment: +''' +$ step-ca --context=mybiz --password-file ./password.txt +''' +` app.Flags = append(app.Flags, commands.AppCommand.Flags...) app.Flags = append(app.Flags, cli.HelpFlag) app.Copyright = fmt.Sprintf("(c) 2018-%d Smallstep Labs, Inc.", time.Now().Year()) diff --git a/commands/app.go b/commands/app.go index 8c40de0e..fc9cd15b 100644 --- a/commands/app.go +++ b/commands/app.go @@ -16,6 +16,7 @@ import ( "github.com/smallstep/certificates/pki" "github.com/urfave/cli" "go.step.sm/cli-utils/errs" + "go.step.sm/cli-utils/step" ) // AppCommand is the action used as the top action. @@ -57,6 +58,11 @@ certificate issuer private key used in the RA mode.`, Usage: "token used to enable the linked ca.", EnvVar: "STEP_CA_TOKEN", }, + cli.StringFlag{ + Name: "context", + Usage: "The name of the authority's context.", + EnvVar: "STEP_CA_CONTEXT", + }, }, } @@ -69,15 +75,23 @@ func appAction(ctx *cli.Context) error { resolver := ctx.String("resolver") token := ctx.String("token") - // If zero cmd line args show help, if >1 cmd line args show error. - if ctx.NArg() == 0 { - return cli.ShowAppHelp(ctx) + if ctx.NArg() > 1 { + return errs.TooManyArguments(ctx) } - if err := errs.NumberOfArguments(ctx, 1); err != nil { - return err + + if caCtx := ctx.String("context"); caCtx != "" { + if err := step.Contexts().SetCurrent(caCtx); err != nil { + return err + } + } + + var configFile string + if ctx.NArg() > 0 { + configFile = ctx.Args().Get(0) + } else { + configFile = step.CaConfigFile() } - configFile := ctx.Args().Get(0) cfg, err := config.LoadConfiguration(configFile) if err != nil { fatal(err) From 750e9ee2f86813f772167830994fd3228a8b1e63 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 23 Mar 2022 19:14:28 -0700 Subject: [PATCH 047/241] Attempt to fix TestBootstrapClientServerRotation This change attempts to fix the test TestBootstrapClientServerRotation. Due to the backdate, the renew options get too large, causing continuous renewals, and random errors. After experimenting with different options, truncating durations to seconds have shown better results than rounding or just use the plain time. --- ca/renew.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ca/renew.go b/ca/renew.go index 915be787..27898993 100644 --- a/ca/renew.go +++ b/ca/renew.go @@ -60,7 +60,10 @@ func NewTLSRenewer(cert *tls.Certificate, fn RenewFunc, opts ...tlsRenewerOption } } - period := cert.Leaf.NotAfter.Sub(cert.Leaf.NotBefore) + // Use the current time to calculate the initial period. Using a notBefore + // in the past might set a renewBefore too large, causing continuous + // renewals due to the negative values in nextRenewDuration. + period := cert.Leaf.NotAfter.Sub(time.Now().Truncate(time.Second)) if period < minCertDuration { return nil, errors.Errorf("period must be greater than or equal to %s, but got %v.", minCertDuration, period) } @@ -181,7 +184,7 @@ func (r *TLSRenewer) renewCertificate() { } func (r *TLSRenewer) nextRenewDuration(notAfter time.Time) time.Duration { - d := time.Until(notAfter) - r.renewBefore + d := time.Until(notAfter).Truncate(time.Second) - r.renewBefore n := rand.Int63n(int64(r.renewJitter)) d -= time.Duration(n) if d < 0 { From 52d7f084d2917cf4f2a6839496e7726debb9ddab Mon Sep 17 00:00:00 2001 From: Andrew Reed Date: Mon, 28 Mar 2022 09:18:18 -0500 Subject: [PATCH 048/241] Add /roots.pem handler (#866) * Add /roots.pem handler * Review changes * Remove no peer cert test case --- api/api.go | 25 +++++++++++++++++++++++++ api/api_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/api/api.go b/api/api.go index 47d2fd27..1c47c03d 100644 --- a/api/api.go +++ b/api/api.go @@ -21,6 +21,7 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" + "github.com/smallstep/certificates/api/log" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" @@ -259,6 +260,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("GET", "/provisioners", h.Provisioners) r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey) r.MethodFunc("GET", "/roots", h.Roots) + r.MethodFunc("GET", "/roots.pem", h.RootsPEM) r.MethodFunc("GET", "/federation", h.Federation) // SSH CA r.MethodFunc("POST", "/ssh/sign", h.SSHSign) @@ -364,6 +366,29 @@ func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) { }, http.StatusCreated) } +// RootsPEM returns all the root certificates for the CA in PEM format. +func (h *caHandler) RootsPEM(w http.ResponseWriter, r *http.Request) { + roots, err := h.Authority.GetRoots() + if err != nil { + WriteError(w, errs.InternalServerErr(err)) + return + } + + w.Header().Set("Content-Type", "application/x-pem-file") + + for _, root := range roots { + block := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: root.Raw, + }) + + if _, err := w.Write(block); err != nil { + log.Error(w, err) + return + } + } +} + // Federation returns all the public certificates in the federation. func (h *caHandler) Federation(w http.ResponseWriter, r *http.Request) { federated, err := h.Authority.GetFederation() diff --git a/api/api_test.go b/api/api_test.go index 25abdeff..39c77de7 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1344,6 +1344,46 @@ func Test_caHandler_Roots(t *testing.T) { } } +func Test_caHandler_RootsPEM(t *testing.T) { + parsedRoot := parseCertificate(rootPEM) + tests := []struct { + name string + roots []*x509.Certificate + err error + statusCode int + expect string + }{ + {"one root", []*x509.Certificate{parsedRoot}, nil, http.StatusOK, rootPEM}, + {"two roots", []*x509.Certificate{parsedRoot, parsedRoot}, nil, http.StatusOK, rootPEM + "\n" + rootPEM}, + {"fail", nil, errors.New("an error"), http.StatusInternalServerError, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := New(&mockAuthority{ret1: tt.roots, err: tt.err}).(*caHandler) + req := httptest.NewRequest("GET", "https://example.com/roots", nil) + w := httptest.NewRecorder() + h.RootsPEM(w, req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.RootsPEM StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.RootsPEM unexpected error = %v", err) + } + if tt.statusCode < http.StatusBadRequest { + if !bytes.Equal(bytes.TrimSpace(body), []byte(tt.expect)) { + t.Errorf("caHandler.RootsPEM Body = %s, wants %s", body, tt.expect) + } + } + }) + } +} + func Test_caHandler_Federation(t *testing.T) { cs := &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)}, From 4cde2696e5bd90a350f65878581a400e4caf7bb4 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sun, 27 Mar 2022 21:40:01 +0200 Subject: [PATCH 049/241] Update cloud.google.com/go/kms --- go.mod | 17 +++++----- go.sum | 101 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index aad8e124..8f8c3143 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/smallstep/certificates go 1.16 require ( - cloud.google.com/go v0.83.0 + cloud.google.com/go v0.100.2 + cloud.google.com/go/kms v1.4.0 + cloud.google.com/go/security v1.3.0 github.com/Azure/azure-sdk-for-go v58.0.0+incompatible github.com/Azure/go-autorest/autorest v0.11.17 github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 @@ -18,9 +20,9 @@ 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/google/go-cmp v0.5.6 + github.com/google/go-cmp v0.5.7 github.com/google/uuid v1.3.0 - github.com/googleapis/gax-go/v2 v2.0.5 + github.com/googleapis/gax-go/v2 v2.1.1 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 @@ -37,11 +39,10 @@ require ( go.step.sm/crypto v0.15.3 go.step.sm/linkedca v0.11.0 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 - golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d - golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect - google.golang.org/api v0.47.0 - google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 - google.golang.org/grpc v1.43.0 + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd + 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 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index f124df98..47f84801 100644 --- a/go.sum +++ b/go.sum @@ -18,20 +18,38 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0 h1:bAMqZidYkmIsUqe6PtkEPT7Q+vfizScn+jfNA6jwK9c= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0 h1:mPL/MzDDYHsh5tHRS9mhmhWlcgClCrCa6ApQCU6wnHI= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v0.1.0 h1:W2vbGCrE3Z7J/x3WXLxxGl9LMSB2uhsAA7Ss/6u/qRY= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/security v1.3.0 h1:BhCl33x+KQI4qiZnFrfr2gAGhb2aZ0ZvKB3Y4QlEfgo= +cloud.google.com/go/security v1.3.0/go.mod h1:pQsnLAXfMzuWVJdctBs8BV3tGd3Jr0SMYu6KK3QXYAs= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -294,8 +312,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -314,6 +333,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -321,8 +342,10 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +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/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -804,8 +827,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs= -golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/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/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= @@ -817,8 +840,12 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -897,14 +924,24 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/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= @@ -982,6 +1019,9 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1014,8 +1054,19 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0 h1:sQLWZQvP6jPGIP4JGPkJu4zHswrv81iobiyszr3b/0I= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0 h1:67zQnAE0T2rB0A3CwLSas0K+SbVzSxP+zTLkQLexeiw= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1068,8 +1119,29 @@ google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q= -google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +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/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= @@ -1097,9 +1169,12 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= -google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +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/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= From 580a9c14762aab8adde8433441d04e282f4abc74 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 23 Mar 2022 14:56:39 -0700 Subject: [PATCH 050/241] Get linked RA configuration using the linked ca client. --- authority/authority.go | 48 ++++++++++++++++++++++++++++++------------ authority/linkedca.go | 18 ++++++++++------ 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index cc26635e..1aac594a 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -253,6 +253,21 @@ func (a *Authority) init() error { } } + // Initialize linkedca client if necessary. On a linked RA, the issuer + // configuration might come from majordomo. + var linkedcaClient *linkedCaClient + if a.config.AuthorityConfig.EnableAdmin && a.linkedCAToken != "" && a.adminDB == nil { + linkedcaClient, err = newLinkedCAClient(a.linkedCAToken) + if err != nil { + return err + } + // If authorityId is configured make sure it matches the one in the token + if id := a.config.AuthorityConfig.AuthorityID; id != "" && !strings.EqualFold(id, linkedcaClient.authorityID) { + return errors.New("error initializing linkedca: token authority and configured authority do not match") + } + linkedcaClient.Run() + } + // Initialize the X.509 CA Service if it has not been set in the options. if a.x509CAService == nil { var options casapi.Options @@ -260,6 +275,22 @@ func (a *Authority) init() error { options = *a.config.AuthorityConfig.Options } + // Configure linked RA + if linkedcaClient != nil && options.CertificateAuthority == "" { + conf, err := linkedcaClient.GetConfiguration(context.Background()) + if err != nil { + return err + } + if conf.RaConfig != nil { + options.CertificateAuthority = conf.RaConfig.CaUrl + options.CertificateAuthorityFingerprint = conf.RaConfig.Fingerprint + options.CertificateIssuer = &casapi.CertificateIssuer{ + Type: conf.RaConfig.Provisioner.Type.String(), + Provisioner: conf.RaConfig.Provisioner.Name, + } + } + } + // Set the issuer password if passed in the flags. if options.CertificateIssuer != nil && a.issuerPassword != nil { options.CertificateIssuer.Password = string(a.issuerPassword) @@ -481,24 +512,13 @@ func (a *Authority) init() error { // Initialize step-ca Admin Database if it's not already initialized using // WithAdminDB. if a.adminDB == nil { - if a.linkedCAToken == "" { - // Check if AuthConfig already exists - a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID) - if err != nil { - return err - } + if linkedcaClient != nil { + a.adminDB = linkedcaClient } else { - // Use the linkedca client as the admindb. - client, err := newLinkedCAClient(a.linkedCAToken) + a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID) if err != nil { return err } - // If authorityId is configured make sure it matches the one in the token - if id := a.config.AuthorityConfig.AuthorityID; id != "" && !strings.EqualFold(id, client.authorityID) { - return errors.New("error initializing linkedca: token authority and configured authority do not match") - } - client.Run() - a.adminDB = client } } diff --git a/authority/linkedca.go b/authority/linkedca.go index 00d5ceef..6a0800c2 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -152,13 +152,21 @@ func (c *linkedCaClient) GetProvisioner(ctx context.Context, id string) (*linked } func (c *linkedCaClient) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) { + resp, err := c.GetConfiguration(ctx) + if err != nil { + return nil, err + } + return resp.Provisioners, nil +} + +func (c *linkedCaClient) GetConfiguration(ctx context.Context) (*linkedca.ConfigurationResponse, error) { resp, err := c.client.GetConfiguration(ctx, &linkedca.ConfigurationRequest{ AuthorityId: c.authorityID, }) if err != nil { - return nil, errors.Wrap(err, "error getting provisioners") + return nil, errors.Wrap(err, "error getting configuration") } - return resp.Provisioners, nil + return resp, nil } func (c *linkedCaClient) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { @@ -205,11 +213,9 @@ func (c *linkedCaClient) GetAdmin(ctx context.Context, id string) (*linkedca.Adm } func (c *linkedCaClient) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) { - resp, err := c.client.GetConfiguration(ctx, &linkedca.ConfigurationRequest{ - AuthorityId: c.authorityID, - }) + resp, err := c.GetConfiguration(ctx) if err != nil { - return nil, errors.Wrap(err, "error getting admins") + return nil, err } return resp.Admins, nil } From 6851842841732cd8bf220bf82af09f240ff89335 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 28 Mar 2022 15:06:56 -0700 Subject: [PATCH 051/241] Fix unit tests. --- authority/authorize_test.go | 2 +- authority/provisioner/acme_test.go | 3 ++- authority/provisioner/aws_test.go | 11 ++++++----- authority/provisioner/azure_test.go | 11 ++++++----- authority/provisioner/gcp_test.go | 7 ++++--- authority/provisioner/jwk_test.go | 3 ++- authority/provisioner/k8sSA_test.go | 3 ++- authority/provisioner/noop_test.go | 2 +- authority/provisioner/oidc_test.go | 3 ++- authority/provisioner/x5c_test.go | 3 ++- 10 files changed, 28 insertions(+), 20 deletions(-) diff --git a/authority/authorize_test.go b/authority/authorize_test.go index b631741a..6a35c4e1 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -488,7 +488,7 @@ func TestAuthority_authorizeSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Len(t, 7, got) + assert.Len(t, 8, got) } } }) diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index bc4e97e0..8ab29b26 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -175,9 +175,10 @@ 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) for _, o := range opts { switch v := o.(type) { + case *ACME: case *provisionerExtensionOption: assert.Equals(t, v.Type, TypeACME) assert.Equals(t, v.Name, tc.p.GetName()) diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 559a48f1..f17ec231 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}, @@ -675,6 +675,7 @@ func TestAWS_AuthorizeSign(t *testing.T) { assert.Len(t, tt.wantLen, got) for _, o := range got { switch v := o.(type) { + case *AWS: case certificateOptionsFunc: case *provisionerExtensionOption: assert.Equals(t, v.Type, TypeAWS) diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index 40bb4698..28de642c 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -473,11 +473,11 @@ 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", p5, args{t5}, 5, http.StatusOK, false}, - {"ok", p7, args{t7}, 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}, + {"ok", p5, args{t5}, 6, http.StatusOK, false}, + {"ok", p7, args{t7}, 6, http.StatusOK, false}, {"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true}, {"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true}, {"fail subscription", p6, args{t6}, 0, http.StatusUnauthorized, true}, @@ -504,6 +504,7 @@ func TestAzure_AuthorizeSign(t *testing.T) { assert.Len(t, tt.wantLen, got) for _, o := range got { switch v := o.(type) { + case *Azure: case certificateOptionsFunc: case *provisionerExtensionOption: assert.Equals(t, v.Type, TypeAzure) diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index b8c437c3..7d3bd073 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}, @@ -547,6 +547,7 @@ func TestGCP_AuthorizeSign(t *testing.T) { assert.Len(t, tt.wantLen, got) for _, o := range got { switch v := o.(type) { + case *GCP: case certificateOptionsFunc: case *provisionerExtensionOption: assert.Equals(t, v.Type, TypeGCP) diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index dde2f836..f9000961 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -295,9 +295,10 @@ 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 *JWK: case certificateOptionsFunc: case *provisionerExtensionOption: assert.Equals(t, v.Type, TypeJWK) diff --git a/authority/provisioner/k8sSA_test.go b/authority/provisioner/k8sSA_test.go index 378d4471..e6305f6e 100644 --- a/authority/provisioner/k8sSA_test.go +++ b/authority/provisioner/k8sSA_test.go @@ -281,6 +281,7 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { tot := 0 for _, o := range opts { switch v := o.(type) { + case *K8sSA: case certificateOptionsFunc: case *provisionerExtensionOption: assert.Equals(t, v.Type, TypeK8sSA) @@ -298,7 +299,7 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { } tot++ } - assert.Equals(t, tot, 5) + assert.Equals(t, tot, 6) } } } diff --git a/authority/provisioner/noop_test.go b/authority/provisioner/noop_test.go index 19e4d235..b10d1d29 100644 --- a/authority/provisioner/noop_test.go +++ b/authority/provisioner/noop_test.go @@ -24,6 +24,6 @@ func Test_noop(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignMethod) sigOptions, err := p.AuthorizeSign(ctx, "foo") - assert.Equals(t, []SignOption{}, sigOptions) + assert.Equals(t, []SignOption{&p}, sigOptions) assert.Equals(t, nil, err) } diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index c1a94b1d..bfa46027 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -322,9 +322,10 @@ 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 *OIDC: case certificateOptionsFunc: case *provisionerExtensionOption: assert.Equals(t, v.Type, TypeOIDC) diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 7932d045..91e789a1 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -466,9 +466,10 @@ 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.Equals(t, len(opts), 8) for _, o := range opts { switch v := o.(type) { + case *X5C: case certificateOptionsFunc: case *provisionerExtensionOption: assert.Equals(t, v.Type, TypeX5C) From 0b388942e8a7cdc2ed1a530f007aeee77051f015 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 28 Mar 2022 18:23:36 -0700 Subject: [PATCH 052/241] Upgrade linkedca package. --- go.mod | 4 ++-- go.sum | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 8f8c3143..16007f5b 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.0 go.step.sm/crypto v0.15.3 - go.step.sm/linkedca v0.11.0 + go.step.sm/linkedca v0.12.0 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd google.golang.org/api v0.70.0 @@ -50,4 +50,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 47f84801..108cfec9 100644 --- a/go.sum +++ b/go.sum @@ -709,6 +709,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.15.3 h1:f3GMl+aCydt294BZRjTYwpaXRqwwndvoTY2NLN4wu10= go.step.sm/crypto v0.15.3/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= +go.step.sm/linkedca v0.12.0 h1:FA18uJO5P6W2pklcezMs+w+N3dVbpKEE1LP9HLsJgg4= +go.step.sm/linkedca v0.12.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 0e052fe2993d2fa398b013e0ffef845eeb35661d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 30 Mar 2022 14:21:39 +0200 Subject: [PATCH 053/241] 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 | 114 ++++++---------------- authority/authority.go | 1 + authority/policy.go | 82 ++++++++++++---- ca/adminClient.go | 129 +++++++++++++++++++++++++ 9 files changed, 280 insertions(+), 125 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)) - return - } + prov := linkedca.ProvisionerFromContext(ctx) - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) - if err != nil { - api.WriteError(w, err) + if prov.Policy == nil { + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } - var policy = new(linkedca.Policy) - if err := read.ProtoJSON(r.Body, policy); err != nil { - api.WriteError(w, err) + var newPolicy = new(linkedca.Policy) + if !api.ReadProtoJSONWithCheck(w, r.Body, newPolicy) { return } - prov.Policy = policy - err = par.auth.UpdateProvisioner(ctx, prov) + 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 } - 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 054/241] 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 055/241] 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 056/241] Improve middleware test coverage --- authority/admin/api/acme.go | 53 ++-- 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, 205 insertions(+), 340 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,48 +34,27 @@ 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) - 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()) - } + if !acmeProvisioner.RequireEab { + render.Error(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner '%s'", prov.GetName())) + return + } - acmeProvisioner := details.GetACME() - if acmeProvisioner == nil { - return false, nil, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID()) + next(w, r.WithContext(ctx)) } - - return acmeProvisioner.GetRequireEab(), prov, nil } type acmeAdminResponderInterface interface { 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, - 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" - }, - }, nil - }, + "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, + err: err, + statusCode: 500, } - 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, - }, - }, + }, + "ok/eab-disabled": func(t *testing.T) test { + prov := &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" - }, - }, 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, - }, - }, + prov := &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 057/241] 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 058/241] 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 059/241] 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 060/241] 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 061/241] 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 062/241] 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 df8ffb35afef1dfc4cecedfa77ea03ee7ef190fb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 5 Apr 2022 17:39:06 -0700 Subject: [PATCH 063/241] Remove unnecessary database in provisioner config. --- authority/provisioner/provisioner.go | 3 --- authority/provisioners.go | 1 - 2 files changed, 4 deletions(-) diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 7438ea17..0d5cd41a 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/pkg/errors" - "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" "golang.org/x/crypto/ssh" ) @@ -212,8 +211,6 @@ type Config struct { Claims Claims // Audiences are the audiences used in the default provisioner, (JWK). Audiences Audiences - // DB is the interface to the authority DB client. - DB db.AuthDB // SSHKeys are the root SSH public keys SSHKeys *SSHKeys // GetIdentityFunc is a function that returns an identity that will be diff --git a/authority/provisioners.go b/authority/provisioners.go index a6ac5aa8..496421f5 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -103,7 +103,6 @@ func (a *Authority) generateProvisionerConfig(ctx context.Context) (provisioner. return provisioner.Config{ Claims: claimer.Claims(), Audiences: a.config.GetAudiences(), - DB: a.db, SSHKeys: &provisioner.SSHKeys{ UserKeys: sshKeys.UserKeys, HostKeys: sshKeys.HostKeys, From 41c6ded85e537c1f1a31080f3cbb8e1a9e364cdc Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 5 Apr 2022 18:00:01 -0700 Subject: [PATCH 064/241] Store in the db the provisioner that granted a cert. --- db/db.go | 50 +++++++++++++++++++++++------ db/db_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 9 deletions(-) diff --git a/db/db.go b/db/db.go index 6d48723f..3427d2bb 100644 --- a/db/db.go +++ b/db/db.go @@ -8,20 +8,22 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/nosql" "github.com/smallstep/nosql/database" "golang.org/x/crypto/ssh" ) var ( - certsTable = []byte("x509_certs") - revokedCertsTable = []byte("revoked_x509_certs") - revokedSSHCertsTable = []byte("revoked_ssh_certs") - usedOTTTable = []byte("used_ott") - sshCertsTable = []byte("ssh_certs") - sshHostsTable = []byte("ssh_hosts") - sshUsersTable = []byte("ssh_users") - sshHostPrincipalsTable = []byte("ssh_host_principals") + certsTable = []byte("x509_certs") + certsToProvisionerTable = []byte("x509_certs_provisioner") + revokedCertsTable = []byte("revoked_x509_certs") + revokedSSHCertsTable = []byte("revoked_ssh_certs") + usedOTTTable = []byte("used_ott") + sshCertsTable = []byte("ssh_certs") + sshHostsTable = []byte("ssh_hosts") + sshUsersTable = []byte("ssh_users") + sshHostPrincipalsTable = []byte("ssh_host_principals") ) // ErrAlreadyExists can be returned if the DB attempts to set a key that has @@ -82,7 +84,7 @@ func New(c *Config) (AuthDB, error) { tables := [][]byte{ revokedCertsTable, certsTable, usedOTTTable, sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, - revokedSSHCertsTable, + revokedSSHCertsTable, certsToProvisionerTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { @@ -210,6 +212,36 @@ func (db *DB) StoreCertificate(crt *x509.Certificate) error { return nil } +type certsToProvionersData struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +// StoreCertificateChain stores the leaf certificate and the provisioner that +// authorized the certificate. +func (d *DB) StoreCertificateChain(p provisioner.Interface, chain ...*x509.Certificate) error { + leaf := chain[0] + if err := d.StoreCertificate(leaf); err != nil { + return err + } + if p != nil { + b, err := json.Marshal(certsToProvionersData{ + ID: p.GetID(), + Name: p.GetName(), + Type: p.GetType().String(), + }) + if err != nil { + return errors.Wrap(err, "error marshaling json") + } + + if err := d.Set(certsToProvisionerTable, []byte(leaf.SerialNumber.String()), b); err != nil { + return errors.Wrap(err, "database Set error") + } + } + return nil +} + // UseToken returns true if we were able to successfully store the token for // for the first time, false otherwise. func (db *DB) UseToken(id, tok string) (bool, error) { diff --git a/db/db_test.go b/db/db_test.go index 40f59215..5a7e2d38 100644 --- a/db/db_test.go +++ b/db/db_test.go @@ -1,10 +1,14 @@ package db import ( + "crypto/x509" "errors" + "math/big" "testing" "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/nosql" "github.com/smallstep/nosql/database" ) @@ -158,3 +162,87 @@ func TestUseToken(t *testing.T) { }) } } + +func TestDB_StoreCertificateChain(t *testing.T) { + p := &provisioner.JWK{ + ID: "some-id", + Name: "admin", + Type: "JWK", + } + chain := []*x509.Certificate{ + {Raw: []byte("the certificate"), SerialNumber: big.NewInt(1234)}, + } + type fields struct { + DB nosql.DB + isUp bool + } + type args struct { + p provisioner.Interface + chain []*x509.Certificate + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + {"ok", fields{&MockNoSQLDB{ + MSet: func(bucket, key, value []byte) error { + switch string(bucket) { + case "x509_certs": + assert.Equals(t, key, []byte("1234")) + assert.Equals(t, value, []byte("the certificate")) + case "x509_certs_provisioner": + assert.Equals(t, key, []byte("1234")) + assert.Equals(t, value, []byte(`{"id":"some-id","name":"admin","type":"JWK"}`)) + default: + t.Errorf("unexpected bucket %s", bucket) + } + return nil + }, + }, true}, args{p, chain}, false}, + {"ok no provisioner", fields{&MockNoSQLDB{ + MSet: func(bucket, key, value []byte) error { + switch string(bucket) { + case "x509_certs": + assert.Equals(t, key, []byte("1234")) + assert.Equals(t, value, []byte("the certificate")) + default: + t.Errorf("unexpected bucket %s", bucket) + } + return nil + }, + }, true}, args{nil, chain}, false}, + {"fail store certificate", fields{&MockNoSQLDB{ + MSet: func(bucket, key, value []byte) error { + switch string(bucket) { + case "x509_certs": + return errors.New("test error") + default: + return nil + } + }, + }, true}, args{p, chain}, true}, + {"fail store provisioner", fields{&MockNoSQLDB{ + MSet: func(bucket, key, value []byte) error { + switch string(bucket) { + case "x509_certs_provisioner": + return errors.New("test error") + default: + return nil + } + }, + }, true}, args{p, chain}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DB{ + DB: tt.fields.DB, + isUp: tt.fields.isUp, + } + if err := d.StoreCertificateChain(tt.args.p, tt.args.chain...); (err != nil) != tt.wantErr { + t.Errorf("DB.StoreCertificateChain() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From 7d6116c3d052bef1f2e723e67e177280b621f3c4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 5 Apr 2022 19:24:53 -0700 Subject: [PATCH 065/241] Add GetCertificateData and refactor x509_certs_data. --- db/db.go | 63 +++++++++++++++++++++++++++++++--------------- db/db_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 23 deletions(-) diff --git a/db/db.go b/db/db.go index 3427d2bb..a3ebb19f 100644 --- a/db/db.go +++ b/db/db.go @@ -15,15 +15,15 @@ import ( ) var ( - certsTable = []byte("x509_certs") - certsToProvisionerTable = []byte("x509_certs_provisioner") - revokedCertsTable = []byte("revoked_x509_certs") - revokedSSHCertsTable = []byte("revoked_ssh_certs") - usedOTTTable = []byte("used_ott") - sshCertsTable = []byte("ssh_certs") - sshHostsTable = []byte("ssh_hosts") - sshUsersTable = []byte("ssh_users") - sshHostPrincipalsTable = []byte("ssh_host_principals") + certsTable = []byte("x509_certs") + certsDataTable = []byte("x509_certs_data") + revokedCertsTable = []byte("revoked_x509_certs") + revokedSSHCertsTable = []byte("revoked_ssh_certs") + usedOTTTable = []byte("used_ott") + sshCertsTable = []byte("ssh_certs") + sshHostsTable = []byte("ssh_hosts") + sshUsersTable = []byte("ssh_users") + sshHostPrincipalsTable = []byte("ssh_host_principals") ) // ErrAlreadyExists can be returned if the DB attempts to set a key that has @@ -84,7 +84,7 @@ func New(c *Config) (AuthDB, error) { tables := [][]byte{ revokedCertsTable, certsTable, usedOTTTable, sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, - revokedSSHCertsTable, certsToProvisionerTable, + revokedSSHCertsTable, certsDataTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { @@ -204,6 +204,19 @@ func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) { return cert, nil } +// GetCertificateData returns the data stored for a provisioner +func (db *DB) GetCertificateData(serialNumber string) (*CertificateData, error) { + b, err := db.Get(certsDataTable, []byte(serialNumber)) + if err != nil { + return nil, errors.Wrap(err, "database Get error") + } + var data CertificateData + if err := json.Unmarshal(b, &data); err != nil { + return nil, errors.Wrap(err, "error unmarshaling json") + } + return &data, nil +} + // StoreCertificate stores a certificate PEM. func (db *DB) StoreCertificate(crt *x509.Certificate) error { if err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil { @@ -212,7 +225,15 @@ func (db *DB) StoreCertificate(crt *x509.Certificate) error { return nil } -type certsToProvionersData struct { +// CertificateData is the JSON representation of the data stored in +// x509_certs_data table. +type CertificateData struct { + Provisioner *ProvisionerData `json:"provisioner,omitempty"` +} + +// ProvisionerData is the JSON representation of the provisioner stored in the +// x509_certs_data table. +type ProvisionerData struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` @@ -220,24 +241,26 @@ type certsToProvionersData struct { // StoreCertificateChain stores the leaf certificate and the provisioner that // authorized the certificate. -func (d *DB) StoreCertificateChain(p provisioner.Interface, chain ...*x509.Certificate) error { +func (db *DB) StoreCertificateChain(p provisioner.Interface, chain ...*x509.Certificate) error { leaf := chain[0] - if err := d.StoreCertificate(leaf); err != nil { + if err := db.StoreCertificate(leaf); err != nil { return err } + data := &CertificateData{} if p != nil { - b, err := json.Marshal(certsToProvionersData{ + data.Provisioner = &ProvisionerData{ ID: p.GetID(), Name: p.GetName(), Type: p.GetType().String(), - }) - if err != nil { - return errors.Wrap(err, "error marshaling json") } + } - if err := d.Set(certsToProvisionerTable, []byte(leaf.SerialNumber.String()), b); err != nil { - return errors.Wrap(err, "database Set error") - } + b, err := json.Marshal(data) + if err != nil { + return errors.Wrap(err, "error marshaling json") + } + if err := db.Set(certsDataTable, []byte(leaf.SerialNumber.String()), b); err != nil { + return errors.Wrap(err, "database Set error") } return nil } diff --git a/db/db_test.go b/db/db_test.go index 5a7e2d38..d7c58c9c 100644 --- a/db/db_test.go +++ b/db/db_test.go @@ -4,6 +4,7 @@ import ( "crypto/x509" "errors" "math/big" + "reflect" "testing" "github.com/smallstep/assert" @@ -192,9 +193,9 @@ func TestDB_StoreCertificateChain(t *testing.T) { case "x509_certs": assert.Equals(t, key, []byte("1234")) assert.Equals(t, value, []byte("the certificate")) - case "x509_certs_provisioner": + case "x509_certs_data": assert.Equals(t, key, []byte("1234")) - assert.Equals(t, value, []byte(`{"id":"some-id","name":"admin","type":"JWK"}`)) + assert.Equals(t, value, []byte(`{"provisioner":{"id":"some-id","name":"admin","type":"JWK"}}`)) default: t.Errorf("unexpected bucket %s", bucket) } @@ -207,6 +208,9 @@ func TestDB_StoreCertificateChain(t *testing.T) { case "x509_certs": assert.Equals(t, key, []byte("1234")) assert.Equals(t, value, []byte("the certificate")) + case "x509_certs_data": + assert.Equals(t, key, []byte("1234")) + assert.Equals(t, value, []byte(`{}`)) default: t.Errorf("unexpected bucket %s", bucket) } @@ -226,7 +230,7 @@ func TestDB_StoreCertificateChain(t *testing.T) { {"fail store provisioner", fields{&MockNoSQLDB{ MSet: func(bucket, key, value []byte) error { switch string(bucket) { - case "x509_certs_provisioner": + case "x509_certs_data": return errors.New("test error") default: return nil @@ -246,3 +250,63 @@ func TestDB_StoreCertificateChain(t *testing.T) { }) } } + +func TestDB_GetCertificateData(t *testing.T) { + type fields struct { + DB nosql.DB + isUp bool + } + type args struct { + serialNumber string + } + tests := []struct { + name string + fields fields + args args + want *CertificateData + wantErr bool + }{ + {"ok", fields{&MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, []byte("x509_certs_data")) + assert.Equals(t, key, []byte("1234")) + return []byte(`{"provisioner":{"id":"some-id","name":"admin","type":"JWK"}}`), nil + }, + }, true}, args{"1234"}, &CertificateData{ + Provisioner: &ProvisionerData{ + ID: "some-id", Name: "admin", Type: "JWK", + }, + }, false}, + {"fail not found", fields{&MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + return nil, database.ErrNotFound + }, + }, true}, args{"1234"}, nil, true}, + {"fail db", fields{&MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + return nil, errors.New("an error") + }, + }, true}, args{"1234"}, nil, true}, + {"fail unmarshal", fields{&MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + return []byte(`{"bad-json"}`), nil + }, + }, true}, args{"1234"}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db := &DB{ + DB: tt.fields.DB, + isUp: tt.fields.isUp, + } + got, err := db.GetCertificateData(tt.args.serialNumber) + if (err != nil) != tt.wantErr { + t.Errorf("DB.GetCertificateData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("DB.GetCertificateData() = %v, want %v", got, tt.want) + } + }) + } +} From db337debcdc6ca59cde20900d114f0f48bc12a33 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 5 Apr 2022 19:25:47 -0700 Subject: [PATCH 066/241] Load provisioner from the database instead of the extension. --- authority/provisioners.go | 34 +++++++++++++++++++++++++++++----- authority/tls.go | 2 ++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/authority/provisioners.go b/authority/provisioners.go index 496421f5..c0eec520 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -13,6 +13,7 @@ import ( "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/config" "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" @@ -44,13 +45,36 @@ func (a *Authority) GetProvisioners(cursor string, limit int) (provisioner.List, // LoadProvisionerByCertificate returns an interface to the provisioner that // provisioned the certificate. func (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisioner.Interface, error) { + // Default implementation looks at the provisioner extension. + loadProvisioner := func() (provisioner.Interface, error) { + p, ok := a.provisioners.LoadByCertificate(crt) + if !ok { + return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate") + } + return p, nil + } + + // Attempt to load the provisioner using the linked db + // TODO:(mariano) + + // Attempt to load the provisioner from the db + if db, ok := a.db.(interface { + GetCertificateData(string) (*db.CertificateData, error) + }); ok { + if data, err := db.GetCertificateData(crt.SerialNumber.String()); err == nil && data.Provisioner != nil { + loadProvisioner = func() (provisioner.Interface, error) { + p, ok := a.provisioners.Load(data.Provisioner.ID) + if !ok { + return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate") + } + return p, nil + } + } + } + a.adminMutex.RLock() defer a.adminMutex.RUnlock() - p, ok := a.provisioners.LoadByCertificate(crt) - if !ok { - return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate") - } - return p, nil + return loadProvisioner() } // LoadProvisionerByToken returns an interface to the provisioner that diff --git a/authority/tls.go b/authority/tls.go index 93bc0408..bebcdf1b 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -347,6 +347,8 @@ func (a *Authority) storeCertificate(prov provisioner.Interface, fullchain []*x5 // Store certificate in local db switch s := a.db.(type) { + case linkedChainStorer: + return s.StoreCertificateChain(prov, fullchain...) case certificateChainStorer: return s.StoreCertificateChain(fullchain...) default: From 479c6d2bf563fcc0073779e5f736823d135a3fe2 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 7 Apr 2022 12:37:34 +0200 Subject: [PATCH 067/241] Fix ACME IPv6 HTTP-01 challenges Fixes #890 --- acme/challenge.go | 13 ++++++++++++- acme/challenge_test.go | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/acme/challenge.go b/acme/challenge.go index 0e1994e4..9f08bae5 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -79,7 +79,7 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, } func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { - u := &url.URL{Scheme: "http", Host: ch.Value, Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} + u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} resp, err := vo.HTTPGet(u.String()) if err != nil { @@ -119,6 +119,17 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb return nil } +// http01ChallengeHost checks if a Challenge value is an IPv6 address +// and adds square brackets if that's the case, so that it can be used +// as a hostname. Returns the original Challenge value as the host to +// use in other cases. +func http01ChallengeHost(value string) string { + if ip := net.ParseIP(value); ip != nil && ip.To4() == nil { + value = "[" + value + "]" + } + return value +} + func tlsAlert(err error) uint8 { var opErr *net.OpError if errors.As(err, &opErr) { diff --git a/acme/challenge_test.go b/acme/challenge_test.go index d8ce4d76..c05b25e7 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -13,6 +13,7 @@ import ( "encoding/asn1" "encoding/base64" "encoding/hex" + "errors" "fmt" "io" "math/big" @@ -23,9 +24,9 @@ import ( "testing" "time" - "github.com/pkg/errors" - "github.com/smallstep/assert" "go.step.sm/crypto/jose" + + "github.com/smallstep/assert" ) func Test_storeError(t *testing.T) { @@ -2350,3 +2351,34 @@ func Test_serverName(t *testing.T) { }) } } + +func Test_http01ChallengeHost(t *testing.T) { + tests := []struct { + name string + value string + want string + }{ + { + name: "dns", + value: "www.example.com", + want: "www.example.com", + }, + { + name: "ipv4", + value: "127.0.0.1", + want: "127.0.0.1", + }, + { + name: "ipv6", + value: "::1", + want: "[::1]", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := http01ChallengeHost(tt.value); got != tt.want { + t.Errorf("http01ChallengeHost() = %v, want %v", got, tt.want) + } + }) + } +} From 7df52dbb767b49312a2ab012de97f6d42e0de461 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 7 Apr 2022 14:11:53 +0200 Subject: [PATCH 068/241] 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 c55b27a2fc4420e4b6647ee264b31b5fc38cd675 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 Apr 2022 18:14:43 -0700 Subject: [PATCH 069/241] Refactor admin token to use with RAs. --- authority/authorize.go | 13 +++++------- authority/config/config.go | 12 +++++++++++ authority/config/config_test.go | 36 ++++++++++++++++++++++++++++++++ authority/linkedca.go | 22 ++++++++++++++++++++ authority/provisioners.go | 19 ++++++++++------- ca/adminClient.go | 37 +++++++++++++++++---------------- ca/client.go | 21 +++++++------------ go.mod | 2 +- go.sum | 2 -- 9 files changed, 114 insertions(+), 50 deletions(-) diff --git a/authority/authorize.go b/authority/authorize.go index 7c1c2ff6..0c1c6f22 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -130,22 +130,19 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc // According to "rfc7519 JSON Web Token" acceptable skew should be no // more than a few minutes. if err := claims.ValidateWithLeeway(jose.Expected{ - Issuer: prov.GetName(), + Issuer: "step-admin-client/1.0", Time: time.Now().UTC(), }, time.Minute); err != nil { return nil, admin.WrapError(admin.ErrorUnauthorizedType, err, "x5c.authorizeToken; invalid x5c claims") } // validate audience: path matches the current path - if r.URL.Path != claims.Audience[0] { - return nil, admin.NewError(admin.ErrorUnauthorizedType, - "x5c.authorizeToken; x5c token has invalid audience "+ - "claim (aud); expected %s, but got %s", r.URL.Path, claims.Audience) + if !matchesAudience(claims.Audience, a.config.Audience(r.URL.Path)) { + return nil, admin.NewError(admin.ErrorUnauthorizedType, "x5c.authorizeToken; x5c token has invalid audience claim (aud)") } if claims.Subject == "" { - return nil, admin.NewError(admin.ErrorUnauthorizedType, - "x5c.authorizeToken; x5c token subject cannot be empty") + return nil, admin.NewError(admin.ErrorUnauthorizedType, "x5c.authorizeToken; x5c token subject cannot be empty") } var ( @@ -156,7 +153,7 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc adminSANs := append([]string{leaf.Subject.CommonName}, leaf.DNSNames...) adminSANs = append(adminSANs, leaf.EmailAddresses...) for _, san := range adminSANs { - if adm, ok = a.LoadAdminBySubProv(san, claims.Issuer); ok { + if adm, ok = a.LoadAdminBySubProv(san, prov.GetName()); ok { adminFound = true break } diff --git a/authority/config/config.go b/authority/config/config.go index 2c437725..4ed7a1cb 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -304,6 +304,18 @@ func (c *Config) GetAudiences() provisioner.Audiences { return audiences } +// Audience returns the list of audiences for a given path. +func (c *Config) Audience(path string) []string { + audiences := make([]string, len(c.DNSNames)+1) + for i, name := range c.DNSNames { + hostname := toHostname(name) + audiences[i] = "https://" + hostname + path + } + // For backward compatibility + audiences[len(c.DNSNames)] = path + return audiences +} + func toHostname(name string) string { // ensure an IPv6 address is represented with square brackets when used as hostname if ip := net.ParseIP(name); ip != nil && ip.To4() == nil { diff --git a/authority/config/config_test.go b/authority/config/config_test.go index b921be13..5a05b3f6 100644 --- a/authority/config/config_test.go +++ b/authority/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "reflect" "testing" "github.com/pkg/errors" @@ -317,3 +318,38 @@ func Test_toHostname(t *testing.T) { }) } } + +func TestConfig_Audience(t *testing.T) { + type fields struct { + DNSNames []string + } + type args struct { + path string + } + tests := []struct { + name string + fields fields + args args + want []string + }{ + {"ok", fields{[]string{ + "ca", "ca.example.com", "127.0.0.1", "::1", + }}, args{"/path"}, []string{ + "https://ca/path", + "https://ca.example.com/path", + "https://127.0.0.1/path", + "https://[::1]/path", + "/path", + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{ + DNSNames: tt.fields.DNSNames, + } + if got := c.Audience(tt.args.path); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Config.Audience() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/linkedca.go b/authority/linkedca.go index 6a0800c2..1b920ce8 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -235,6 +235,28 @@ func (c *linkedCaClient) DeleteAdmin(ctx context.Context, id string) error { return errors.Wrap(err, "error deleting admin") } +func (c *linkedCaClient) GetCertificateData(serial string) (*db.CertificateData, error) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + resp, err := c.client.GetCertificate(ctx, &linkedca.GetCertificateRequest{ + Serial: serial, + }) + if err != nil { + return nil, err + } + + var provisioner *db.ProvisionerData + if p := resp.Provisioner; p != nil { + provisioner = &db.ProvisionerData{ + ID: p.Id, Name: p.Name, Type: p.Type.String(), + } + } + return &db.CertificateData{ + Provisioner: provisioner, + }, nil +} + func (c *linkedCaClient) StoreCertificateChain(prov provisioner.Interface, fullchain ...*x509.Certificate) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() diff --git a/authority/provisioners.go b/authority/provisioners.go index c0eec520..3713705f 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -54,14 +54,19 @@ func (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisi return p, nil } - // Attempt to load the provisioner using the linked db - // TODO:(mariano) - - // Attempt to load the provisioner from the db - if db, ok := a.db.(interface { + // certificateDataGetter is an interface that can be use to retrieve the + // provisioner from a db or a linked ca. + type certificateDataGetter interface { GetCertificateData(string) (*db.CertificateData, error) - }); ok { - if data, err := db.GetCertificateData(crt.SerialNumber.String()); err == nil && data.Provisioner != nil { + } + var cdg certificateDataGetter + if getter, ok := a.adminDB.(certificateDataGetter); ok { + cdg = getter + } else if getter, ok := a.db.(certificateDataGetter); ok { + cdg = getter + } + if cdg != nil { + if data, err := cdg.GetCertificateData(crt.SerialNumber.String()); err == nil && data.Provisioner != nil { loadProvisioner = func() (provisioner.Interface, error) { p, ok := a.provisioners.Load(data.Provisioner.ID) if !ok { diff --git a/ca/adminClient.go b/ca/adminClient.go index 5f3993b1..c3ba666f 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -23,7 +23,10 @@ import ( "google.golang.org/protobuf/encoding/protojson" ) -var adminURLPrefix = "admin" +const ( + adminURLPrefix = "admin" + adminIssuer = "step-admin-client/1.0" +) // AdminClient implements an HTTP client for the CA server. type AdminClient struct { @@ -35,7 +38,6 @@ type AdminClient struct { x5cCertFile string x5cCertStrs []string x5cCert *x509.Certificate - x5cIssuer string x5cSubject string } @@ -77,12 +79,11 @@ func NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) x5cCertFile: o.x5cCertFile, x5cCertStrs: o.x5cCertStrs, x5cCert: o.x5cCert, - x5cIssuer: o.x5cIssuer, x5cSubject: o.x5cSubject, }, nil } -func (c *AdminClient) generateAdminToken(urlPath string) (string, error) { +func (c *AdminClient) generateAdminToken(aud *url.URL) (string, error) { // A random jwt id will be used to identify duplicated tokens jwtID, err := randutil.Hex(64) // 256 bits if err != nil { @@ -93,8 +94,8 @@ func (c *AdminClient) generateAdminToken(urlPath string) (string, error) { tokOptions := []token.Options{ token.WithJWTID(jwtID), token.WithKid(c.x5cJWK.KeyID), - token.WithIssuer(c.x5cIssuer), - token.WithAudience(urlPath), + token.WithIssuer(adminIssuer), + token.WithAudience(aud.String()), token.WithValidity(now, now.Add(token.DefaultValidity)), token.WithX5CCerts(c.x5cCertStrs), } @@ -205,7 +206,7 @@ func (c *AdminClient) GetAdminsPaginate(opts ...AdminOption) (*adminAPI.GetAdmin Path: "/admin/admins", RawQuery: o.rawQuery(), }) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -260,7 +261,7 @@ func (c *AdminClient) CreateAdmin(createAdminRequest *adminAPI.CreateAdminReques return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/admin/admins"}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -292,7 +293,7 @@ retry: func (c *AdminClient) RemoveAdmin(id string) error { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "admins", id)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return errors.Wrapf(err, "error generating admin token") } @@ -324,7 +325,7 @@ func (c *AdminClient) UpdateAdmin(id string, uar *adminAPI.UpdateAdminRequest) ( return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "admins", id)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -371,7 +372,7 @@ func (c *AdminClient) GetProvisioner(opts ...ProvisionerOption) (*linkedca.Provi default: return nil, errors.New("must set either name or id in method options") } - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -410,7 +411,7 @@ func (c *AdminClient) GetProvisionersPaginate(opts ...ProvisionerOption) (*admin Path: "/admin/provisioners", RawQuery: o.rawQuery(), }) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -480,7 +481,7 @@ func (c *AdminClient) RemoveProvisioner(opts ...ProvisionerOption) error { default: return errors.New("must set either name or id in method options") } - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return errors.Wrapf(err, "error generating admin token") } @@ -512,7 +513,7 @@ func (c *AdminClient) CreateProvisioner(prov *linkedca.Provisioner) (*linkedca.P return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -548,7 +549,7 @@ func (c *AdminClient) UpdateProvisioner(name string, prov *linkedca.Provisioner) return errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", name)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return errors.Wrapf(err, "error generating admin token") } @@ -587,7 +588,7 @@ func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName, reference Path: p, RawQuery: o.rawQuery(), }) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -623,7 +624,7 @@ func (c *AdminClient) CreateExternalAccountKey(provisionerName string, eakReques return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab/", provisionerName)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -655,7 +656,7 @@ retry: func (c *AdminClient) RemoveExternalAccountKey(provisionerName, keyID string) error { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab", provisionerName, "/", keyID)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return errors.Wrapf(err, "error generating admin token") } diff --git a/ca/client.go b/ca/client.go index 3a36fcd6..4aa66aac 100644 --- a/ca/client.go +++ b/ca/client.go @@ -116,7 +116,6 @@ type clientOptions struct { x5cCertFile string x5cCertStrs []string x5cCert *x509.Certificate - x5cIssuer string x5cSubject string } @@ -332,19 +331,13 @@ func WithAdminX5C(certs []*x509.Certificate, key interface{}, passwordFile strin } o.x5cCert = certs[0] - o.x5cSubject = o.x5cCert.Subject.CommonName - - for _, e := range o.x5cCert.Extensions { - if e.Id.Equal(stepOIDProvisioner) { - var prov stepProvisionerASN1 - if _, err := asn1.Unmarshal(e.Value, &prov); err != nil { - return errors.Wrap(err, "error unmarshaling provisioner OID from certificate") - } - o.x5cIssuer = string(prov.Name) - } - } - if o.x5cIssuer == "" { - return errors.New("provisioner extension not found in certificate") + switch leaf := certs[0]; { + case leaf.Subject.CommonName != "": + o.x5cSubject = leaf.Subject.CommonName + case len(leaf.DNSNames) > 0: + o.x5cSubject = leaf.DNSNames[0] + case len(leaf.EmailAddresses) > 0: + o.x5cSubject = leaf.EmailAddresses[0] } return nil diff --git a/go.mod b/go.mod index 16007f5b..85d21ceb 100644 --- a/go.mod +++ b/go.mod @@ -50,4 +50,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 108cfec9..47f84801 100644 --- a/go.sum +++ b/go.sum @@ -709,8 +709,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.3 h1:f3GMl+aCydt294BZRjTYwpaXRqwwndvoTY2NLN4wu10= go.step.sm/crypto v0.15.3/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= -go.step.sm/linkedca v0.12.0 h1:FA18uJO5P6W2pklcezMs+w+N3dVbpKEE1LP9HLsJgg4= -go.step.sm/linkedca v0.12.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 d4013f0df63820728f9a515eedeba5c984e20e34 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 Apr 2022 18:19:56 -0700 Subject: [PATCH 070/241] Update linkedca --- go.mod | 4 ++-- go.sum | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 17ea33fe..01ea550b 100644 --- a/go.mod +++ b/go.mod @@ -33,12 +33,12 @@ require ( github.com/slackhq/nebula v1.5.2 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 github.com/smallstep/nosql v0.4.0 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 github.com/urfave/cli v1.22.4 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.0 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd google.golang.org/api v0.70.0 diff --git a/go.sum b/go.sum index e7ddd660..42577048 100644 --- a/go.sum +++ b/go.sum @@ -664,8 +664,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= @@ -710,8 +711,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.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.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 cca5679a111050548a6f98dd794e1dc72aedc63b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 Apr 2022 18:29:38 -0700 Subject: [PATCH 071/241] Use branch dependency for linkedca --- go.mod | 4 ++-- go.sum | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 6a834552..9c0da54d 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,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.12.0 + go.step.sm/linkedca v0.12.1-0.20220408003202-c7e1ed60beab golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd google.golang.org/api v0.70.0 @@ -51,4 +51,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 ba718b16..5dbb5735 100644 --- a/go.sum +++ b/go.sum @@ -711,6 +711,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.12.1-0.20220408003202-c7e1ed60beab h1:s7IZreqiQONduJQfHcv/SI0rqQty3NKmEuiMESwBZwU= +go.step.sm/linkedca v0.12.1-0.20220408003202-c7e1ed60beab/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 304bb5b97ae5f34b1bacf19db2d4aa1eeeba84f4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 Apr 2022 18:31:41 -0700 Subject: [PATCH 072/241] Remove unused code. --- ca/client.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ca/client.go b/ca/client.go index 4aa66aac..0bd93195 100644 --- a/ca/client.go +++ b/ca/client.go @@ -10,7 +10,6 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" - "encoding/asn1" "encoding/hex" "encoding/json" "encoding/pem" @@ -293,18 +292,6 @@ func WithCertificate(cert tls.Certificate) ClientOption { } } -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 - Name []byte - CredentialID []byte - KeyValuePairs []string `asn1:"optional,omitempty"` -} - // WithAdminX5C will set the given file as the X5C certificate for use // by the client. func WithAdminX5C(certs []*x509.Certificate, key interface{}, passwordFile string) ClientOption { From dfdc9c06ed49c5279330fca19d173a377a085f06 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 Apr 2022 18:33:13 -0700 Subject: [PATCH 073/241] Fix linter error importShadow --- authority/linkedca.go | 6 +++--- authority/provisioners.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authority/linkedca.go b/authority/linkedca.go index 1b920ce8..29201164 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -246,14 +246,14 @@ func (c *linkedCaClient) GetCertificateData(serial string) (*db.CertificateData, return nil, err } - var provisioner *db.ProvisionerData + var pd *db.ProvisionerData if p := resp.Provisioner; p != nil { - provisioner = &db.ProvisionerData{ + pd = &db.ProvisionerData{ ID: p.Id, Name: p.Name, Type: p.Type.String(), } } return &db.CertificateData{ - Provisioner: provisioner, + Provisioner: pd, }, nil } diff --git a/authority/provisioners.go b/authority/provisioners.go index 3713705f..0dab24b9 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -275,7 +275,7 @@ func (a *Authority) RemoveProvisioner(ctx context.Context, id string) error { // CreateFirstProvisioner creates and stores the first provisioner when using // admin database provisioner storage. -func CreateFirstProvisioner(ctx context.Context, db admin.DB, password string) (*linkedca.Provisioner, error) { +func CreateFirstProvisioner(ctx context.Context, adminDB admin.DB, password string) (*linkedca.Provisioner, error) { if password == "" { pass, err := ui.PromptPasswordGenerate("Please enter the password to encrypt your first provisioner, leave empty and we'll generate one") if err != nil { @@ -318,7 +318,7 @@ func CreateFirstProvisioner(ctx context.Context, db admin.DB, password string) ( }, }, } - if err := db.CreateProvisioner(ctx, p); err != nil { + if err := adminDB.CreateProvisioner(ctx, p); err != nil { return nil, admin.WrapErrorISE(err, "error creating provisioner") } return p, nil From 0bb15e16f96d7167c3d2c8ff28fc75385caa86d4 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 8 Apr 2022 16:10:26 +0200 Subject: [PATCH 074/241] 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 e53bd64861d6355c2817295ee137a152f7c17219 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 8 Apr 2022 11:13:42 -0700 Subject: [PATCH 075/241] Use release version of linkedca. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9c0da54d..e7653791 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,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.12.1-0.20220408003202-c7e1ed60beab + go.step.sm/linkedca v0.13.0 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd google.golang.org/api v0.70.0 diff --git a/go.sum b/go.sum index 5dbb5735..034904b9 100644 --- a/go.sum +++ b/go.sum @@ -711,8 +711,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.12.1-0.20220408003202-c7e1ed60beab h1:s7IZreqiQONduJQfHcv/SI0rqQty3NKmEuiMESwBZwU= -go.step.sm/linkedca v0.12.1-0.20220408003202-c7e1ed60beab/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= +go.step.sm/linkedca v0.13.0 h1:bRJEsyy3ZpHsSbDjKtaEBb3/vORzDuhS5+rQ2RsGopg= +go.step.sm/linkedca v0.13.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 1d1e09544785d07b9ff4abe95814b00a2a91786d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 8 Apr 2022 13:06:29 -0700 Subject: [PATCH 076/241] Add tests for LoadProvisionerByCertificate. --- authority/provisioners.go | 52 ++++++------ authority/provisioners_test.go | 147 +++++++++++++++++++++++++++++++++ db/db.go | 12 +++ 3 files changed, 186 insertions(+), 25 deletions(-) diff --git a/authority/provisioners.go b/authority/provisioners.go index 0dab24b9..7ff080ed 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -45,41 +45,43 @@ func (a *Authority) GetProvisioners(cursor string, limit int) (provisioner.List, // LoadProvisionerByCertificate returns an interface to the provisioner that // provisioned the certificate. func (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisioner.Interface, error) { - // Default implementation looks at the provisioner extension. - loadProvisioner := func() (provisioner.Interface, error) { - p, ok := a.provisioners.LoadByCertificate(crt) - if !ok { - return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate") - } + a.adminMutex.RLock() + defer a.adminMutex.RUnlock() + if p, err := a.unsafeLoadProvisionerFromDatabase(crt); err == nil { return p, nil } + return a.unsafeLoadProvisionerFromExtension(crt) +} + +func (a *Authority) unsafeLoadProvisionerFromExtension(crt *x509.Certificate) (provisioner.Interface, error) { + p, ok := a.provisioners.LoadByCertificate(crt) + if !ok || p.GetType() == 0 { + return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate") + } + return p, nil +} +func (a *Authority) unsafeLoadProvisionerFromDatabase(crt *x509.Certificate) (provisioner.Interface, error) { // certificateDataGetter is an interface that can be use to retrieve the // provisioner from a db or a linked ca. type certificateDataGetter interface { GetCertificateData(string) (*db.CertificateData, error) } - var cdg certificateDataGetter - if getter, ok := a.adminDB.(certificateDataGetter); ok { - cdg = getter - } else if getter, ok := a.db.(certificateDataGetter); ok { - cdg = getter - } - if cdg != nil { - if data, err := cdg.GetCertificateData(crt.SerialNumber.String()); err == nil && data.Provisioner != nil { - loadProvisioner = func() (provisioner.Interface, error) { - p, ok := a.provisioners.Load(data.Provisioner.ID) - if !ok { - return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate") - } - return p, nil - } + + var err error + var data *db.CertificateData + + if cdg, ok := a.adminDB.(certificateDataGetter); ok { + data, err = cdg.GetCertificateData(crt.SerialNumber.String()) + } else if cdg, ok := a.db.(certificateDataGetter); ok { + data, err = cdg.GetCertificateData(crt.SerialNumber.String()) + } + if err == nil && data != nil && data.Provisioner != nil { + if p, ok := a.provisioners.Load(data.Provisioner.ID); ok { + return p, nil } } - - a.adminMutex.RLock() - defer a.adminMutex.RUnlock() - return loadProvisioner() + return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate") } // LoadProvisionerByToken returns an interface to the provisioner that diff --git a/authority/provisioners_test.go b/authority/provisioners_test.go index 81dc38bf..56cd16b1 100644 --- a/authority/provisioners_test.go +++ b/authority/provisioners_test.go @@ -1,13 +1,21 @@ package authority import ( + "context" + "crypto/x509" "errors" "net/http" + "reflect" "testing" + "time" "github.com/smallstep/assert" "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/db" + "go.step.sm/crypto/jose" + "go.step.sm/crypto/keyutil" ) func TestGetEncryptedKey(t *testing.T) { @@ -67,6 +75,15 @@ func TestGetEncryptedKey(t *testing.T) { } } +type mockAdminDB struct { + admin.MockDB + MGetCertificateData func(string) (*db.CertificateData, error) +} + +func (c *mockAdminDB) GetCertificateData(sn string) (*db.CertificateData, error) { + return c.MGetCertificateData(sn) +} + func TestGetProvisioners(t *testing.T) { type gp struct { a *Authority @@ -104,3 +121,133 @@ func TestGetProvisioners(t *testing.T) { }) } } + +func TestAuthority_LoadProvisionerByCertificate(t *testing.T) { + _, priv, err := keyutil.GenerateDefaultKeyPair() + assert.FatalError(t, err) + csr := getCSR(t, priv) + + sign := func(a *Authority, extraOpts ...provisioner.SignOption) *x509.Certificate { + key, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) + assert.FatalError(t, err) + token, err := generateToken("smallstep test", "step-cli", testAudiences.Sign[0], []string{"test.smallstep.com"}, time.Now(), key) + assert.FatalError(t, err) + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) + opts, err := a.Authorize(ctx, token) + assert.FatalError(t, err) + opts = append(opts, extraOpts...) + certs, err := a.Sign(csr, provisioner.SignOptions{}, opts...) + assert.FatalError(t, err) + return certs[0] + } + getProvisioner := func(a *Authority, name string) provisioner.Interface { + p, ok := a.provisioners.LoadByName(name) + if !ok { + t.Fatalf("provisioner %s does not exists", name) + } + return p + } + removeExtension := provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { + for i, ext := range cert.ExtraExtensions { + if ext.Id.Equal(provisioner.StepOIDProvisioner) { + cert.ExtraExtensions = append(cert.ExtraExtensions[:i], cert.ExtraExtensions[i+1:]...) + break + } + } + return nil + }) + + a0 := testAuthority(t) + + a1 := testAuthority(t) + a1.db = &db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { + return true, nil + }, + MGetCertificateData: func(serialNumber string) (*db.CertificateData, error) { + p, err := a1.LoadProvisionerByName("dev") + if err != nil { + t.Fatal(err) + } + return &db.CertificateData{ + Provisioner: &db.ProvisionerData{ + ID: p.GetID(), + Name: p.GetName(), + Type: p.GetType().String(), + }, + }, nil + }, + } + + a2 := testAuthority(t) + a2.adminDB = &mockAdminDB{ + MGetCertificateData: (func(s string) (*db.CertificateData, error) { + p, err := a2.LoadProvisionerByName("dev") + if err != nil { + t.Fatal(err) + } + return &db.CertificateData{ + Provisioner: &db.ProvisionerData{ + ID: p.GetID(), + Name: p.GetName(), + Type: p.GetType().String(), + }, + }, nil + }), + } + + a3 := testAuthority(t) + a3.db = &db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { + return true, nil + }, + MGetCertificateData: func(serialNumber string) (*db.CertificateData, error) { + return &db.CertificateData{ + Provisioner: &db.ProvisionerData{ + ID: "foo", Name: "foo", Type: "foo", + }, + }, nil + }, + } + + a4 := testAuthority(t) + a4.adminDB = &mockAdminDB{ + MGetCertificateData: func(serialNumber string) (*db.CertificateData, error) { + return &db.CertificateData{ + Provisioner: &db.ProvisionerData{ + ID: "foo", Name: "foo", Type: "foo", + }, + }, nil + }, + } + + type args struct { + crt *x509.Certificate + } + tests := []struct { + name string + authority *Authority + args args + want provisioner.Interface + wantErr bool + }{ + {"ok from certificate", a0, args{sign(a0)}, getProvisioner(a0, "step-cli"), false}, + {"ok from db", a1, args{sign(a1)}, getProvisioner(a1, "dev"), false}, + {"ok from admindb", a2, args{sign(a2)}, getProvisioner(a2, "dev"), false}, + {"fail from certificate", a0, args{sign(a0, removeExtension)}, nil, true}, + {"fail from db", a3, args{sign(a3, removeExtension)}, nil, true}, + {"fail from admindb", a4, args{sign(a4, removeExtension)}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.authority.LoadProvisionerByCertificate(tt.args.crt) + if (err != nil) != tt.wantErr { + t.Errorf("Authority.LoadProvisionerByCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Authority.LoadProvisionerByCertificate() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/db/db.go b/db/db.go index a3ebb19f..602e3623 100644 --- a/db/db.go +++ b/db/db.go @@ -359,6 +359,7 @@ type MockAuthDB struct { MRevoke func(rci *RevokedCertificateInfo) error MRevokeSSH func(rci *RevokedCertificateInfo) error MGetCertificate func(serialNumber string) (*x509.Certificate, error) + MGetCertificateData func(serialNumber string) (*CertificateData, error) MStoreCertificate func(crt *x509.Certificate) error MUseToken func(id, tok string) (bool, error) MIsSSHHost func(principal string) (bool, error) @@ -418,6 +419,17 @@ func (m *MockAuthDB) GetCertificate(serialNumber string) (*x509.Certificate, err return m.Ret1.(*x509.Certificate), m.Err } +// GetCertificateData mock. +func (m *MockAuthDB) GetCertificateData(serialNumber string) (*CertificateData, error) { + if m.MGetCertificateData != nil { + return m.MGetCertificateData(serialNumber) + } + if cd, ok := m.Ret1.(*CertificateData); ok { + return cd, m.Err + } + return nil, m.Err +} + // StoreCertificate mock. func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error { if m.MStoreCertificate != nil { From af8fcf5b01251daadd06a058858123c266166542 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 8 Apr 2022 14:18:24 -0700 Subject: [PATCH 077/241] Use always LoadProvisionerByCertificate on authority package --- authority/authorize.go | 8 ++++---- authority/authorize_test.go | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/authority/authorize.go b/authority/authorize.go index 0c1c6f22..95698b49 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -282,8 +282,8 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error { if isRevoked { return errs.Unauthorized("authority.authorizeRenew: certificate has been revoked", opts...) } - p, ok := a.provisioners.LoadByCertificate(cert) - if !ok { + p, err := a.LoadProvisionerByCertificate(cert) + if err != nil { return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) } if err := p.AuthorizeRenew(context.Background(), cert); err != nil { @@ -383,8 +383,8 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509. return nil, errs.InternalServerErr(err, errs.WithMessage("error validating renew token")) } - p, ok := a.provisioners.LoadByCertificate(leaf) - if !ok { + p, err := a.LoadProvisionerByCertificate(leaf) + if err != nil { return nil, errs.Unauthorized("error validating renew token: cannot get provisioner from certificate") } if err := a.UseToken(ott, p); err != nil { diff --git a/authority/authorize_test.go b/authority/authorize_test.go index a7bec277..c399eac4 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -847,6 +847,29 @@ func TestAuthority_authorizeRenew(t *testing.T) { cert: fooCrt, } }, + "ok/from db": func(t *testing.T) *authorizeTest { + a := testAuthority(t) + a.db = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { + return false, nil + }, + MGetCertificateData: func(serialNumber string) (*db.CertificateData, error) { + p, ok := a.provisioners.LoadByName("step-cli") + if !ok { + t.Fatal("provisioner step-cli not found") + } + return &db.CertificateData{ + Provisioner: &db.ProvisionerData{ + ID: p.GetID(), + }, + }, nil + }, + } + return &authorizeTest{ + auth: a, + cert: fooCrt, + } + }, } for name, genTestCase := range tests { From 2ace3097b766f659aeebe3e8bb7398a7659cdb84 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 8 Apr 2022 14:29:20 -0700 Subject: [PATCH 078/241] Update changelog. --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e4b15e..8fafac5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased - 0.18.3] - DATE ### Added - Added support for renew after expiry using the claim `allowRenewAfterExpiry`. -- Added support for `extraNames` in X.509 templates. +- Added initial support for `extraNames` in X.509 templates. +- Added initial support for automatic configuration of linked RAs. ### Changed - Made SCEP CA URL paths dynamic - Support two latest versions of Go (1.17, 1.18) ### Deprecated ### Removed ### Fixed +- Fixed admin credentials on RAs. ### Security ## [0.18.2] - 2022-03-01 From f2cf9cf828c314bab1ea390d695236798282e606 Mon Sep 17 00:00:00 2001 From: Panagiotis Siatras Date: Mon, 11 Apr 2022 11:56:16 +0300 Subject: [PATCH 079/241] authority/status: removed the package (#892) --- authority/status/status.go | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 authority/status/status.go diff --git a/authority/status/status.go b/authority/status/status.go deleted file mode 100644 index 49e4c0bb..00000000 --- a/authority/status/status.go +++ /dev/null @@ -1,11 +0,0 @@ -package status - -// Type is the type for status. -type Type string - -var ( - // Active active - Active = Type("active") - // Deleted deleted - Deleted = Type("deleted") -) From 256fe113f7f6612dede752c2db07d1f955046117 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 11 Apr 2022 15:25:55 +0200 Subject: [PATCH 080/241] 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 2fbff47acfeda882e07438e1d0800e6cf565b6ed Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 12:18:44 -0700 Subject: [PATCH 081/241] Add missing return in test. --- ca/bootstrap_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index 2332b4d4..9aaa5f1f 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -92,6 +92,7 @@ func mTLSMiddleware(next http.Handler, nonAuthenticatedPaths ...string) http.Han for _, s := range nonAuthenticatedPaths { if strings.HasPrefix(r.URL.Path, s) || strings.HasPrefix(r.URL.Path, "/1.0"+s) { next.ServeHTTP(w, r) + return } } isMTLS := r.TLS != nil && len(r.TLS.PeerCertificates) > 0 From c8c59d68f5988d3fefe4616c45f8f924165f4627 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 12:19:42 -0700 Subject: [PATCH 082/241] Allow mTLS renewals if the provisioner extension does not exists. This fixes a backward compatibility issue with with the new LoadProvisionerByCertificate. --- authority/authorize.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/authority/authorize.go b/authority/authorize.go index 95698b49..6162fc0e 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -284,7 +284,13 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error { } p, err := a.LoadProvisionerByCertificate(cert) if err != nil { - return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) + var ok bool + // For backward compatibility this method will also succeed if the + // provisioner does not have an extension. LoadByCertificate returns the + // noop provisioner if this happens, and it allow certificate renewals. + if p, ok = a.provisioners.LoadByCertificate(cert); !ok { + return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) + } } if err := p.AuthorizeRenew(context.Background(), cert); err != nil { return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) From 435bb8123bd37ca282abee17a66f39a1b06a85e6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 14:14:02 -0700 Subject: [PATCH 083/241] Upgrade codecov to v2 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64cb64cd..c0c9145a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,8 +59,8 @@ jobs: - name: Codecov if: matrix.go == '1.18' - uses: codecov/codecov-action@v1.2.1 + uses: codecov/codecov-action@v2 with: - file: ./coverage.out # optional + files: ./coverage.out # optional name: codecov-umbrella # optional fail_ci_if_error: true # optional (default = false) From 1880b4b2d0f934d4dd11681288eb0034dd2b22fd Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 14:21:14 -0700 Subject: [PATCH 084/241] Add codecov token. It shouldn't be necessary for public repos, but GitHub actions error suggests to add it. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0c9145a..b24426a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,6 +61,7 @@ jobs: if: matrix.go == '1.18' uses: codecov/codecov-action@v2 with: + token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.out # optional name: codecov-umbrella # optional fail_ci_if_error: true # optional (default = false) From 9134bad22cff5fe4af1f3d1f173891c58d91f862 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 14:59:22 -0700 Subject: [PATCH 085/241] Run go mod tidy. --- go.sum | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/go.sum b/go.sum index 0b08d836..73db44d8 100644 --- a/go.sum +++ b/go.sum @@ -461,9 +461,6 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -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/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -513,6 +510,9 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 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/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= @@ -545,7 +545,6 @@ github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -749,8 +748,8 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3 github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -810,6 +809,7 @@ go.step.sm/linkedca v0.12.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1AN 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= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1246,6 +1246,7 @@ 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/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 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= @@ -1277,6 +1278,7 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= 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/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= From 967d9136ca22006f225433a4be1f3b9d407086e1 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 18:44:13 -0700 Subject: [PATCH 086/241] Cleanup Vault CAS integration --- cas/vaultcas/vaultcas.go | 401 ++++++++++++++-------------------- cas/vaultcas/vaultcas_test.go | 19 +- 2 files changed, 180 insertions(+), 240 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 4d7e220d..a6b8d62c 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -1,12 +1,15 @@ package vaultcas import ( + "bytes" "context" "crypto/sha256" "crypto/x509" + "encoding/hex" "encoding/json" "encoding/pem" "math/big" + "strings" "time" "github.com/pkg/errors" @@ -14,7 +17,6 @@ import ( vault "github.com/hashicorp/vault/api" auth "github.com/hashicorp/vault/api/auth/approle" - certutil "github.com/hashicorp/vault/sdk/helper/certutil" ) func init() { @@ -28,7 +30,7 @@ type VaultOptions struct { PKIRoleDefault string `json:"PKIRoleDefault,omitempty"` PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"` - PKIRoleEd25519 string `json:"PKIRoleEd25519,omitempty"` + PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"` RoleID string `json:"roleID,omitempty"` SecretID auth.SecretID `json:"secretID,omitempty"` AppRole string `json:"appRole,omitempty"` @@ -42,207 +44,12 @@ type VaultCAS struct { fingerprint string } -type Certificate struct { +type certBundle struct { leaf *x509.Certificate intermediates []*x509.Certificate root *x509.Certificate } -func loadOptions(config json.RawMessage) (*VaultOptions, error) { - var vc *VaultOptions - - err := json.Unmarshal(config, &vc) - if err != nil { - return nil, errors.Wrap(err, "error decoding vaultCAS config") - } - - if vc.PKI == "" { - vc.PKI = "pki" // use default pki vault name - } - - if vc.PKIRoleDefault == "" { - vc.PKIRoleDefault = "default" // use default pki role name - } - - if vc.PKIRoleRSA == "" { - vc.PKIRoleRSA = vc.PKIRoleDefault - } - if vc.PKIRoleEC == "" { - vc.PKIRoleEC = vc.PKIRoleDefault - } - if vc.PKIRoleEd25519 == "" { - vc.PKIRoleEd25519 = vc.PKIRoleDefault - } - - if vc.RoleID == "" { - return nil, errors.New("vaultCAS config options must define `roleID`") - } - - if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" { - return nil, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") - } - - if vc.PKI == "" { - vc.PKI = "pki" // use default pki vault name - } - - if vc.AppRole == "" { - vc.AppRole = "auth/approle" - } - - return vc, nil -} - -func certificateSort(n []*x509.Certificate) bool { - // sort all cert using bubble sort - isSorted := false - s := 0 - maxSwap := len(n) * (len(n) - 1) / 2 - for s <= maxSwap && !isSorted { - isSorted = true - var i = 0 - for i < len(n)-1 { - if !isSignedBy(n[i], n[i+1]) { - // swap - n[i], n[i+1] = n[i+1], n[i] - isSorted = false - } - i++ - } - s++ - } - return isSorted -} - -func isSignedBy(i, j *x509.Certificate) bool { - signer := x509.NewCertPool() - signer.AddCert(j) - - opts := x509.VerifyOptions{ - Roots: signer, - Intermediates: x509.NewCertPool(), // set empty to avoid using system CA - } - _, err := i.Verify(opts) - return err == nil -} - -func parseCertificates(pemCert string) []*x509.Certificate { - var certs []*x509.Certificate - rest := []byte(pemCert) - var block *pem.Block - for { - block, rest = pem.Decode(rest) - if block == nil { - break - } - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - break - } - certs = append(certs, cert) - } - return certs -} - -func getCertificateAndChain(certb certutil.CertBundle) (*Certificate, error) { - // certutil.CertBundle contains CAChain and Certificate. - // Both could have a common part or different and we are not sure - // how user define their chain inside vault. - // We will create an array of certificate with all parsed certificates - // then sort the array to create a consistent chain - var root *x509.Certificate - var leaf *x509.Certificate - intermediates := make([]*x509.Certificate, 0) - used := make(map[string]bool) // ensure that intermediate are uniq - for _, chain := range append(certb.CAChain, certb.Certificate) { - for _, cert := range parseCertificates(chain) { - if used[cert.SerialNumber.String()] { - continue - } - used[cert.SerialNumber.String()] = true - switch { - case isRoot(cert): - root = cert - case cert.BasicConstraintsValid && cert.IsCA: - intermediates = append(intermediates, cert) - default: - leaf = cert - } - } - } - if ok := certificateSort(intermediates); !ok { - return nil, errors.Errorf("failed to sort certificate, probably one of cert is not part of the chain") - } - - certificate := &Certificate{ - root: root, - leaf: leaf, - intermediates: intermediates, - } - - return certificate, nil -} - -func parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) { - block, _ := pem.Decode([]byte(pemCsr)) - if block == nil { - return nil, errors.Errorf("error decoding certificate request: not a valid PEM encoded block, please verify\r\n%v", pemCsr) - } - - cr, err := x509.ParseCertificateRequest(block.Bytes) - if err != nil { - return nil, errors.Wrap(err, "error parsing certificate request") - } - return cr, nil -} - -func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.Duration) (*x509.Certificate, []*x509.Certificate, error) { - var vaultPKIRole string - - switch { - case cr.PublicKeyAlgorithm == x509.RSA: - vaultPKIRole = v.config.PKIRoleRSA - case cr.PublicKeyAlgorithm == x509.ECDSA: - vaultPKIRole = v.config.PKIRoleEC - case cr.PublicKeyAlgorithm == x509.Ed25519: - vaultPKIRole = v.config.PKIRoleEd25519 - default: - return nil, nil, errors.Errorf("createCertificate: Unsupported public key algorithm '%v'", cr.PublicKeyAlgorithm) - } - - certPemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: cr.Raw}) - if certPemBytes == nil { - return nil, nil, errors.Errorf("createCertificate: Failed to encode pem '%v'", cr.Raw) - } - - y := map[string]interface{}{ - "csr": string(certPemBytes), - "format": "pem_bundle", - "ttl": lifetime.Seconds(), - } - - secret, err := v.client.Logical().Write(v.config.PKI+"/sign/"+vaultPKIRole, y) - if err != nil { - return nil, nil, errors.Wrapf(err, "createCertificate: unable to sign certificate %v", y) - } - if secret == nil { - return nil, nil, errors.New("createCertificate: secret sign is empty") - } - - var certBundle certutil.CertBundle - if err := unmarshalMap(secret.Data, &certBundle); err != nil { - return nil, nil, errors.Wrap(err, "error unmarshaling cert bundle") - } - - cert, err := getCertificateAndChain(certBundle) - if err != nil { - return nil, nil, err - } - - // Return certificate and certificate chain - return cert.leaf, cert.intermediates, nil -} - // New creates a new CertificateAuthorityService implementation // using Hashicorp Vault func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { @@ -305,9 +112,9 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { switch { case req.CSR == nil: - return nil, errors.New("CreateCertificate: `CSR` cannot be nil") + return nil, errors.New("createCertificate `csr` cannot be nil") case req.Lifetime == 0: - return nil, errors.New("CreateCertificate: `LIFETIME` cannot be 0") + return nil, errors.New("createCertificate `lifetime` cannot be 0") } cert, chain, err := v.createCertificate(req.CSR, req.Lifetime) @@ -324,26 +131,28 @@ func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca_chain") if err != nil { - return nil, errors.Wrap(err, "unable to read root") + return nil, errors.Wrap(err, "error reading ca chain") } if secret == nil { - return nil, errors.New("secret root is empty") + return nil, errors.New("error reading ca chain: response is empty") } - var certBundle certutil.CertBundle - if err := unmarshalMap(secret.Data, &certBundle); err != nil { - return nil, errors.Wrap(err, "error unmarshaling cert bundle") + chain, ok := secret.Data["certificate"].(string) + if !ok { + return nil, errors.New("error unmarshaling vault response: certificate not found") } - cert, err := getCertificateAndChain(certBundle) + cert, err := getCertificateBundle(chain) if err != nil { return nil, err } + if cert.root == nil { + return nil, errors.New("error unmarshaling vault response: root certificate not found") + } - sha256Sum := sha256.Sum256(cert.root.Raw) - expectedSum := certutil.GetHexFormatted(sha256Sum[:], "") - if expectedSum != v.fingerprint { - return nil, errors.Errorf("Vault Root CA fingerprint `%s` doesn't match config fingerprint `%v`", expectedSum, v.fingerprint) + sum := sha256.Sum256(cert.root.Raw) + if !strings.EqualFold(v.fingerprint, strings.ToLower(hex.EncodeToString(sum[:]))) { + return nil, errors.New("error verifying vault root: fingerprint does not match") } return &apiv1.GetCertificateAuthorityResponse{ @@ -357,34 +166,28 @@ func (v *VaultCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1. return nil, apiv1.ErrNotImplemented{Message: "vaultCAS does not support renewals"} } +// RevokeCertificate revokes a certificate by serial number. func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { if req.SerialNumber == "" && req.Certificate == nil { - return nil, errors.New("`serialNumber` or `certificate` are required") + return nil, errors.New("revokeCertificate `serialNumber` or `certificate` are required") } - var serialNumber []byte + var sn *big.Int if req.SerialNumber != "" { - // req.SerialNumber is a big.Int string representation - n := new(big.Int) - n, ok := n.SetString(req.SerialNumber, 10) - if !ok { - return nil, errors.Errorf("serialNumber `%v` can't be convert to big.Int", req.SerialNumber) + var ok bool + if sn, ok = new(big.Int).SetString(req.SerialNumber, 10); !ok { + return nil, errors.Errorf("error parsing serialNumber: %v cannot be converted to big.Int", req.SerialNumber) } - serialNumber = n.Bytes() } else { - // req.Certificate.SerialNumber is a big.Int - serialNumber = req.Certificate.SerialNumber.Bytes() + sn = req.Certificate.SerialNumber } - serialNumberDash := certutil.GetHexFormatted(serialNumber, "-") - - y := map[string]interface{}{ - "serial_number": serialNumberDash, + vaultReq := map[string]interface{}{ + "serial_number": formatSerialNumber(sn), } - - _, err := v.client.Logical().Write(v.config.PKI+"/revoke/", y) + _, err := v.client.Logical().Write(v.config.PKI+"/revoke/", vaultReq) if err != nil { - return nil, errors.Wrap(err, "unable to revoke certificate") + return nil, errors.Wrap(err, "error revoking certificate") } return &apiv1.RevokeCertificateResponse{ @@ -393,13 +196,136 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv }, nil } -func unmarshalMap(m map[string]interface{}, v interface{}) error { - b, err := json.Marshal(m) +func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.Duration) (*x509.Certificate, []*x509.Certificate, error) { + var vaultPKIRole string + + switch { + case cr.PublicKeyAlgorithm == x509.RSA: + vaultPKIRole = v.config.PKIRoleRSA + case cr.PublicKeyAlgorithm == x509.ECDSA: + vaultPKIRole = v.config.PKIRoleEC + case cr.PublicKeyAlgorithm == x509.Ed25519: + vaultPKIRole = v.config.PKIRoleEd25519 + default: + return nil, nil, errors.Errorf("unsupported public key algorithm '%v'", cr.PublicKeyAlgorithm) + } + + vaultReq := map[string]interface{}{ + "csr": string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: cr.Raw, + })), + "format": "pem_bundle", + "ttl": lifetime.Seconds(), + } + + secret, err := v.client.Logical().Write(v.config.PKI+"/sign/"+vaultPKIRole, vaultReq) if err != nil { - return err + return nil, nil, errors.Wrap(err, "error signing certificate") + } + if secret == nil { + return nil, nil, errors.New("error signing certificate: response is empty") } - return json.Unmarshal(b, v) + chain, ok := secret.Data["certificate"].(string) + if !ok { + return nil, nil, errors.New("error unmarshaling vault response: certificate not found") + } + + cert, err := getCertificateBundle(chain) + if err != nil { + return nil, nil, err + } + + // Return certificate and certificate chain + return cert.leaf, cert.intermediates, nil +} + +func loadOptions(config json.RawMessage) (*VaultOptions, error) { + var vc *VaultOptions + + err := json.Unmarshal(config, &vc) + if err != nil { + return nil, errors.Wrap(err, "error decoding vaultCAS config") + } + + if vc.PKI == "" { + vc.PKI = "pki" // use default pki vault name + } + + if vc.PKIRoleDefault == "" { + vc.PKIRoleDefault = "default" // use default pki role name + } + + if vc.PKIRoleRSA == "" { + vc.PKIRoleRSA = vc.PKIRoleDefault + } + if vc.PKIRoleEC == "" { + vc.PKIRoleEC = vc.PKIRoleDefault + } + if vc.PKIRoleEd25519 == "" { + vc.PKIRoleEd25519 = vc.PKIRoleDefault + } + + if vc.RoleID == "" { + return nil, errors.New("vaultCAS config options must define `roleID`") + } + + if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" { + return nil, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") + } + + if vc.PKI == "" { + vc.PKI = "pki" // use default pki vault name + } + + if vc.AppRole == "" { + vc.AppRole = "auth/approle" + } + + return vc, nil +} + +func parseCertificates(pemCert string) []*x509.Certificate { + var certs []*x509.Certificate + rest := []byte(pemCert) + var block *pem.Block + for { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + break + } + certs = append(certs, cert) + } + return certs +} + +func getCertificateBundle(chain string) (*certBundle, error) { + var root *x509.Certificate + var leaf *x509.Certificate + var intermediates []*x509.Certificate + for _, cert := range parseCertificates(chain) { + switch { + case isRoot(cert): + root = cert + case cert.BasicConstraintsValid && cert.IsCA: + intermediates = append(intermediates, cert) + default: + leaf = cert + } + } + + certificate := &certBundle{ + root: root, + leaf: leaf, + intermediates: intermediates, + } + + return certificate, nil } // isRoot returns true if the given certificate is a root certificate. @@ -409,3 +335,16 @@ func isRoot(cert *x509.Certificate) bool { } return false } + +// formatSerialNumber formats a serial number to a dash-separated hexadecimal +// string. +func formatSerialNumber(sn *big.Int) string { + var ret bytes.Buffer + for _, b := range sn.Bytes() { + if ret.Len() > 0 { + ret.WriteString("-") + } + ret.WriteString(hex.EncodeToString([]byte{b})) + } + return ret.String() +} diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 1febf1ce..9f73a1ee 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -16,6 +16,7 @@ import ( vault "github.com/hashicorp/vault/api" auth "github.com/hashicorp/vault/api/auth/approle" "github.com/smallstep/certificates/cas/apiv1" + "go.step.sm/crypto/pemutil" ) var ( @@ -80,13 +81,13 @@ func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate { return crt } -func mustParseCertificateRequest(t *testing.T, pemCert string) *x509.CertificateRequest { +func mustParseCertificateRequest(t *testing.T, pemData string) *x509.CertificateRequest { t.Helper() - crt, err := parseCertificateRequest(pemCert) + csr, err := pemutil.ParseCertificateRequest([]byte(pemData)) if err != nil { t.Fatal(err) } - return crt + return csr } func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { @@ -107,17 +108,17 @@ func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { }`) case r.RequestURI == "/v1/pki/sign/ec": w.WriteHeader(http.StatusOK) - cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned + "\n" + testRootCertificate}} writeJSON(w, cert) return case r.RequestURI == "/v1/pki/sign/rsa": w.WriteHeader(http.StatusOK) - cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned + "\n" + testRootCertificate}} writeJSON(w, cert) return case r.RequestURI == "/v1/pki/sign/ed25519": w.WriteHeader(http.StatusOK) - cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned + "\n" + testRootCertificate}} writeJSON(w, cert) return case r.RequestURI == "/v1/pki/cert/ca_chain": @@ -232,21 +233,21 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { Lifetime: time.Hour, }}, &apiv1.CreateCertificateResponse{ Certificate: mustParseCertificate(t, testCertificateSigned), - CertificateChain: []*x509.Certificate{}, + CertificateChain: nil, }, false}, {"ok rsa", fields{client, options}, args{&apiv1.CreateCertificateRequest{ CSR: mustParseCertificateRequest(t, testCertificateCsrRsa), Lifetime: time.Hour, }}, &apiv1.CreateCertificateResponse{ Certificate: mustParseCertificate(t, testCertificateSigned), - CertificateChain: []*x509.Certificate{}, + CertificateChain: nil, }, false}, {"ok ed25519", fields{client, options}, args{&apiv1.CreateCertificateRequest{ CSR: mustParseCertificateRequest(t, testCertificateCsrEd25519), Lifetime: time.Hour, }}, &apiv1.CreateCertificateResponse{ Certificate: mustParseCertificate(t, testCertificateSigned), - CertificateChain: []*x509.Certificate{}, + CertificateChain: nil, }, false}, {"fail CSR", fields{client, options}, args{&apiv1.CreateCertificateRequest{ CSR: nil, From 26e40068c89957ad792b8996205c60da3533e714 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 18:49:14 -0700 Subject: [PATCH 087/241] Remove unnecessary dependencies. --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index 139c82e1..8a5f0af1 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,6 @@ require ( 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/hashicorp/vault/sdk v0.3.0 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 From 790a19c6f6f17d984cc260c44fa4dca6400fe597 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 10:01:22 -0700 Subject: [PATCH 088/241] make json names uniform Co-authored-by: Ahmet Demir --- cas/vaultcas/vaultcas.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index a6b8d62c..e27381df 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -27,7 +27,7 @@ func init() { type VaultOptions struct { PKI string `json:"pki,omitempty"` - PKIRoleDefault string `json:"PKIRoleDefault,omitempty"` + PKIRoleDefault string `json:"pKIRoleDefault,omitempty"` PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"` PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"` From 48bc20c9f3c40ffbbf104faa3aa79d9e0e8ce84d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 11:11:36 -0700 Subject: [PATCH 089/241] Unify json parameters. --- cas/vaultcas/vaultcas.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index e27381df..151a25de 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -27,7 +27,7 @@ func init() { type VaultOptions struct { PKI string `json:"pki,omitempty"` - PKIRoleDefault string `json:"pKIRoleDefault,omitempty"` + PKIRoleDefault string `json:"pkiRoleDefault,omitempty"` PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"` PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"` From 76c483c36fc9f3d46014baf6cbc35cad914cdbf9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 11:15:28 -0700 Subject: [PATCH 090/241] Add missing comments. --- cas/vaultcas/vaultcas.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 151a25de..519412c9 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -25,6 +25,8 @@ func init() { }) } +// VaultOptions defines the configuration options added using the +// apiv1.Options.Config field. type VaultOptions struct { PKI string `json:"pki,omitempty"` PKIRoleDefault string `json:"pkiRoleDefault,omitempty"` @@ -128,6 +130,8 @@ func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv }, nil } +// GetCertificateAuthority returns the root certificate of the certificate +// authority using the configured fingerprint. func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca_chain") if err != nil { From 25d0ca258dafeee35f10e95bece4d1e78fb6c60d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 14:14:02 -0700 Subject: [PATCH 091/241] Upgrade codecov to v2 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64cb64cd..c0c9145a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,8 +59,8 @@ jobs: - name: Codecov if: matrix.go == '1.18' - uses: codecov/codecov-action@v1.2.1 + uses: codecov/codecov-action@v2 with: - file: ./coverage.out # optional + files: ./coverage.out # optional name: codecov-umbrella # optional fail_ci_if_error: true # optional (default = false) From c4ff0f1cc36d55a26e08d6b10c79fd29c50f75c2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Apr 2022 14:21:14 -0700 Subject: [PATCH 092/241] Add codecov token. It shouldn't be necessary for public repos, but GitHub actions error suggests to add it. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0c9145a..b24426a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,6 +61,7 @@ jobs: if: matrix.go == '1.18' uses: codecov/codecov-action@v2 with: + token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.out # optional name: codecov-umbrella # optional fail_ci_if_error: true # optional (default = false) From ea5f7f2acc2757128a7cc72fd946dd1177ac270e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 13:57:55 -0700 Subject: [PATCH 093/241] Fix SANs for step-ca certificate Co-authored-by: Herman Slatman --- authority/tls.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/tls.go b/authority/tls.go index dab8775e..5d295073 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -546,7 +546,7 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { } // Create initial certificate request. - cr, err := x509util.CreateCertificateRequest(a.config.CommonName, a.config.DNSNames, signer) + cr, err := x509util.CreateCertificateRequest(a.config.CommonName, sans, signer) if err != nil { return fatal(err) } From e29c85bbd46e5bd44c810284b3b94d7f38a4950c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 14:04:46 -0700 Subject: [PATCH 094/241] Use errors and fmt instead of pkg/errors. --- cas/vaultcas/vaultcas.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 519412c9..c29ef691 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -8,11 +8,12 @@ import ( "encoding/hex" "encoding/json" "encoding/pem" + "errors" + "fmt" "math/big" "strings" "time" - "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" vault "github.com/hashicorp/vault/api" @@ -73,7 +74,7 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { client, err := vault.NewClient(config) if err != nil { - return nil, errors.Wrap(err, "unable to initialize vault client") + return nil, fmt.Errorf("unable to initialize vault client: %w", err) } var appRoleAuth *auth.AppRoleAuth @@ -92,12 +93,12 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { ) } if err != nil { - return nil, errors.Wrap(err, "unable to initialize AppRole auth method") + return nil, fmt.Errorf("unable to initialize AppRole auth method: %w", err) } authInfo, err := client.Auth().Login(ctx, appRoleAuth) if err != nil { - return nil, errors.Wrap(err, "unable to login to AppRole auth method") + return nil, fmt.Errorf("unable to login to AppRole auth method: %w", err) } if authInfo == nil { return nil, errors.New("no auth info was returned after login") @@ -135,7 +136,7 @@ func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca_chain") if err != nil { - return nil, errors.Wrap(err, "error reading ca chain") + return nil, fmt.Errorf("error reading ca chain: %w", err) } if secret == nil { return nil, errors.New("error reading ca chain: response is empty") @@ -180,7 +181,7 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv if req.SerialNumber != "" { var ok bool if sn, ok = new(big.Int).SetString(req.SerialNumber, 10); !ok { - return nil, errors.Errorf("error parsing serialNumber: %v cannot be converted to big.Int", req.SerialNumber) + return nil, fmt.Errorf("error parsing serialNumber: %v cannot be converted to big.Int", req.SerialNumber) } } else { sn = req.Certificate.SerialNumber @@ -191,7 +192,7 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv } _, err := v.client.Logical().Write(v.config.PKI+"/revoke/", vaultReq) if err != nil { - return nil, errors.Wrap(err, "error revoking certificate") + return nil, fmt.Errorf("error revoking certificate: %w", err) } return &apiv1.RevokeCertificateResponse{ @@ -211,7 +212,7 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time. case cr.PublicKeyAlgorithm == x509.Ed25519: vaultPKIRole = v.config.PKIRoleEd25519 default: - return nil, nil, errors.Errorf("unsupported public key algorithm '%v'", cr.PublicKeyAlgorithm) + return nil, nil, fmt.Errorf("unsupported public key algorithm %v", cr.PublicKeyAlgorithm) } vaultReq := map[string]interface{}{ @@ -225,7 +226,7 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time. secret, err := v.client.Logical().Write(v.config.PKI+"/sign/"+vaultPKIRole, vaultReq) if err != nil { - return nil, nil, errors.Wrap(err, "error signing certificate") + return nil, nil, fmt.Errorf("error signing certificate: %w", err) } if secret == nil { return nil, nil, errors.New("error signing certificate: response is empty") @@ -250,7 +251,7 @@ func loadOptions(config json.RawMessage) (*VaultOptions, error) { err := json.Unmarshal(config, &vc) if err != nil { - return nil, errors.Wrap(err, "error decoding vaultCAS config") + return nil, fmt.Errorf("error decoding vaultCAS config: %w", err) } if vc.PKI == "" { From 1c24863d2f6482e32cd57c831e7734fd021ddd84 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 14:41:25 -0700 Subject: [PATCH 095/241] Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e4b15e..fce3fafa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Added support for renew after expiry using the claim `allowRenewAfterExpiry`. - Added support for `extraNames` in X.509 templates. +- Added RA support using a Vault instance as the CA. ### Changed - Made SCEP CA URL paths dynamic - Support two latest versions of Go (1.17, 1.18) From 00cd0f5f2194bd4a1573fdf3d105f5bbfe7c2e1d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 14:44:55 -0700 Subject: [PATCH 096/241] Apply suggestions from code review Co-authored-by: Herman Slatman --- authority/authorize.go | 2 +- authority/provisioners.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authority/authorize.go b/authority/authorize.go index 6162fc0e..7dc58575 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -287,7 +287,7 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error { var ok bool // For backward compatibility this method will also succeed if the // provisioner does not have an extension. LoadByCertificate returns the - // noop provisioner if this happens, and it allow certificate renewals. + // noop provisioner if this happens, and it allows certificate renewals. if p, ok = a.provisioners.LoadByCertificate(cert); !ok { return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) } diff --git a/authority/provisioners.go b/authority/provisioners.go index 7ff080ed..c20c7b68 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -62,7 +62,7 @@ func (a *Authority) unsafeLoadProvisionerFromExtension(crt *x509.Certificate) (p } func (a *Authority) unsafeLoadProvisionerFromDatabase(crt *x509.Certificate) (provisioner.Interface, error) { - // certificateDataGetter is an interface that can be use to retrieve the + // certificateDataGetter is an interface that can be used to retrieve the // provisioner from a db or a linked ca. type certificateDataGetter interface { GetCertificateData(string) (*db.CertificateData, error) From 0dc5646e319d5e1d911a72e2567f3c6c926264c1 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 12 Apr 2022 15:21:18 -0700 Subject: [PATCH 097/241] add Postgres to available databases in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c29ccdf..68883662 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Setting up a *public key infrastructure* (PKI) is out of reach for many small te - [Short-lived certificates](https://smallstep.com/blog/passive-revocation.html) with automated enrollment, renewal, and passive revocation - Capable of high availability (HA) deployment using [root federation](https://smallstep.com/blog/step-v0.8.3-federation-root-rotation.html) and/or multiple intermediaries - Can operate as [an online intermediate CA for an existing root CA](https://smallstep.com/docs/tutorials/intermediate-ca-new-ca) -- [Badger, BoltDB, and MySQL database backends](https://smallstep.com/docs/step-ca/configuration#databases) +- [Badger, BoltDB, Postgres, and MySQL database backends](https://smallstep.com/docs/step-ca/configuration#databases) ### ⚙️ Many ways to automate From 0a5dc237dfc1c6619e00d7337e850eb212029ffa Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 17:56:39 -0700 Subject: [PATCH 098/241] Fix typo in comment. --- authority/authorize.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/authority/authorize.go b/authority/authorize.go index 7dc58575..7121c55f 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -286,8 +286,9 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error { if err != nil { var ok bool // For backward compatibility this method will also succeed if the - // provisioner does not have an extension. LoadByCertificate returns the - // noop provisioner if this happens, and it allows certificate renewals. + // certificate does not have a provisioner extension. LoadByCertificate + // returns the noop provisioner if this happens, and it allows + // certificate renewals. if p, ok = a.provisioners.LoadByCertificate(cert); !ok { return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) } From 3694ba30dce49ecdf8f60ca2a6a7ca7315b723b9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 12 Apr 2022 18:42:27 -0700 Subject: [PATCH 099/241] Store certificate and provisioner in one transaction. --- db/db.go | 13 ++++++------ db/db_test.go | 57 ++++++++++++++++++--------------------------------- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/db/db.go b/db/db.go index 602e3623..eccaf801 100644 --- a/db/db.go +++ b/db/db.go @@ -243,9 +243,7 @@ type ProvisionerData struct { // authorized the certificate. func (db *DB) StoreCertificateChain(p provisioner.Interface, chain ...*x509.Certificate) error { leaf := chain[0] - if err := db.StoreCertificate(leaf); err != nil { - return err - } + serialNumber := []byte(leaf.SerialNumber.String()) data := &CertificateData{} if p != nil { data.Provisioner = &ProvisionerData{ @@ -254,13 +252,16 @@ func (db *DB) StoreCertificateChain(p provisioner.Interface, chain ...*x509.Cert Type: p.GetType().String(), } } - b, err := json.Marshal(data) if err != nil { return errors.Wrap(err, "error marshaling json") } - if err := db.Set(certsDataTable, []byte(leaf.SerialNumber.String()), b); err != nil { - return errors.Wrap(err, "database Set error") + // Add certificate and certificate data in one transaction. + tx := new(database.Tx) + tx.Set(certsTable, serialNumber, leaf.Raw) + tx.Set(certsDataTable, serialNumber, b) + if err := db.Update(tx); err != nil { + return errors.Wrap(err, "database Update error") } return nil } diff --git a/db/db_test.go b/db/db_test.go index d7c58c9c..b4515a5b 100644 --- a/db/db_test.go +++ b/db/db_test.go @@ -188,53 +188,36 @@ func TestDB_StoreCertificateChain(t *testing.T) { wantErr bool }{ {"ok", fields{&MockNoSQLDB{ - MSet: func(bucket, key, value []byte) error { - switch string(bucket) { - case "x509_certs": - assert.Equals(t, key, []byte("1234")) - assert.Equals(t, value, []byte("the certificate")) - case "x509_certs_data": - assert.Equals(t, key, []byte("1234")) - assert.Equals(t, value, []byte(`{"provisioner":{"id":"some-id","name":"admin","type":"JWK"}}`)) - default: - t.Errorf("unexpected bucket %s", bucket) + MUpdate: func(tx *database.Tx) error { + if len(tx.Operations) != 2 { + t.Fatal("unexpected number of operations") } + assert.Equals(t, []byte("x509_certs"), tx.Operations[0].Bucket) + assert.Equals(t, []byte("1234"), tx.Operations[0].Key) + assert.Equals(t, []byte("the certificate"), tx.Operations[0].Value) + assert.Equals(t, []byte("x509_certs_data"), tx.Operations[1].Bucket) + assert.Equals(t, []byte("1234"), tx.Operations[1].Key) + assert.Equals(t, []byte(`{"provisioner":{"id":"some-id","name":"admin","type":"JWK"}}`), tx.Operations[1].Value) return nil }, }, true}, args{p, chain}, false}, {"ok no provisioner", fields{&MockNoSQLDB{ - MSet: func(bucket, key, value []byte) error { - switch string(bucket) { - case "x509_certs": - assert.Equals(t, key, []byte("1234")) - assert.Equals(t, value, []byte("the certificate")) - case "x509_certs_data": - assert.Equals(t, key, []byte("1234")) - assert.Equals(t, value, []byte(`{}`)) - default: - t.Errorf("unexpected bucket %s", bucket) + MUpdate: func(tx *database.Tx) error { + if len(tx.Operations) != 2 { + t.Fatal("unexpected number of operations") } + assert.Equals(t, []byte("x509_certs"), tx.Operations[0].Bucket) + assert.Equals(t, []byte("1234"), tx.Operations[0].Key) + assert.Equals(t, []byte("the certificate"), tx.Operations[0].Value) + assert.Equals(t, []byte("x509_certs_data"), tx.Operations[1].Bucket) + assert.Equals(t, []byte("1234"), tx.Operations[1].Key) + assert.Equals(t, []byte(`{}`), tx.Operations[1].Value) return nil }, }, true}, args{nil, chain}, false}, {"fail store certificate", fields{&MockNoSQLDB{ - MSet: func(bucket, key, value []byte) error { - switch string(bucket) { - case "x509_certs": - return errors.New("test error") - default: - return nil - } - }, - }, true}, args{p, chain}, true}, - {"fail store provisioner", fields{&MockNoSQLDB{ - MSet: func(bucket, key, value []byte) error { - switch string(bucket) { - case "x509_certs_data": - return errors.New("test error") - default: - return nil - } + MUpdate: func(tx *database.Tx) error { + return errors.New("test error") }, }, true}, args{p, chain}, true}, } From 4e4d4e882ffc9053112a2cc83b3799e6ad0cc7da Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 13 Apr 2022 14:50:06 -0700 Subject: [PATCH 100/241] Use a fixed string for renewal token issuer. --- authority/authorize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/authorize.go b/authority/authorize.go index 7121c55f..b0a1fab4 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -399,7 +399,7 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509. } if err := claims.ValidateWithLeeway(jose.Expected{ - Issuer: p.GetName(), + Issuer: "step-ca-client/1.0", Subject: leaf.Subject.CommonName, Time: time.Now().UTC(), }, time.Minute); err != nil { From 674dc3c8443faac244ff0dea7a1d706227bfadda Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 13 Apr 2022 15:11:54 -0700 Subject: [PATCH 101/241] Rename unreleased claim to allowRenewalAfterExpiry for consistency. --- CHANGELOG.md | 2 +- authority/config/config.go | 28 ++++++++--------- authority/provisioner/claims.go | 40 ++++++++++++------------ authority/provisioner/controller.go | 4 +-- authority/provisioner/controller_test.go | 12 +++---- authority/provisioner/utils_test.go | 32 +++++++++---------- authority/provisioners.go | 14 ++++----- go.mod | 2 +- go.sum | 4 +-- 9 files changed, 69 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e4b15e..f276240b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased - 0.18.3] - DATE ### Added -- Added support for renew after expiry using the claim `allowRenewAfterExpiry`. +- Added support for certificate renewals after expiry using the claim `allowRenewalAfterExpiry`. - Added support for `extraNames` in X.509 templates. ### Changed - Made SCEP CA URL paths dynamic diff --git a/authority/config/config.go b/authority/config/config.go index 2c437725..de4aa938 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -26,27 +26,27 @@ var ( DefaultBackdate = time.Minute // DefaultDisableRenewal disables renewals per provisioner. DefaultDisableRenewal = false - // DefaultAllowRenewAfterExpiry allows renewals even if the certificate is + // DefaultAllowRenewalAfterExpiry allows renewals even if the certificate is // expired. - DefaultAllowRenewAfterExpiry = false + DefaultAllowRenewalAfterExpiry = false // DefaultEnableSSHCA enable SSH CA features per provisioner or globally // for all provisioners. DefaultEnableSSHCA = false // GlobalProvisionerClaims default claims for the Authority. Can be overridden // by provisioner specific claims. GlobalProvisionerClaims = provisioner.Claims{ - MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs - MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, - DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, - MinUserSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs - MaxUserSSHDur: &provisioner.Duration{Duration: 24 * time.Hour}, - DefaultUserSSHDur: &provisioner.Duration{Duration: 16 * time.Hour}, - MinHostSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs - MaxHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, - DefaultHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, - EnableSSHCA: &DefaultEnableSSHCA, - DisableRenewal: &DefaultDisableRenewal, - AllowRenewAfterExpiry: &DefaultAllowRenewAfterExpiry, + MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs + MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, + DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, + MinUserSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs + MaxUserSSHDur: &provisioner.Duration{Duration: 24 * time.Hour}, + DefaultUserSSHDur: &provisioner.Duration{Duration: 16 * time.Hour}, + MinHostSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs + MaxHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, + DefaultHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, + EnableSSHCA: &DefaultEnableSSHCA, + DisableRenewal: &DefaultDisableRenewal, + AllowRenewalAfterExpiry: &DefaultAllowRenewalAfterExpiry, } ) diff --git a/authority/provisioner/claims.go b/authority/provisioner/claims.go index 2a3e2c61..96f19b37 100644 --- a/authority/provisioner/claims.go +++ b/authority/provisioner/claims.go @@ -24,8 +24,8 @@ type Claims struct { EnableSSHCA *bool `json:"enableSSHCA,omitempty"` // Renewal properties - DisableRenewal *bool `json:"disableRenewal,omitempty"` - AllowRenewAfterExpiry *bool `json:"allowRenewAfterExpiry,omitempty"` + DisableRenewal *bool `json:"disableRenewal,omitempty"` + AllowRenewalAfterExpiry *bool `json:"allowRenewalAfterExpiry,omitempty"` } // Claimer is the type that controls claims. It provides an interface around the @@ -44,22 +44,22 @@ func NewClaimer(claims *Claims, global Claims) (*Claimer, error) { // Claims returns the merge of the inner and global claims. func (c *Claimer) Claims() Claims { disableRenewal := c.IsDisableRenewal() - allowRenewAfterExpiry := c.AllowRenewAfterExpiry() + allowRenewalAfterExpiry := c.AllowRenewalAfterExpiry() enableSSHCA := c.IsSSHCAEnabled() return Claims{ - MinTLSDur: &Duration{c.MinTLSCertDuration()}, - MaxTLSDur: &Duration{c.MaxTLSCertDuration()}, - DefaultTLSDur: &Duration{c.DefaultTLSCertDuration()}, - MinUserSSHDur: &Duration{c.MinUserSSHCertDuration()}, - MaxUserSSHDur: &Duration{c.MaxUserSSHCertDuration()}, - DefaultUserSSHDur: &Duration{c.DefaultUserSSHCertDuration()}, - MinHostSSHDur: &Duration{c.MinHostSSHCertDuration()}, - MaxHostSSHDur: &Duration{c.MaxHostSSHCertDuration()}, - DefaultHostSSHDur: &Duration{c.DefaultHostSSHCertDuration()}, - EnableSSHCA: &enableSSHCA, - DisableRenewal: &disableRenewal, - AllowRenewAfterExpiry: &allowRenewAfterExpiry, + MinTLSDur: &Duration{c.MinTLSCertDuration()}, + MaxTLSDur: &Duration{c.MaxTLSCertDuration()}, + DefaultTLSDur: &Duration{c.DefaultTLSCertDuration()}, + MinUserSSHDur: &Duration{c.MinUserSSHCertDuration()}, + MaxUserSSHDur: &Duration{c.MaxUserSSHCertDuration()}, + DefaultUserSSHDur: &Duration{c.DefaultUserSSHCertDuration()}, + MinHostSSHDur: &Duration{c.MinHostSSHCertDuration()}, + MaxHostSSHDur: &Duration{c.MaxHostSSHCertDuration()}, + DefaultHostSSHDur: &Duration{c.DefaultHostSSHCertDuration()}, + EnableSSHCA: &enableSSHCA, + DisableRenewal: &disableRenewal, + AllowRenewalAfterExpiry: &allowRenewalAfterExpiry, } } @@ -109,14 +109,14 @@ func (c *Claimer) IsDisableRenewal() bool { return *c.claims.DisableRenewal } -// AllowRenewAfterExpiry returns if the renewal flow is authorized if the +// AllowRenewalAfterExpiry returns if the renewal flow is authorized if the // certificate is expired. If the property is not set within the provisioner // then the global value from the authority configuration will be used. -func (c *Claimer) AllowRenewAfterExpiry() bool { - if c.claims == nil || c.claims.AllowRenewAfterExpiry == nil { - return *c.global.AllowRenewAfterExpiry +func (c *Claimer) AllowRenewalAfterExpiry() bool { + if c.claims == nil || c.claims.AllowRenewalAfterExpiry == nil { + return *c.global.AllowRenewalAfterExpiry } - return *c.claims.AllowRenewAfterExpiry + return *c.claims.AllowRenewalAfterExpiry } // DefaultSSHCertDuration returns the default SSH certificate duration for the diff --git a/authority/provisioner/controller.go b/authority/provisioner/controller.go index a91ebaac..afd28dcc 100644 --- a/authority/provisioner/controller.go +++ b/authority/provisioner/controller.go @@ -124,7 +124,7 @@ func DefaultAuthorizeRenew(ctx context.Context, p *Controller, cert *x509.Certif if now.Before(cert.NotBefore) { return errs.Unauthorized("certificate is not yet valid" + " " + now.UTC().Format(time.RFC3339Nano) + " vs " + cert.NotBefore.Format(time.RFC3339Nano)) } - if now.After(cert.NotAfter) && !p.Claimer.AllowRenewAfterExpiry() { + if now.After(cert.NotAfter) && !p.Claimer.AllowRenewalAfterExpiry() { return errs.Unauthorized("certificate has expired") } @@ -144,7 +144,7 @@ func DefaultAuthorizeSSHRenew(ctx context.Context, p *Controller, cert *ssh.Cert if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) { return errs.Unauthorized("certificate is not yet valid") } - if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) && !p.Claimer.AllowRenewAfterExpiry() { + if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) && !p.Claimer.AllowRenewalAfterExpiry() { return errs.Unauthorized("certificate has expired") } diff --git a/authority/provisioner/controller_test.go b/authority/provisioner/controller_test.go index 9fb90e9d..ebd38df1 100644 --- a/authority/provisioner/controller_test.go +++ b/authority/provisioner/controller_test.go @@ -160,13 +160,13 @@ func TestController_AuthorizeRenew(t *testing.T) { NotBefore: now, NotAfter: now.Add(time.Hour), }}, false}, - {"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error { + {"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error { return nil }}, args{ctx, &x509.Certificate{ NotBefore: now, NotAfter: now.Add(time.Hour), }}, false}, - {"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{ + {"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{ NotBefore: now.Add(-time.Hour), NotAfter: now.Add(-time.Minute), }}, false}, @@ -231,13 +231,13 @@ func TestController_AuthorizeSSHRenew(t *testing.T) { ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(time.Hour).Unix()), }}, false}, - {"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error { + {"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error { return nil }}, args{ctx, &ssh.Certificate{ ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(time.Hour).Unix()), }}, false}, - {"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{ + {"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{ ValidAfter: uint64(now.Add(-time.Hour).Unix()), ValidBefore: uint64(now.Add(-time.Minute).Unix()), }}, false}, @@ -296,7 +296,7 @@ func TestDefaultAuthorizeRenew(t *testing.T) { }}, false}, {"ok renew after expiry", args{ctx, &Controller{ Interface: &JWK{}, - Claimer: mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), + Claimer: mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), }, &x509.Certificate{ NotBefore: now.Add(-time.Hour), NotAfter: now.Add(-time.Minute), @@ -354,7 +354,7 @@ func TestDefaultAuthorizeSSHRenew(t *testing.T) { }}, false}, {"ok renew after expiry", args{ctx, &Controller{ Interface: &JWK{}, - Claimer: mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), + Claimer: mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), }, &ssh.Certificate{ ValidAfter: uint64(now.Add(-time.Hour).Unix()), ValidBefore: uint64(now.Add(-time.Minute).Unix()), diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index c55c58d2..3d032ea0 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -24,22 +24,22 @@ import ( ) var ( - defaultDisableRenewal = false - defaultAllowRenewAfterExpiry = false - defaultEnableSSHCA = true - globalProvisionerClaims = Claims{ - MinTLSDur: &Duration{5 * time.Minute}, - MaxTLSDur: &Duration{24 * time.Hour}, - DefaultTLSDur: &Duration{24 * time.Hour}, - MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs - MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, - DefaultUserSSHDur: &Duration{Duration: 16 * time.Hour}, - MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs - MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, - DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, - EnableSSHCA: &defaultEnableSSHCA, - DisableRenewal: &defaultDisableRenewal, - AllowRenewAfterExpiry: &defaultAllowRenewAfterExpiry, + defaultDisableRenewal = false + defaultAllowRenewalAfterExpiry = false + defaultEnableSSHCA = true + globalProvisionerClaims = Claims{ + MinTLSDur: &Duration{5 * time.Minute}, + MaxTLSDur: &Duration{24 * time.Hour}, + DefaultTLSDur: &Duration{24 * time.Hour}, + MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs + MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, + DefaultUserSSHDur: &Duration{Duration: 16 * time.Hour}, + MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs + MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + EnableSSHCA: &defaultEnableSSHCA, + DisableRenewal: &defaultDisableRenewal, + AllowRenewalAfterExpiry: &defaultAllowRenewalAfterExpiry, } testAudiences = Audiences{ Sign: []string{"https://ca.smallstep.com/1.0/sign", "https://ca.smallstep.com/sign"}, diff --git a/authority/provisioners.go b/authority/provisioners.go index a6ac5aa8..c00ae179 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -437,8 +437,8 @@ func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) { } pc := &provisioner.Claims{ - DisableRenewal: &c.DisableRenewal, - AllowRenewAfterExpiry: &c.AllowRenewAfterExpiry, + DisableRenewal: &c.DisableRenewal, + AllowRenewalAfterExpiry: &c.AllowRenewalAfterExpiry, } var err error @@ -476,18 +476,18 @@ func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims { } disableRenewal := config.DefaultDisableRenewal - allowRenewAfterExpiry := config.DefaultAllowRenewAfterExpiry + allowRenewalAfterExpiry := config.DefaultAllowRenewalAfterExpiry if c.DisableRenewal != nil { disableRenewal = *c.DisableRenewal } - if c.AllowRenewAfterExpiry != nil { - allowRenewAfterExpiry = *c.AllowRenewAfterExpiry + if c.AllowRenewalAfterExpiry != nil { + allowRenewalAfterExpiry = *c.AllowRenewalAfterExpiry } lc := &linkedca.Claims{ - DisableRenewal: disableRenewal, - AllowRenewAfterExpiry: allowRenewAfterExpiry, + DisableRenewal: disableRenewal, + AllowRenewalAfterExpiry: allowRenewalAfterExpiry, } if c.DefaultTLSDur != nil || c.MinTLSDur != nil || c.MaxTLSDur != nil { diff --git a/go.mod b/go.mod index 01ea550b..44d233bd 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,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.12.0 + go.step.sm/linkedca v0.15.0 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd google.golang.org/api v0.70.0 diff --git a/go.sum b/go.sum index 42577048..a7bca88e 100644 --- a/go.sum +++ b/go.sum @@ -711,8 +711,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.12.0 h1:FA18uJO5P6W2pklcezMs+w+N3dVbpKEE1LP9HLsJgg4= -go.step.sm/linkedca v0.12.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= +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= From 5f714f248527508ff45482c97165fe7d023d4657 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 13 Apr 2022 15:59:37 -0700 Subject: [PATCH 102/241] Fix tests for AuthorizeRenewToken --- authority/authorize_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/authority/authorize_test.go b/authority/authorize_test.go index c399eac4..cdcef1ad 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -1404,7 +1404,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { t1, c1 := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { @@ -1423,7 +1423,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { t2, c2 := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), IssuedAt: jose.NewNumericDate(now), @@ -1443,7 +1443,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { @@ -1462,7 +1462,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { badProvisioner, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { @@ -1500,7 +1500,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { badSubject, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "bad-subject", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { @@ -1519,7 +1519,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { badNotBefore, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/sign"}, Subject: "test.example.com", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now.Add(5 * time.Minute)), Expiry: jose.NewNumericDate(now.Add(10 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { @@ -1538,7 +1538,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { badExpiry, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/sign"}, Subject: "test.example.com", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now.Add(-5 * time.Minute)), Expiry: jose.NewNumericDate(now.Add(-time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { @@ -1557,7 +1557,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { badIssuedAt, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/sign"}, Subject: "test.example.com", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), IssuedAt: jose.NewNumericDate(now.Add(5 * time.Minute)), @@ -1577,7 +1577,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { badAudience, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/sign"}, Subject: "test.example.com", - Issuer: "step-cli", + Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { From ad5aedfa6077fb1172adfc50f2fc07b2de67d06d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 13 Apr 2022 16:00:15 -0700 Subject: [PATCH 103/241] Fix backward compatibility in AuthorizeAdminToken This commit validates both new and old issuers. --- authority/authorize.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/authority/authorize.go b/authority/authorize.go index b0a1fab4..fdf3941b 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -130,8 +130,7 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc // According to "rfc7519 JSON Web Token" acceptable skew should be no // more than a few minutes. if err := claims.ValidateWithLeeway(jose.Expected{ - Issuer: "step-admin-client/1.0", - Time: time.Now().UTC(), + Time: time.Now().UTC(), }, time.Minute); err != nil { return nil, admin.WrapError(admin.ErrorUnauthorizedType, err, "x5c.authorizeToken; invalid x5c claims") } @@ -141,6 +140,12 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc return nil, admin.NewError(admin.ErrorUnauthorizedType, "x5c.authorizeToken; x5c token has invalid audience claim (aud)") } + // validate issuer: old versions used the provisioner name, new version uses + // 'step-admin-client/1.0' + if claims.Issuer != "step-admin-client/1.0" && claims.Issuer != prov.GetName() { + return nil, admin.NewError(admin.ErrorUnauthorizedType, "x5c.authorizeToken; x5c token has invalid issuer claim (iss)") + } + if claims.Subject == "" { return nil, admin.NewError(admin.ErrorUnauthorizedType, "x5c.authorizeToken; x5c token subject cannot be empty") } From 30d5d89a13fa4bbb272801a9e82c28e271045647 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 15 Apr 2022 10:43:10 +0200 Subject: [PATCH 104/241] 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" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - - "go.step.sm/linkedca" - "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" ) 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 105/241] 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 3aebe8d019cb5de7c0d07fe3835a6f707c50a14d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 15 Apr 2022 12:19:32 -0700 Subject: [PATCH 106/241] Add missing comma in comment. --- cas/apiv1/options.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index cc5998ae..f69f933b 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -16,9 +16,9 @@ type Options struct { Type string `json:"type"` // CertificateAuthority reference: - // In StepCAS the value is the CA url, e.g. "https://ca.smallstep.com:9000". + // In StepCAS the value is the CA url, e.g., "https://ca.smallstep.com:9000". // In CloudCAS the format is "projects/*/locations/*/certificateAuthorities/*". - // In VaultCAS the value is the url, e.g. "https://vault.smallstep.com". + // In VaultCAS the value is the url, e.g., "https://vault.smallstep.com". CertificateAuthority string `json:"certificateAuthority,omitempty"` // CertificateAuthorityFingerprint is the root fingerprint used to From 99702d36484eadd855b2aaf46e1c9945c84e985a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 18 Apr 2022 21:14:30 +0200 Subject: [PATCH 107/241] 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 c066694c0cff29f9eae7e6f1aa05a842fc3b8781 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 18 Apr 2022 12:38:09 -0700 Subject: [PATCH 108/241] Allow renew token issuer to be the provisioner name. For consistency with AuthorizeAdminToken, AuthorizeRenewToken will allow the issuer to be either the fixed string 'step-ca-client/1.0' or the provisioner name. --- authority/authorize.go | 7 ++++++- authority/authorize_test.go | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/authority/authorize.go b/authority/authorize.go index fdf3941b..7f9f456c 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -404,7 +404,6 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509. } if err := claims.ValidateWithLeeway(jose.Expected{ - Issuer: "step-ca-client/1.0", Subject: leaf.Subject.CommonName, Time: time.Now().UTC(), }, time.Minute); err != nil { @@ -429,6 +428,12 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509. return nil, errs.InternalServerErr(err, errs.WithMessage("error validating renew token: invalid audience claim (aud)")) } + // validate issuer: old versions used the provisioner name, new version uses + // 'step-ca-client/1.0' + if claims.Issuer != "step-ca-client/1.0" && claims.Issuer != p.GetName() { + return nil, admin.NewError(admin.ErrorUnauthorizedType, "error validating renew token: invalid issuer claim (iss)") + } + return leaf, nil } diff --git a/authority/authorize_test.go b/authority/authorize_test.go index cdcef1ad..0a1ef53c 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -1440,6 +1440,25 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { }) return nil })) + t3, c3 := generateX5cToken(a1, signer, jose.Claims{ + Audience: []string{"https://example.com/1.0/renew"}, + Subject: "test.example.com", + Issuer: "step-cli", + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), + }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { + cert.NotBefore = now + cert.NotAfter = now.Add(time.Hour) + b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) + if err != nil { + return err + } + cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, + Value: b, + }) + return nil + })) badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", @@ -1607,6 +1626,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) { }{ {"ok", a1, args{ctx, t1}, c1, false}, {"ok expired cert", a1, args{ctx, t2}, c2, false}, + {"ok provisioner issuer", a1, args{ctx, t3}, c3, false}, {"fail token", a1, args{ctx, "not.a.token"}, nil, true}, {"fail token reuse", a1, args{ctx, t1}, nil, true}, {"fail token signature", a1, args{ctx, badSigner}, nil, true}, From 8d15a027a77203700ffc8947a1a0cc1baaa02d69 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 18 Apr 2022 21:47:13 +0200 Subject: [PATCH 109/241] 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 110/241] 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 111/241] 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 112/241] 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 113/241] 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 4770b405ba1e8147597e516ee6708615ec0293c7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 18 Apr 2022 15:18:23 -0700 Subject: [PATCH 114/241] Drop any query string from the admin tokens This commit makes sure the admin token audience is passed without a query string (or any fragment). --- ca/adminClient.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ca/adminClient.go b/ca/adminClient.go index c3ba666f..72f62dd8 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -90,6 +90,13 @@ func (c *AdminClient) generateAdminToken(aud *url.URL) (string, error) { return "", err } + // Drop any query string parameter from the token audience + aud = &url.URL{ + Scheme: aud.Scheme, + Host: aud.Host, + Path: aud.Path, + } + now := time.Now() tokOptions := []token.Options{ token.WithJWTID(jwtID), From 7f9034d22aff899bc433e5ceb00b7f25fc441567 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 10:24:52 +0200 Subject: [PATCH 115/241] 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 116/241] 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(), - } -} - -// 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) + Message: e.Error(), } - - 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 117/241] 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 118/241] 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 119/241] 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 120/241] 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 62e57f2073a94ad0b797ee1e9fb6d63a3b196844 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 21:24:21 +0200 Subject: [PATCH 121/241] Update changelog for v0.19.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb9ea0f9..916d9148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,41 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased - 0.18.3] - DATE +## [Unreleased - 0.19.1] - DATE +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security + +## [0.19.0] - 2022-04-19 ### Added - Added support for certificate renewals after expiry using the claim `allowRenewalAfterExpiry`. - Added support for `extraNames` in X.509 templates. +- Added `armv5` builds. - Added RA support using a Vault instance as the CA. +- Added `WithX509SignerFunc` authority option. +- Added a new `/roots.pem` endpoint to download the CA roots in PEM format. +- Added support for Azure `Managed Identity` tokens. - Added support for automatic configuration of linked RAs. +- Added support for the `--context` flag. It's now possible to start the + CA with `step --context=abc` to use the configuration from context `abc`. + When a context has been configured and no configuration file is provided + on startup, the configuration for the current context is used. +- Added startup info logging and option to skip it (`--quiet`). ### Changed -- Made SCEP CA URL paths dynamic -- Support two latest versions of Go (1.17, 1.18) +- Made SCEP CA URL paths dynamic. +- Support two latest versions of Go (1.17, 1.18). +- Upgrade go.step.sm/crypto to v0.16.1. +- Upgrade go.step.sm/linkedca to v0.15.0. ### Deprecated +- Go 1.16 support. ### Removed ### Fixed - Fixed admin credentials on RAs. +- Fixed ACME HTTP-01 challenges for IPv6 identifiers. +- Various improvements under the hood. ### Security ## [0.18.2] - 2022-03-01 @@ -52,7 +74,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Support for multiple certificate authority contexts. - Support for generating extractable keys and certificates on a pkcs#11 module. ### Changed -- Support two latest versions of golang (1.16, 1.17) +- Support two latest versions of Go (1.16, 1.17) ### Deprecated - go 1.15 support From 714b5e61e2951de15c77e1b2fc40140bec739182 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 19 Apr 2022 22:50:28 +0200 Subject: [PATCH 122/241] Fix `step` -> `step-ca` --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 916d9148..fdd504e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added support for Azure `Managed Identity` tokens. - Added support for automatic configuration of linked RAs. - Added support for the `--context` flag. It's now possible to start the - CA with `step --context=abc` to use the configuration from context `abc`. + CA with `step-ca --context=abc` to use the configuration from context `abc`. When a context has been configured and no configuration file is provided on startup, the configuration for the current context is used. - Added startup info logging and option to skip it (`--quiet`). From 18ca66069efa1ff7b2216144ba709d2314a20d44 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 19 Apr 2022 14:16:27 -0700 Subject: [PATCH 123/241] [action] issue uploading to scoop - attempt setting goamd64 --- .goreleaser.yml | 42 ++++-------------------------------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 441d5785..6886a88a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -29,6 +29,7 @@ builds: binary: bin/step-ca ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} + goamd64: v1 - id: step-cloudkms-init env: @@ -50,6 +51,7 @@ builds: binary: bin/step-cloudkms-init ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} + goamd64: v1 - id: step-awskms-init env: @@ -71,6 +73,7 @@ builds: binary: bin/step-awskms-init ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} + goamd64: v1 archives: - @@ -231,41 +234,4 @@ scoop: # Default is empty. license: "Apache-2.0" - #dockers: - # - dockerfile: docker/Dockerfile - # goos: linux - # goarch: amd64 - # use_buildx: true - # image_templates: - # - "smallstep/step-cli:latest" - # - "smallstep/step-cli:{{ .Tag }}" - # build_flag_templates: - # - "--platform=linux/amd64" - # - dockerfile: docker/Dockerfile - # goos: linux - # goarch: 386 - # use_buildx: true - # image_templates: - # - "smallstep/step-cli:latest" - # - "smallstep/step-cli:{{ .Tag }}" - # build_flag_templates: - # - "--platform=linux/386" - # - dockerfile: docker/Dockerfile - # goos: linux - # goarch: arm - # goarm: 7 - # use_buildx: true - # image_templates: - # - "smallstep/step-cli:latest" - # - "smallstep/step-cli:{{ .Tag }}" - # build_flag_templates: - # - "--platform=linux/arm/v7" - # - dockerfile: docker/Dockerfile - # goos: linux - # goarch: arm64 - # use_buildx: true - # image_templates: - # - "smallstep/step-cli:latest" - # - "smallstep/step-cli:{{ .Tag }}" - # build_flag_templates: - # - "--platform=linux/arm64/v8" + goamd64: v1 From 27b3d82f1d2d76d958253ff24ff52ba2449fbde0 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 19 Apr 2022 14:30:32 -0700 Subject: [PATCH 124/241] [action] goamd64 another attempt at fix --- .goreleaser.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 6886a88a..74c269e0 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -29,7 +29,8 @@ builds: binary: bin/step-ca ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - goamd64: v1 + goamd64: + - v1 - id: step-cloudkms-init env: @@ -51,7 +52,8 @@ builds: binary: bin/step-cloudkms-init ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - goamd64: v1 + goamd64: + - v1 - id: step-awskms-init env: @@ -73,7 +75,8 @@ builds: binary: bin/step-awskms-init ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - goamd64: v1 + goamd64: + - v1 archives: - From 605a959029d1a05dbfdb4a3f1c5fcf8a62d788fd Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 19 Apr 2022 14:53:47 -0700 Subject: [PATCH 125/241] [action] attempt to pin goreleaser version --- .github/workflows/release.yml | 2 +- .goreleaser.yml | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ab7084d..c90d949a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,7 +139,7 @@ jobs: name: Run GoReleaser uses: goreleaser/goreleaser-action@5a54d7e660bda43b405e8463261b3d25631ffe86 # v2.7.0 with: - version: latest + version: 'v1.7.0' args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.PAT }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 74c269e0..7d57e657 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -29,8 +29,6 @@ builds: binary: bin/step-ca ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - goamd64: - - v1 - id: step-cloudkms-init env: @@ -52,8 +50,6 @@ builds: binary: bin/step-cloudkms-init ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - goamd64: - - v1 - id: step-awskms-init env: @@ -75,8 +71,6 @@ builds: binary: bin/step-awskms-init ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - goamd64: - - v1 archives: - @@ -236,5 +230,3 @@ scoop: # Your app's license # Default is empty. license: "Apache-2.0" - - goamd64: v1 From 97b64aa8512f2d1bb587e6358f762d8be5e46f0c Mon Sep 17 00:00:00 2001 From: Carl Tashian Date: Wed, 20 Apr 2022 09:24:53 -0700 Subject: [PATCH 126/241] Cosmetic fix for consistency in the startup messages --- ca/ca.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ca/ca.go b/ca/ca.go index 0d4f1578..0380d166 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -321,7 +321,7 @@ func (ca *CA) Run() error { log.Printf("X.509 Root Fingerprint: %s", x509util.Fingerprint(crt)) } if authorityInfo.SSHCAHostPublicKey != nil { - log.Printf("SSH Host CA Key is %s\n", authorityInfo.SSHCAHostPublicKey) + log.Printf("SSH Host CA Key: %s\n", authorityInfo.SSHCAHostPublicKey) } if authorityInfo.SSHCAUserPublicKey != nil { log.Printf("SSH User CA Key: %s\n", authorityInfo.SSHCAUserPublicKey) From 340aa3206c1b01177292eb9347c17d851e806370 Mon Sep 17 00:00:00 2001 From: Carl Tashian Date: Wed, 20 Apr 2022 09:48:06 -0700 Subject: [PATCH 127/241] We now have an armv5 step-ca build; remove guard clause from RA install script --- scripts/install-step-ra.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/install-step-ra.sh b/scripts/install-step-ra.sh index 1da64ed6..74aa1914 100644 --- a/scripts/install-step-ra.sh +++ b/scripts/install-step-ra.sh @@ -38,11 +38,6 @@ case $arch in armv7*) arch="armv7" ;; esac -if [ "$arch" = "armv5" ]; then - echo "This script doesn't work on armv5 machines" - exit 1 -fi - if ! hash jq &> /dev/null; then echo "This script requires the jq commmand; please install it." exit 1 From a2cfbe3d546404932ca15bd8b7db981932a9a179 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 21 Apr 2022 12:14:03 +0200 Subject: [PATCH 128/241] 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 | 40 +-- authority/policy/policy.go | 32 +-- authority/provisioners.go | 70 ++--- policy/engine_test.go | 418 +++++++++++++---------------- policy/options.go | 143 ++-------- policy/options_test.go | 369 +++---------------------- 11 files changed, 343 insertions(+), 796 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,24 +67,20 @@ func (o *X509NameOptions) HasNames() bool { len(o.URIDomains) > 0 } -// GetDeniedNameOptions returns the x509 denied name policy configuration -func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { +// GetAllowedNameOptions returns x509 allowed name policy configuration +func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions { if o == nil { return nil } - return o.DeniedNames + return o.AllowedNames } -// GetAllowedUserNameOptions returns the SSH allowed user name policy -// configuration. -func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { +// GetDeniedNameOptions returns the x509 denied name policy configuration +func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { if o == nil { return nil } - if o.User == nil { - return nil - } - return o.User.AllowedNames + return o.DeniedNames } func (o *X509PolicyOptions) IsWildcardLiteralAllowed() bool { @@ -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) - 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") + _, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128") 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), + 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, + nw1, nw2, nw3, }, - 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 129/241] Fix ACME policy comments --- acme/api/account_test.go | 9 ++++++ acme/api/order.go | 16 ++++++++--- acme/api/order_test.go | 47 ++++++++++++++++++++++++++++---- authority/admin/api/acme.go | 8 +----- authority/admin/api/acme_test.go | 16 ++++++----- 5 files changed, 72 insertions(+), 24 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,35 @@ func TestHandler_NewOrder(t *testing.T) { err: acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty"), } }, + "fail/acmeProvisionerFromContext-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, &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{ @@ -770,7 +798,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{ @@ -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 130/241] 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 131/241] 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 132/241] 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 133/241] 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 134/241] 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 135/241] 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 136/241] 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 137/241] 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 138/241] 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 139/241] 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 140/241] 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 141/241] 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 b91affdd34ae30e6c6e3ef61ea2fa8ac30944aed Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 25 Apr 2022 10:23:07 -0700 Subject: [PATCH 142/241] exposing authority configuration for provisioner cli commands --- authority/admin/db.go | 46 +++++++++++++++++++++++++++++++++ authority/admins.go | 6 ++--- authority/authority.go | 26 ++++++++++++++++--- authority/provisioners.go | 8 +++--- ca/adminClient.go | 24 +++++++++--------- ca/client.go | 53 ++++++++++++++++++++------------------- 6 files changed, 115 insertions(+), 48 deletions(-) diff --git a/authority/admin/db.go b/authority/admin/db.go index bf34a3c2..6e4e7c49 100644 --- a/authority/admin/db.go +++ b/authority/admin/db.go @@ -71,6 +71,52 @@ type DB interface { DeleteAdmin(ctx context.Context, id string) error } +type NoDB struct{} + +func NewNoDB() *NoDB { + return &NoDB{} +} + +func (n *NoDB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { + return nil +} + +func (n *NoDB) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) { + return nil, nil +} + +func (n *NoDB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) { + return nil, nil +} + +func (n *NoDB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { + return nil +} + +func (n *NoDB) DeleteProvisioner(ctx context.Context, id string) error { + return nil +} + +func (n *NoDB) CreateAdmin(ctx context.Context, admin *linkedca.Admin) error { + return nil +} + +func (n *NoDB) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) { + return nil, nil +} + +func (n *NoDB) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) { + return nil, nil +} + +func (n *NoDB) UpdateAdmin(ctx context.Context, prov *linkedca.Admin) error { + return nil +} + +func (n *NoDB) DeleteAdmin(ctx context.Context, id string) error { + return nil +} + // MockDB is an implementation of the DB interface that should only be used as // a mock in tests. type MockDB struct { diff --git a/authority/admins.go b/authority/admins.go index b975297a..c8e1ac66 100644 --- a/authority/admins.go +++ b/authority/admins.go @@ -49,7 +49,7 @@ func (a *Authority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov pr return admin.WrapErrorISE(err, "error creating admin") } if err := a.admins.Store(adm, prov); err != nil { - if err := a.reloadAdminResources(ctx); err != nil { + if err := a.ReloadAdminResources(ctx); err != nil { return admin.WrapErrorISE(err, "error reloading admin resources on failed admin store") } return admin.WrapErrorISE(err, "error storing admin in authority cache") @@ -66,7 +66,7 @@ func (a *Authority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Adm return nil, admin.WrapErrorISE(err, "error updating cached admin %s", id) } if err := a.adminDB.UpdateAdmin(ctx, adm); err != nil { - if err := a.reloadAdminResources(ctx); err != nil { + if err := a.ReloadAdminResources(ctx); err != nil { return nil, admin.WrapErrorISE(err, "error reloading admin resources on failed admin update") } return nil, admin.WrapErrorISE(err, "error updating admin %s", id) @@ -88,7 +88,7 @@ func (a *Authority) removeAdmin(ctx context.Context, id string) error { return admin.WrapErrorISE(err, "error removing admin %s from authority cache", id) } if err := a.adminDB.DeleteAdmin(ctx, id); err != nil { - if err := a.reloadAdminResources(ctx); err != nil { + if err := a.ReloadAdminResources(ctx); err != nil { return admin.WrapErrorISE(err, "error reloading admin resources on failed admin remove") } return admin.WrapErrorISE(err, "error deleting admin %s", id) diff --git a/authority/authority.go b/authority/authority.go index 9db38e14..2c10b626 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -115,6 +115,20 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) { return a, nil } +// FromOptions creates an Authority exclusively using the passed in options +// and does not intialize the Authority. +func FromOptions(opts ...Option) (*Authority, error) { + var a = new(Authority) + + // Apply options. + for _, fn := range opts { + if err := fn(a); err != nil { + return nil, err + } + } + return a, nil +} + // NewEmbedded initializes an authority that can be embedded in a different // project without the limitations of the config. func NewEmbedded(opts ...Option) (*Authority, error) { @@ -153,8 +167,8 @@ func NewEmbedded(opts ...Option) (*Authority, error) { return a, nil } -// reloadAdminResources reloads admins and provisioners from the DB. -func (a *Authority) reloadAdminResources(ctx context.Context) error { +// ReloadAdminResources reloads admins and provisioners from the DB. +func (a *Authority) ReloadAdminResources(ctx context.Context) error { var ( provList provisioner.List adminList []*linkedca.Admin @@ -551,7 +565,7 @@ func (a *Authority) init() error { } // Load Provisioners and Admins - if err := a.reloadAdminResources(context.Background()); err != nil { + if err := a.ReloadAdminResources(context.Background()); err != nil { return err } @@ -587,6 +601,12 @@ func (a *Authority) GetAdminDatabase() admin.DB { return a.adminDB } +// GetConfig returns the config. +func (a *Authority) GetConfig() *config.Config { + return a.config +} + +// GetInfo returns information about the authority. func (a *Authority) GetInfo() Info { ai := Info{ StartTime: a.startTime, diff --git a/authority/provisioners.go b/authority/provisioners.go index 63fb630b..5944f007 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -145,7 +145,7 @@ func (a *Authority) generateProvisionerConfig(ctx context.Context) (provisioner. } -// StoreProvisioner stores an provisioner.Interface to the authority. +// StoreProvisioner stores a provisioner to the authority. func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { a.adminMutex.Lock() defer a.adminMutex.Unlock() @@ -191,7 +191,7 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi } if err := a.provisioners.Store(certProv); err != nil { - if err := a.reloadAdminResources(ctx); err != nil { + if err := a.ReloadAdminResources(ctx); err != nil { return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner store") } return admin.WrapErrorISE(err, "error storing provisioner in authority cache") @@ -223,7 +223,7 @@ func (a *Authority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisio return admin.WrapErrorISE(err, "error updating provisioner '%s' in authority cache", nu.Name) } if err := a.adminDB.UpdateProvisioner(ctx, nu); err != nil { - if err := a.reloadAdminResources(ctx); err != nil { + if err := a.ReloadAdminResources(ctx); err != nil { return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner update") } return admin.WrapErrorISE(err, "error updating provisioner '%s'", nu.Name) @@ -267,7 +267,7 @@ func (a *Authority) RemoveProvisioner(ctx context.Context, id string) error { } // Remove provisioner from database. if err := a.adminDB.DeleteProvisioner(ctx, provID); err != nil { - if err := a.reloadAdminResources(ctx); err != nil { + if err := a.ReloadAdminResources(ctx); err != nil { return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner remove") } return admin.WrapErrorISE(err, "error deleting provisioner %s", provName) diff --git a/ca/adminClient.go b/ca/adminClient.go index 72f62dd8..e898a898 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -363,19 +363,19 @@ retry: // GetProvisioner performs the GET /admin/provisioners/{name} request to the CA. func (c *AdminClient) GetProvisioner(opts ...ProvisionerOption) (*linkedca.Provisioner, error) { var retried bool - o := new(provisionerOptions) - if err := o.apply(opts); err != nil { + o := new(ProvisionerOptions) + if err := o.Apply(opts); err != nil { return nil, err } var u *url.URL switch { - case len(o.id) > 0: + case len(o.ID) > 0: u = c.endpoint.ResolveReference(&url.URL{ Path: "/admin/provisioners/id", RawQuery: o.rawQuery(), }) - case len(o.name) > 0: - u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.name)}) + case len(o.Name) > 0: + u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.Name)}) default: return nil, errors.New("must set either name or id in method options") } @@ -410,8 +410,8 @@ retry: // GetProvisionersPaginate performs the GET /admin/provisioners request to the CA. func (c *AdminClient) GetProvisionersPaginate(opts ...ProvisionerOption) (*adminAPI.GetProvisionersResponse, error) { var retried bool - o := new(provisionerOptions) - if err := o.apply(opts); err != nil { + o := new(ProvisionerOptions) + if err := o.Apply(opts); err != nil { return nil, err } u := c.endpoint.ResolveReference(&url.URL{ @@ -472,19 +472,19 @@ func (c *AdminClient) RemoveProvisioner(opts ...ProvisionerOption) error { retried bool ) - o := new(provisionerOptions) - if err := o.apply(opts); err != nil { + o := new(ProvisionerOptions) + if err := o.Apply(opts); err != nil { return err } switch { - case len(o.id) > 0: + case len(o.ID) > 0: u = c.endpoint.ResolveReference(&url.URL{ Path: path.Join(adminURLPrefix, "provisioners/id"), RawQuery: o.rawQuery(), }) - case len(o.name) > 0: - u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.name)}) + case len(o.Name) > 0: + u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.Name)}) default: return errors.New("must set either name or id in method options") } diff --git a/ca/client.go b/ca/client.go index 0bd93195..3871c749 100644 --- a/ca/client.go +++ b/ca/client.go @@ -425,16 +425,17 @@ func parseEndpoint(endpoint string) (*url.URL, error) { } // ProvisionerOption is the type of options passed to the Provisioner method. -type ProvisionerOption func(o *provisionerOptions) error +type ProvisionerOption func(o *ProvisionerOptions) error -type provisionerOptions struct { - cursor string - limit int - id string - name string +// ProvisionerOptions stores options for the provisioner CRUD API. +type ProvisionerOptions struct { + Cursor string + Limit int + ID string + Name string } -func (o *provisionerOptions) apply(opts []ProvisionerOption) (err error) { +func (o *ProvisionerOptions) Apply(opts []ProvisionerOption) (err error) { for _, fn := range opts { if err = fn(o); err != nil { return @@ -443,51 +444,51 @@ func (o *provisionerOptions) apply(opts []ProvisionerOption) (err error) { return } -func (o *provisionerOptions) rawQuery() string { +func (o *ProvisionerOptions) rawQuery() string { v := url.Values{} - if len(o.cursor) > 0 { - v.Set("cursor", o.cursor) + if len(o.Cursor) > 0 { + v.Set("cursor", o.Cursor) } - if o.limit > 0 { - v.Set("limit", strconv.Itoa(o.limit)) + if o.Limit > 0 { + v.Set("limit", strconv.Itoa(o.Limit)) } - if len(o.id) > 0 { - v.Set("id", o.id) + if len(o.ID) > 0 { + v.Set("id", o.ID) } - if len(o.name) > 0 { - v.Set("name", o.name) + if len(o.Name) > 0 { + v.Set("name", o.Name) } return v.Encode() } // WithProvisionerCursor will request the provisioners starting with the given cursor. func WithProvisionerCursor(cursor string) ProvisionerOption { - return func(o *provisionerOptions) error { - o.cursor = cursor + return func(o *ProvisionerOptions) error { + o.Cursor = cursor return nil } } // WithProvisionerLimit will request the given number of provisioners. func WithProvisionerLimit(limit int) ProvisionerOption { - return func(o *provisionerOptions) error { - o.limit = limit + return func(o *ProvisionerOptions) error { + o.Limit = limit return nil } } // WithProvisionerID will request the given provisioner. func WithProvisionerID(id string) ProvisionerOption { - return func(o *provisionerOptions) error { - o.id = id + return func(o *ProvisionerOptions) error { + o.ID = id return nil } } // WithProvisionerName will request the given provisioner. func WithProvisionerName(name string) ProvisionerOption { - return func(o *provisionerOptions) error { - o.name = name + return func(o *ProvisionerOptions) error { + o.Name = name return nil } } @@ -810,8 +811,8 @@ retry: // paginate the provisioners. func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersResponse, error) { var retried bool - o := new(provisionerOptions) - if err := o.apply(opts); err != nil { + o := new(ProvisionerOptions) + if err := o.Apply(opts); err != nil { return nil, err } u := c.endpoint.ResolveReference(&url.URL{ From 76112c2da1fb9fc61e458cdbb4e74f60d2a728fd Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 26 Apr 2022 01:47:07 +0200 Subject: [PATCH 143/241] 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"), - }, - cert: &ssh.Certificate{ - ValidPrincipals: []string{ - "localhost", - }, + want: false, + wantErr: &NamePolicyError{ + Reason: NotAllowed, + NameType: DNSNameType, + Name: "host", }, - 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 144/241] 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 145/241] 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 | 428 +++++++++++----------- 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 | 103 +++--- 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, 485 insertions(+), 473 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 { - 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 { + engine, err := authPolicy.New(policyOptions) + if 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"}, + existingPolicyEngine, err := policy.New(&policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.hosts.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) - existingSSHHostPolicyEngine, err := policy.NewSSHHostPolicyEngine(&policy.SSHPolicyOptions{ - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - DNSDomains: []string{"*.hosts.example.com"}, + newX509Options := &policy.Options{ + X509: &policy.X509PolicyOptions{ + AllowedNames: &policy.X509NameOptions{ + DNSDomains: []string{"*.local"}, }, + DeniedNames: &policy.X509NameOptions{ + DNSDomains: []string{"badhost.local"}, + }, + AllowWildcardLiteral: true, + DisableCommonNameVerification: false, }, - }) - assert.NoError(t, err) + } - existingSSHUserPolicyEngine, err := policy.NewSSHUserPolicyEngine(&policy.SSHPolicyOptions{ - User: &policy.SSHUserCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - EmailAddresses: []string{"@mails.example.com"}, + 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) + } - newX509PolicyEngine, 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"}, + }, + }, }, - DeniedNames: &policy.X509NameOptions{ - DNSDomains: []string{"badhost.local"}, + } + + 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"}, + }, + }, }, - AllowWildcardLiteral: true, - DisableCommonNameVerification: false, - }) - assert.NoError(t, err) + } - newSSHHostPolicyEngine, err := policy.NewSSHHostPolicyEngine(&policy.SSHPolicyOptions{ - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ + newOptions := &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{"*"}, + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.local"}, + }, + DeniedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"badhost.local"}, + }, }, - DeniedNames: &policy.SSHNameOptions{ - Principals: []string{"root"}, + 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"}, + newAdminX509Options := &policy.Options{ + X509: &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"}, + newAdminSSHHostOptions := &policy.Options{ + SSH: &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"}, + newAdminSSHUserOptions := &policy.Options{ + SSH: &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 + 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"}, + userPolicyOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + User: &policy.SSHUserCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + Principals: []string{"user"}, + }, }, }, - Host: &policy.SSHHostCertificateOptions{ - AllowedNames: &policy.SSHNameOptions{ - DNSDomains: []string{"*.test.com"}, + } + userPolicy, err := policy.New(userPolicyOptions) + assert.FatalError(t, err) + + hostPolicyOptions := &policy.Options{ + SSH: &policy.SSHPolicyOptions{ + Host: &policy.SSHHostCertificateOptions{ + AllowedNames: &policy.SSHNameOptions{ + DNSDomains: []string{"*.test.com"}, + }, }, }, } - userPolicy, err := policy.NewSSHUserPolicyEngine(policyOptions) - assert.FatalError(t, err) - hostPolicy, err := policy.NewSSHHostPolicyEngine(policyOptions) + 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 146/241] 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 147/241] 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 9628fa356224902ba9527a41d0ec99a627cd3344 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Apr 2022 12:54:54 -0700 Subject: [PATCH 148/241] Add methods to store and retrieve an authority from the context. --- authority/authority.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/authority/authority.go b/authority/authority.go index 9db38e14..091a01ae 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "encoding/hex" "log" + "net/http" "strings" "sync" "time" @@ -153,6 +154,27 @@ func NewEmbedded(opts ...Option) (*Authority, error) { return a, nil } +type authorityKey struct{} + +// NewContext adds the given authority to the context. +func NewContext(ctx context.Context, a *Authority) context.Context { + return context.WithValue(ctx, authorityKey{}, a) +} + +// FromContext returns the current authority from the given context. +func FromContext(ctx context.Context) (a *Authority, ok bool) { + a, ok = ctx.Value(authorityKey{}).(*Authority) + return +} + +// Middleware adds the current authority to the request context. +func (a *Authority) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := NewContext(r.Context(), a) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + // reloadAdminResources reloads admins and provisioners from the DB. func (a *Authority) reloadAdminResources(ctx context.Context) error { var ( From 900a640f016981b2b9d13bbbb5626c6532dff35a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Apr 2022 12:55:28 -0700 Subject: [PATCH 149/241] Enable the authority middleware in the server --- ca/ca.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ca/ca.go b/ca/ca.go index 0380d166..bb8e15ac 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -279,6 +279,10 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { insecureHandler = logger.Middleware(insecureHandler) } + // Add authority handler + handler = auth.Middleware(handler) + insecureHandler = auth.Middleware(insecureHandler) + ca.srv = server.New(cfg.Address, handler, tlsConfig) // only start the insecure server if the insecure address is configured From a6b8e65d69f7703108c65ec8b6ef221692dea5df Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Apr 2022 12:58:40 -0700 Subject: [PATCH 150/241] Retrieve the authority from the context in api methods. --- api/api.go | 94 +++++++++++++++++++++++++++--------------------- api/rekey.go | 7 ++-- api/renew.go | 14 ++++---- api/revoke.go | 11 +++--- api/sign.go | 9 ++--- api/ssh.go | 44 +++++++++++++---------- api/sshRekey.go | 10 +++--- api/sshRenew.go | 13 +++---- api/sshRevoke.go | 9 +++-- 9 files changed, 121 insertions(+), 90 deletions(-) diff --git a/api/api.go b/api/api.go index da6309fd..9b795cf0 100644 --- a/api/api.go +++ b/api/api.go @@ -52,6 +52,16 @@ type Authority interface { Version() authority.Version } +var errAuthority = errors.New("authority is not in context") + +func mustAuthority(ctx context.Context) Authority { + a, ok := authority.FromContext(ctx) + if !ok { + panic(errAuthority) + } + return a +} + // TimeDuration is an alias of provisioner.TimeDuration type TimeDuration = provisioner.TimeDuration @@ -251,40 +261,40 @@ func New(auth Authority) RouterHandler { } func (h *caHandler) Route(r Router) { - r.MethodFunc("GET", "/version", h.Version) - r.MethodFunc("GET", "/health", h.Health) - r.MethodFunc("GET", "/root/{sha}", h.Root) - r.MethodFunc("POST", "/sign", h.Sign) - r.MethodFunc("POST", "/renew", h.Renew) - r.MethodFunc("POST", "/rekey", h.Rekey) - r.MethodFunc("POST", "/revoke", h.Revoke) - r.MethodFunc("GET", "/provisioners", h.Provisioners) - r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey) - r.MethodFunc("GET", "/roots", h.Roots) - r.MethodFunc("GET", "/roots.pem", h.RootsPEM) - r.MethodFunc("GET", "/federation", h.Federation) + r.MethodFunc("GET", "/version", Version) + r.MethodFunc("GET", "/health", Health) + r.MethodFunc("GET", "/root/{sha}", Root) + r.MethodFunc("POST", "/sign", Sign) + r.MethodFunc("POST", "/renew", Renew) + r.MethodFunc("POST", "/rekey", Rekey) + r.MethodFunc("POST", "/revoke", Revoke) + r.MethodFunc("GET", "/provisioners", Provisioners) + r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey) + r.MethodFunc("GET", "/roots", Roots) + r.MethodFunc("GET", "/roots.pem", RootsPEM) + r.MethodFunc("GET", "/federation", Federation) // SSH CA - r.MethodFunc("POST", "/ssh/sign", h.SSHSign) - r.MethodFunc("POST", "/ssh/renew", h.SSHRenew) - r.MethodFunc("POST", "/ssh/revoke", h.SSHRevoke) - r.MethodFunc("POST", "/ssh/rekey", h.SSHRekey) - r.MethodFunc("GET", "/ssh/roots", h.SSHRoots) - r.MethodFunc("GET", "/ssh/federation", h.SSHFederation) - r.MethodFunc("POST", "/ssh/config", h.SSHConfig) - r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) - r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost) - r.MethodFunc("GET", "/ssh/hosts", h.SSHGetHosts) - r.MethodFunc("POST", "/ssh/bastion", h.SSHBastion) + r.MethodFunc("POST", "/ssh/sign", SSHSign) + r.MethodFunc("POST", "/ssh/renew", SSHRenew) + r.MethodFunc("POST", "/ssh/revoke", SSHRevoke) + r.MethodFunc("POST", "/ssh/rekey", SSHRekey) + r.MethodFunc("GET", "/ssh/roots", SSHRoots) + r.MethodFunc("GET", "/ssh/federation", SSHFederation) + r.MethodFunc("POST", "/ssh/config", SSHConfig) + r.MethodFunc("POST", "/ssh/config/{type}", SSHConfig) + r.MethodFunc("POST", "/ssh/check-host", SSHCheckHost) + r.MethodFunc("GET", "/ssh/hosts", SSHGetHosts) + r.MethodFunc("POST", "/ssh/bastion", SSHBastion) // For compatibility with old code: - r.MethodFunc("POST", "/re-sign", h.Renew) - r.MethodFunc("POST", "/sign-ssh", h.SSHSign) - r.MethodFunc("GET", "/ssh/get-hosts", h.SSHGetHosts) + r.MethodFunc("POST", "/re-sign", Renew) + r.MethodFunc("POST", "/sign-ssh", SSHSign) + r.MethodFunc("GET", "/ssh/get-hosts", SSHGetHosts) } // Version is an HTTP handler that returns the version of the server. -func (h *caHandler) Version(w http.ResponseWriter, r *http.Request) { - v := h.Authority.Version() +func Version(w http.ResponseWriter, r *http.Request) { + v := mustAuthority(r.Context()).Version() render.JSON(w, VersionResponse{ Version: v.Version, RequireClientAuthentication: v.RequireClientAuthentication, @@ -292,17 +302,17 @@ func (h *caHandler) Version(w http.ResponseWriter, r *http.Request) { } // Health is an HTTP handler that returns the status of the server. -func (h *caHandler) Health(w http.ResponseWriter, r *http.Request) { +func Health(w http.ResponseWriter, r *http.Request) { render.JSON(w, HealthResponse{Status: "ok"}) } // Root is an HTTP handler that using the SHA256 from the URL, returns the root // certificate for the given SHA256. -func (h *caHandler) Root(w http.ResponseWriter, r *http.Request) { +func Root(w http.ResponseWriter, r *http.Request) { sha := chi.URLParam(r, "sha") sum := strings.ToLower(strings.ReplaceAll(sha, "-", "")) // Load root certificate with the - cert, err := h.Authority.Root(sum) + cert, err := mustAuthority(r.Context()).Root(sum) if err != nil { render.Error(w, errs.Wrapf(http.StatusNotFound, err, "%s was not found", r.RequestURI)) return @@ -320,18 +330,19 @@ func certChainToPEM(certChain []*x509.Certificate) []Certificate { } // Provisioners returns the list of provisioners configured in the authority. -func (h *caHandler) Provisioners(w http.ResponseWriter, r *http.Request) { +func Provisioners(w http.ResponseWriter, r *http.Request) { cursor, limit, err := ParseCursor(r) if err != nil { render.Error(w, err) return } - p, next, err := h.Authority.GetProvisioners(cursor, limit) + p, next, err := mustAuthority(r.Context()).GetProvisioners(cursor, limit) if err != nil { render.Error(w, errs.InternalServerErr(err)) return } + render.JSON(w, &ProvisionersResponse{ Provisioners: p, NextCursor: next, @@ -339,19 +350,20 @@ func (h *caHandler) Provisioners(w http.ResponseWriter, r *http.Request) { } // ProvisionerKey returns the encrypted key of a provisioner by it's key id. -func (h *caHandler) ProvisionerKey(w http.ResponseWriter, r *http.Request) { +func ProvisionerKey(w http.ResponseWriter, r *http.Request) { kid := chi.URLParam(r, "kid") - key, err := h.Authority.GetEncryptedKey(kid) + key, err := mustAuthority(r.Context()).GetEncryptedKey(kid) if err != nil { render.Error(w, errs.NotFoundErr(err)) return } + render.JSON(w, &ProvisionerKeyResponse{key}) } // Roots returns all the root certificates for the CA. -func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) { - roots, err := h.Authority.GetRoots() +func Roots(w http.ResponseWriter, r *http.Request) { + roots, err := mustAuthority(r.Context()).GetRoots() if err != nil { render.Error(w, errs.ForbiddenErr(err, "error getting roots")) return @@ -368,8 +380,8 @@ func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) { } // RootsPEM returns all the root certificates for the CA in PEM format. -func (h *caHandler) RootsPEM(w http.ResponseWriter, r *http.Request) { - roots, err := h.Authority.GetRoots() +func RootsPEM(w http.ResponseWriter, r *http.Request) { + roots, err := mustAuthority(r.Context()).GetRoots() if err != nil { render.Error(w, errs.InternalServerErr(err)) return @@ -391,8 +403,8 @@ func (h *caHandler) RootsPEM(w http.ResponseWriter, r *http.Request) { } // Federation returns all the public certificates in the federation. -func (h *caHandler) Federation(w http.ResponseWriter, r *http.Request) { - federated, err := h.Authority.GetFederation() +func Federation(w http.ResponseWriter, r *http.Request) { + federated, err := mustAuthority(r.Context()).GetFederation() if err != nil { render.Error(w, errs.ForbiddenErr(err, "error getting federated roots")) return diff --git a/api/rekey.go b/api/rekey.go index 3116cf74..cda843a3 100644 --- a/api/rekey.go +++ b/api/rekey.go @@ -27,7 +27,7 @@ func (s *RekeyRequest) Validate() error { } // Rekey is similar to renew except that the certificate will be renewed with new key from csr. -func (h *caHandler) Rekey(w http.ResponseWriter, r *http.Request) { +func Rekey(w http.ResponseWriter, r *http.Request) { if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { render.Error(w, errs.BadRequest("missing client certificate")) return @@ -44,7 +44,8 @@ func (h *caHandler) Rekey(w http.ResponseWriter, r *http.Request) { return } - certChain, err := h.Authority.Rekey(r.TLS.PeerCertificates[0], body.CsrPEM.CertificateRequest.PublicKey) + a := mustAuthority(r.Context()) + certChain, err := a.Rekey(r.TLS.PeerCertificates[0], body.CsrPEM.CertificateRequest.PublicKey) if err != nil { render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Rekey")) return @@ -60,6 +61,6 @@ func (h *caHandler) Rekey(w http.ResponseWriter, r *http.Request) { ServerPEM: certChainPEM[0], CaPEM: caPEM, CertChainPEM: certChainPEM, - TLSOptions: h.Authority.GetTLSOptions(), + TLSOptions: a.GetTLSOptions(), }, http.StatusCreated) } diff --git a/api/renew.go b/api/renew.go index 9c4bff32..6e9f680f 100644 --- a/api/renew.go +++ b/api/renew.go @@ -16,14 +16,15 @@ const ( // Renew uses the information of certificate in the TLS connection to create a // new one. -func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { - cert, err := h.getPeerCertificate(r) +func Renew(w http.ResponseWriter, r *http.Request) { + cert, err := getPeerCertificate(r) if err != nil { render.Error(w, err) return } - certChain, err := h.Authority.Renew(cert) + a := mustAuthority(r.Context()) + certChain, err := a.Renew(cert) if err != nil { render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew")) return @@ -39,17 +40,18 @@ func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { ServerPEM: certChainPEM[0], CaPEM: caPEM, CertChainPEM: certChainPEM, - TLSOptions: h.Authority.GetTLSOptions(), + TLSOptions: a.GetTLSOptions(), }, http.StatusCreated) } -func (h *caHandler) getPeerCertificate(r *http.Request) (*x509.Certificate, error) { +func getPeerCertificate(r *http.Request) (*x509.Certificate, error) { if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { return r.TLS.PeerCertificates[0], nil } if s := r.Header.Get(authorizationHeader); s != "" { if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 { - return h.Authority.AuthorizeRenewToken(r.Context(), parts[1]) + ctx := r.Context() + return mustAuthority(ctx).AuthorizeRenewToken(ctx, parts[1]) } } return nil, errs.BadRequest("missing client certificate") diff --git a/api/revoke.go b/api/revoke.go index c9da2c18..aebbb875 100644 --- a/api/revoke.go +++ b/api/revoke.go @@ -1,7 +1,6 @@ package api import ( - "context" "net/http" "golang.org/x/crypto/ocsp" @@ -49,7 +48,7 @@ func (r *RevokeRequest) Validate() (err error) { // NOTE: currently only Passive revocation is supported. // // TODO: Add CRL and OCSP support. -func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { +func Revoke(w http.ResponseWriter, r *http.Request) { var body RevokeRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -68,12 +67,14 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { PassiveOnly: body.Passive, } - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod) + ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.RevokeMethod) + a := mustAuthority(ctx) + // A token indicates that we are using the api via a provisioner token, // otherwise it is assumed that the certificate is revoking itself over mTLS. if len(body.OTT) > 0 { logOtt(w, body.OTT) - if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil { + if _, err := a.Authorize(ctx, body.OTT); err != nil { render.Error(w, errs.UnauthorizedErr(err)) return } @@ -98,7 +99,7 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { opts.MTLS = true } - if err := h.Authority.Revoke(ctx, opts); err != nil { + if err := a.Revoke(ctx, opts); err != nil { render.Error(w, errs.ForbiddenErr(err, "error revoking certificate")) return } diff --git a/api/sign.go b/api/sign.go index b6bfcc8b..b263e2e9 100644 --- a/api/sign.go +++ b/api/sign.go @@ -49,7 +49,7 @@ type SignResponse struct { // Sign is an HTTP handler that reads a certificate request and an // one-time-token (ott) from the body and creates a new certificate with the // information in the certificate request. -func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { +func Sign(w http.ResponseWriter, r *http.Request) { var body SignRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -68,13 +68,14 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { TemplateData: body.TemplateData, } - signOpts, err := h.Authority.AuthorizeSign(body.OTT) + a := mustAuthority(r.Context()) + signOpts, err := a.AuthorizeSign(body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) return } - certChain, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...) + certChain, err := a.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error signing certificate")) return @@ -89,6 +90,6 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { ServerPEM: certChainPEM[0], CaPEM: caPEM, CertChainPEM: certChainPEM, - TLSOptions: h.Authority.GetTLSOptions(), + TLSOptions: a.GetTLSOptions(), }, http.StatusCreated) } diff --git a/api/ssh.go b/api/ssh.go index 3b0de7c1..f3056fc5 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -250,7 +250,7 @@ type SSHBastionResponse struct { // SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token // (ott) from the body and creates a new SSH certificate with the information in // the request. -func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { +func SSHSign(w http.ResponseWriter, r *http.Request) { var body SSHSignRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -288,13 +288,15 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { } ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHSignMethod) - signOpts, err := h.Authority.Authorize(ctx, body.OTT) + + a := mustAuthority(ctx) + signOpts, err := a.Authorize(ctx, body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) return } - cert, err := h.Authority.SignSSH(ctx, publicKey, opts, signOpts...) + cert, err := a.SignSSH(ctx, publicKey, opts, signOpts...) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error signing ssh certificate")) return @@ -302,7 +304,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { var addUserCertificate *SSHCertificate if addUserPublicKey != nil && authority.IsValidForAddUser(cert) == nil { - addUserCert, err := h.Authority.SignSSHAddUser(ctx, addUserPublicKey, cert) + addUserCert, err := a.SignSSHAddUser(ctx, addUserPublicKey, cert) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error signing ssh certificate")) return @@ -315,7 +317,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { if cr := body.IdentityCSR.CertificateRequest; cr != nil { ctx := authority.NewContextWithSkipTokenReuse(r.Context()) ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) - signOpts, err := h.Authority.Authorize(ctx, body.OTT) + signOpts, err := a.Authorize(ctx, body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) return @@ -327,7 +329,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { NotAfter: time.Unix(int64(cert.ValidBefore), 0), }) - certChain, err := h.Authority.Sign(cr, provisioner.SignOptions{}, signOpts...) + certChain, err := a.Sign(cr, provisioner.SignOptions{}, signOpts...) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error signing identity certificate")) return @@ -344,8 +346,9 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { // SSHRoots is an HTTP handler that returns the SSH public keys for user and host // certificates. -func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) { - keys, err := h.Authority.GetSSHRoots(r.Context()) +func SSHRoots(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + keys, err := mustAuthority(ctx).GetSSHRoots(ctx) if err != nil { render.Error(w, errs.InternalServerErr(err)) return @@ -369,8 +372,9 @@ func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) { // SSHFederation is an HTTP handler that returns the federated SSH public keys // for user and host certificates. -func (h *caHandler) SSHFederation(w http.ResponseWriter, r *http.Request) { - keys, err := h.Authority.GetSSHFederation(r.Context()) +func SSHFederation(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + keys, err := mustAuthority(ctx).GetSSHFederation(ctx) if err != nil { render.Error(w, errs.InternalServerErr(err)) return @@ -394,7 +398,7 @@ func (h *caHandler) SSHFederation(w http.ResponseWriter, r *http.Request) { // SSHConfig is an HTTP handler that returns rendered templates for ssh clients // and servers. -func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { +func SSHConfig(w http.ResponseWriter, r *http.Request) { var body SSHConfigRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -405,7 +409,8 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { return } - ts, err := h.Authority.GetSSHConfig(r.Context(), body.Type, body.Data) + ctx := r.Context() + ts, err := mustAuthority(ctx).GetSSHConfig(ctx, body.Type, body.Data) if err != nil { render.Error(w, errs.InternalServerErr(err)) return @@ -426,7 +431,7 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { } // SSHCheckHost is the HTTP handler that returns if a hosts certificate exists or not. -func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { +func SSHCheckHost(w http.ResponseWriter, r *http.Request) { var body SSHCheckPrincipalRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -437,7 +442,8 @@ func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { return } - exists, err := h.Authority.CheckSSHHost(r.Context(), body.Principal, body.Token) + ctx := r.Context() + exists, err := mustAuthority(ctx).CheckSSHHost(ctx, body.Principal, body.Token) if err != nil { render.Error(w, errs.InternalServerErr(err)) return @@ -448,13 +454,14 @@ func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { } // SSHGetHosts is the HTTP handler that returns a list of valid ssh hosts. -func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { +func SSHGetHosts(w http.ResponseWriter, r *http.Request) { var cert *x509.Certificate if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { cert = r.TLS.PeerCertificates[0] } - hosts, err := h.Authority.GetSSHHosts(r.Context(), cert) + ctx := r.Context() + hosts, err := mustAuthority(ctx).GetSSHHosts(ctx, cert) if err != nil { render.Error(w, errs.InternalServerErr(err)) return @@ -465,7 +472,7 @@ func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { } // SSHBastion provides returns the bastion configured if any. -func (h *caHandler) SSHBastion(w http.ResponseWriter, r *http.Request) { +func SSHBastion(w http.ResponseWriter, r *http.Request) { var body SSHBastionRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -476,7 +483,8 @@ func (h *caHandler) SSHBastion(w http.ResponseWriter, r *http.Request) { return } - bastion, err := h.Authority.GetSSHBastion(r.Context(), body.User, body.Hostname) + ctx := r.Context() + bastion, err := mustAuthority(ctx).GetSSHBastion(ctx, body.User, body.Hostname) if err != nil { render.Error(w, errs.InternalServerErr(err)) return diff --git a/api/sshRekey.go b/api/sshRekey.go index 92278950..184f208a 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -39,7 +39,7 @@ type SSHRekeyResponse struct { // SSHRekey is an HTTP handler that reads an RekeySSHRequest with a one-time-token // (ott) from the body and creates a new SSH certificate with the information in // the request. -func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { +func SSHRekey(w http.ResponseWriter, r *http.Request) { var body SSHRekeyRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -59,7 +59,9 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { } ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRekeyMethod) - signOpts, err := h.Authority.Authorize(ctx, body.OTT) + + a := mustAuthority(ctx) + signOpts, err := a.Authorize(ctx, body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) return @@ -70,7 +72,7 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { return } - newCert, err := h.Authority.RekeySSH(ctx, oldCert, publicKey, signOpts...) + newCert, err := a.RekeySSH(ctx, oldCert, publicKey, signOpts...) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error rekeying ssh certificate")) return @@ -80,7 +82,7 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { notBefore := time.Unix(int64(oldCert.ValidAfter), 0) notAfter := time.Unix(int64(oldCert.ValidBefore), 0) - identity, err := h.renewIdentityCertificate(r, notBefore, notAfter) + identity, err := renewIdentityCertificate(r, notBefore, notAfter) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error renewing identity certificate")) return diff --git a/api/sshRenew.go b/api/sshRenew.go index 78d16fa6..606b45bb 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -37,7 +37,7 @@ type SSHRenewResponse struct { // SSHRenew is an HTTP handler that reads an RenewSSHRequest with a one-time-token // (ott) from the body and creates a new SSH certificate with the information in // the request. -func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { +func SSHRenew(w http.ResponseWriter, r *http.Request) { var body SSHRenewRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -51,7 +51,8 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { } ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRenewMethod) - _, err := h.Authority.Authorize(ctx, body.OTT) + a := mustAuthority(ctx) + _, err := a.Authorize(ctx, body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) return @@ -62,7 +63,7 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { return } - newCert, err := h.Authority.RenewSSH(ctx, oldCert) + newCert, err := a.RenewSSH(ctx, oldCert) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error renewing ssh certificate")) return @@ -72,7 +73,7 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { notBefore := time.Unix(int64(oldCert.ValidAfter), 0) notAfter := time.Unix(int64(oldCert.ValidBefore), 0) - identity, err := h.renewIdentityCertificate(r, notBefore, notAfter) + identity, err := renewIdentityCertificate(r, notBefore, notAfter) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error renewing identity certificate")) return @@ -85,7 +86,7 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { } // renewIdentityCertificate request the client TLS certificate if present. If notBefore and notAfter are passed the -func (h *caHandler) renewIdentityCertificate(r *http.Request, notBefore, notAfter time.Time) ([]Certificate, error) { +func renewIdentityCertificate(r *http.Request, notBefore, notAfter time.Time) ([]Certificate, error) { if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { return nil, nil } @@ -105,7 +106,7 @@ func (h *caHandler) renewIdentityCertificate(r *http.Request, notBefore, notAfte cert.NotAfter = notAfter } - certChain, err := h.Authority.Renew(cert) + certChain, err := mustAuthority(r.Context()).Renew(cert) if err != nil { return nil, err } diff --git a/api/sshRevoke.go b/api/sshRevoke.go index a33082cd..d377def9 100644 --- a/api/sshRevoke.go +++ b/api/sshRevoke.go @@ -48,7 +48,7 @@ func (r *SSHRevokeRequest) Validate() (err error) { // Revoke supports handful of different methods that revoke a Certificate. // // NOTE: currently only Passive revocation is supported. -func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) { +func SSHRevoke(w http.ResponseWriter, r *http.Request) { var body SSHRevokeRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, errs.BadRequestErr(err, "error reading request body")) @@ -68,16 +68,19 @@ func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) { } ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRevokeMethod) + a := mustAuthority(ctx) + // A token indicates that we are using the api via a provisioner token, // otherwise it is assumed that the certificate is revoking itself over mTLS. logOtt(w, body.OTT) - if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil { + + if _, err := a.Authorize(ctx, body.OTT); err != nil { render.Error(w, errs.UnauthorizedErr(err)) return } opts.OTT = body.OTT - if err := h.Authority.Revoke(ctx, opts); err != nil { + if err := a.Revoke(ctx, opts); err != nil { render.Error(w, errs.ForbiddenErr(err, "error revoking ssh certificate")) return } From a93653ea8e2d1c2274784a719e8665660a69b574 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Apr 2022 14:32:55 -0700 Subject: [PATCH 151/241] Use api.Route instead of the caHandler. --- api/api.go | 15 +++++++++------ ca/ca.go | 5 ++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/api/api.go b/api/api.go index 9b795cf0..2137e29a 100644 --- a/api/api.go +++ b/api/api.go @@ -249,18 +249,21 @@ type FederationResponse struct { } // caHandler is the type used to implement the different CA HTTP endpoints. -type caHandler struct { - Authority Authority +type caHandler struct{} + +// Route configures the http request router. +func (h *caHandler) Route(r Router) { + Route(r) } // New creates a new RouterHandler with the CA endpoints. +// +// Deprecated: Use api.Route(r Router) func New(auth Authority) RouterHandler { - return &caHandler{ - Authority: auth, - } + return &caHandler{} } -func (h *caHandler) Route(r Router) { +func Route(r Router) { r.MethodFunc("GET", "/version", Version) r.MethodFunc("GET", "/health", Health) r.MethodFunc("GET", "/root/{sha}", Root) diff --git a/ca/ca.go b/ca/ca.go index bb8e15ac..24da6311 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -170,10 +170,9 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { insecureHandler := http.Handler(insecureMux) // Add regular CA api endpoints in / and /1.0 - routerHandler := api.New(auth) - routerHandler.Route(mux) + api.Route(mux) mux.Route("/1.0", func(r chi.Router) { - routerHandler.Route(r) + api.Route(r) }) //Add ACME api endpoints in /acme and /1.0/acme From 817af3d6965be47ccec02d602cbd0e3a10d8bf59 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 10:38:53 -0700 Subject: [PATCH 152/241] Fix unit tests on the api package --- api/api.go | 7 +++-- api/api_test.go | 78 +++++++++++++++++++++++++--------------------- api/revoke_test.go | 4 +-- api/ssh_test.go | 56 ++++++++++++++++----------------- 4 files changed, 77 insertions(+), 68 deletions(-) diff --git a/api/api.go b/api/api.go index 2137e29a..e5f4f944 100644 --- a/api/api.go +++ b/api/api.go @@ -54,7 +54,8 @@ type Authority interface { var errAuthority = errors.New("authority is not in context") -func mustAuthority(ctx context.Context) Authority { +// mustAuthority will be replaced on unit tests. +var mustAuthority = func(ctx context.Context) Authority { a, ok := authority.FromContext(ctx) if !ok { panic(errAuthority) @@ -249,7 +250,9 @@ type FederationResponse struct { } // caHandler is the type used to implement the different CA HTTP endpoints. -type caHandler struct{} +type caHandler struct { + Authority Authority +} // Route configures the http request router. func (h *caHandler) Route(r Router) { diff --git a/api/api_test.go b/api/api_test.go index 39c77de7..698b629c 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -171,6 +171,17 @@ func parseCertificateRequest(data string) *x509.CertificateRequest { return csr } +func mockMustAuthority(t *testing.T, a Authority) { + t.Helper() + fn := mustAuthority + t.Cleanup(func() { + mustAuthority = fn + }) + mustAuthority = func(ctx context.Context) Authority { + return a + } +} + type mockAuthority struct { ret1, ret2 interface{} err error @@ -789,11 +800,10 @@ func Test_caHandler_Route(t *testing.T) { } } -func Test_caHandler_Health(t *testing.T) { +func Test_Health(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com/health", nil) w := httptest.NewRecorder() - h := New(&mockAuthority{}).(*caHandler) - h.Health(w, req) + Health(w, req) res := w.Result() if res.StatusCode != 200 { @@ -811,7 +821,7 @@ func Test_caHandler_Health(t *testing.T) { } } -func Test_caHandler_Root(t *testing.T) { +func Test_Root(t *testing.T) { tests := []struct { name string root *x509.Certificate @@ -832,9 +842,9 @@ func Test_caHandler_Root(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ret1: tt.root, err: tt.err}).(*caHandler) + mockMustAuthority(t, &mockAuthority{ret1: tt.root, err: tt.err}) w := httptest.NewRecorder() - h.Root(w, req) + Root(w, req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -855,7 +865,7 @@ func Test_caHandler_Root(t *testing.T) { } } -func Test_caHandler_Sign(t *testing.T) { +func Test_Sign(t *testing.T) { csr := parseCertificateRequest(csrPEM) valid, err := json.Marshal(SignRequest{ CsrPEM: CertificateRequest{csr}, @@ -896,7 +906,7 @@ func Test_caHandler_Sign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.signErr, authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return tt.certAttrOpts, tt.autherr @@ -904,10 +914,10 @@ func Test_caHandler_Sign(t *testing.T) { getTLSOptions: func() *authority.TLSOptions { return nil }, - }).(*caHandler) + }) req := httptest.NewRequest("POST", "http://example.com/sign", strings.NewReader(tt.input)) w := httptest.NewRecorder() - h.Sign(logging.NewResponseLogger(w), req) + Sign(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -928,7 +938,7 @@ func Test_caHandler_Sign(t *testing.T) { } } -func Test_caHandler_Renew(t *testing.T) { +func Test_Renew(t *testing.T) { cs := &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)}, } @@ -1018,7 +1028,7 @@ func Test_caHandler_Renew(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.err, authorizeRenewToken: func(ctx context.Context, ott string) (*x509.Certificate, error) { jwt, chain, err := jose.ParseX5cInsecure(ott, []*x509.Certificate{tt.root}) @@ -1039,12 +1049,12 @@ func Test_caHandler_Renew(t *testing.T) { getTLSOptions: func() *authority.TLSOptions { return nil }, - }).(*caHandler) + }) req := httptest.NewRequest("POST", "http://example.com/renew", nil) req.TLS = tt.tls req.Header = tt.header w := httptest.NewRecorder() - h.Renew(logging.NewResponseLogger(w), req) + Renew(logging.NewResponseLogger(w), req) res := w.Result() defer res.Body.Close() @@ -1073,7 +1083,7 @@ func Test_caHandler_Renew(t *testing.T) { } } -func Test_caHandler_Rekey(t *testing.T) { +func Test_Rekey(t *testing.T) { cs := &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)}, } @@ -1104,16 +1114,16 @@ func Test_caHandler_Rekey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.err, getTLSOptions: func() *authority.TLSOptions { return nil }, - }).(*caHandler) + }) req := httptest.NewRequest("POST", "http://example.com/rekey", strings.NewReader(tt.input)) req.TLS = tt.tls w := httptest.NewRecorder() - h.Rekey(logging.NewResponseLogger(w), req) + Rekey(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -1134,7 +1144,7 @@ func Test_caHandler_Rekey(t *testing.T) { } } -func Test_caHandler_Provisioners(t *testing.T) { +func Test_Provisioners(t *testing.T) { type fields struct { Authority Authority } @@ -1200,10 +1210,8 @@ func Test_caHandler_Provisioners(t *testing.T) { assert.FatalError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := &caHandler{ - Authority: tt.fields.Authority, - } - h.Provisioners(tt.args.w, tt.args.r) + mockMustAuthority(t, tt.fields.Authority) + Provisioners(tt.args.w, tt.args.r) rec := tt.args.w.(*httptest.ResponseRecorder) res := rec.Result() @@ -1238,7 +1246,7 @@ func Test_caHandler_Provisioners(t *testing.T) { } } -func Test_caHandler_ProvisionerKey(t *testing.T) { +func Test_ProvisionerKey(t *testing.T) { type fields struct { Authority Authority } @@ -1270,10 +1278,8 @@ func Test_caHandler_ProvisionerKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := &caHandler{ - Authority: tt.fields.Authority, - } - h.ProvisionerKey(tt.args.w, tt.args.r) + mockMustAuthority(t, tt.fields.Authority) + ProvisionerKey(tt.args.w, tt.args.r) rec := tt.args.w.(*httptest.ResponseRecorder) res := rec.Result() @@ -1298,7 +1304,7 @@ func Test_caHandler_ProvisionerKey(t *testing.T) { } } -func Test_caHandler_Roots(t *testing.T) { +func Test_Roots(t *testing.T) { cs := &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)}, } @@ -1319,11 +1325,11 @@ func Test_caHandler_Roots(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) + mockMustAuthority(t, &mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}) req := httptest.NewRequest("GET", "http://example.com/roots", nil) req.TLS = tt.tls w := httptest.NewRecorder() - h.Roots(w, req) + Roots(w, req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -1360,10 +1366,10 @@ func Test_caHandler_RootsPEM(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ret1: tt.roots, err: tt.err}).(*caHandler) + mockMustAuthority(t, &mockAuthority{ret1: tt.roots, err: tt.err}) req := httptest.NewRequest("GET", "https://example.com/roots", nil) w := httptest.NewRecorder() - h.RootsPEM(w, req) + RootsPEM(w, req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -1384,7 +1390,7 @@ func Test_caHandler_RootsPEM(t *testing.T) { } } -func Test_caHandler_Federation(t *testing.T) { +func Test_Federation(t *testing.T) { cs := &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)}, } @@ -1405,11 +1411,11 @@ func Test_caHandler_Federation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) + mockMustAuthority(t, &mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}) req := httptest.NewRequest("GET", "http://example.com/federation", nil) req.TLS = tt.tls w := httptest.NewRecorder() - h.Federation(w, req) + Federation(w, req) res := w.Result() if res.StatusCode != tt.statusCode { diff --git a/api/revoke_test.go b/api/revoke_test.go index 7635ce68..fa46dd90 100644 --- a/api/revoke_test.go +++ b/api/revoke_test.go @@ -223,13 +223,13 @@ func Test_caHandler_Revoke(t *testing.T) { for name, _tc := range tests { tc := _tc(t) t.Run(name, func(t *testing.T) { - h := New(tc.auth).(*caHandler) + mockMustAuthority(t, tc.auth) req := httptest.NewRequest("POST", "http://example.com/revoke", strings.NewReader(tc.input)) if tc.tls != nil { req.TLS = tc.tls } w := httptest.NewRecorder() - h.Revoke(logging.NewResponseLogger(w), req) + Revoke(logging.NewResponseLogger(w), req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) diff --git a/api/ssh_test.go b/api/ssh_test.go index 88a301f5..c6fee2de 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -251,7 +251,7 @@ func TestSignSSHRequest_Validate(t *testing.T) { } } -func Test_caHandler_SSHSign(t *testing.T) { +func Test_SSHSign(t *testing.T) { user, err := getSignedUserCertificate() assert.FatalError(t, err) host, err := getSignedHostCertificate() @@ -315,7 +315,7 @@ func Test_caHandler_SSHSign(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return []provisioner.SignOption{}, tt.authErr }, @@ -328,11 +328,11 @@ func Test_caHandler_SSHSign(t *testing.T) { sign: func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { return tt.tlsSignCerts, tt.tlsSignErr }, - }).(*caHandler) + }) req := httptest.NewRequest("POST", "http://example.com/ssh/sign", bytes.NewReader(tt.req)) w := httptest.NewRecorder() - h.SSHSign(logging.NewResponseLogger(w), req) + SSHSign(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -353,7 +353,7 @@ func Test_caHandler_SSHSign(t *testing.T) { } } -func Test_caHandler_SSHRoots(t *testing.T) { +func Test_SSHRoots(t *testing.T) { user, err := ssh.NewPublicKey(sshUserKey.Public()) assert.FatalError(t, err) userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) @@ -378,15 +378,15 @@ func Test_caHandler_SSHRoots(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ getSSHRoots: func(ctx context.Context) (*authority.SSHKeys, error) { return tt.keys, tt.keysErr }, - }).(*caHandler) + }) req := httptest.NewRequest("GET", "http://example.com/ssh/roots", http.NoBody) w := httptest.NewRecorder() - h.SSHRoots(logging.NewResponseLogger(w), req) + SSHRoots(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -407,7 +407,7 @@ func Test_caHandler_SSHRoots(t *testing.T) { } } -func Test_caHandler_SSHFederation(t *testing.T) { +func Test_SSHFederation(t *testing.T) { user, err := ssh.NewPublicKey(sshUserKey.Public()) assert.FatalError(t, err) userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) @@ -432,15 +432,15 @@ func Test_caHandler_SSHFederation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ getSSHFederation: func(ctx context.Context) (*authority.SSHKeys, error) { return tt.keys, tt.keysErr }, - }).(*caHandler) + }) req := httptest.NewRequest("GET", "http://example.com/ssh/federation", http.NoBody) w := httptest.NewRecorder() - h.SSHFederation(logging.NewResponseLogger(w), req) + SSHFederation(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -461,7 +461,7 @@ func Test_caHandler_SSHFederation(t *testing.T) { } } -func Test_caHandler_SSHConfig(t *testing.T) { +func Test_SSHConfig(t *testing.T) { userOutput := []templates.Output{ {Name: "config.tpl", Type: templates.File, Comment: "#", Path: "ssh/config", Content: []byte("UserKnownHostsFile /home/user/.step/ssh/known_hosts")}, {Name: "known_host.tpl", Type: templates.File, Comment: "#", Path: "ssh/known_host", Content: []byte("@cert-authority * ecdsa-sha2-nistp256 AAAA...=")}, @@ -492,15 +492,15 @@ func Test_caHandler_SSHConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ getSSHConfig: func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { return tt.output, tt.err }, - }).(*caHandler) + }) req := httptest.NewRequest("GET", "http://example.com/ssh/config", strings.NewReader(tt.req)) w := httptest.NewRecorder() - h.SSHConfig(logging.NewResponseLogger(w), req) + SSHConfig(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -521,7 +521,7 @@ func Test_caHandler_SSHConfig(t *testing.T) { } } -func Test_caHandler_SSHCheckHost(t *testing.T) { +func Test_SSHCheckHost(t *testing.T) { tests := []struct { name string req string @@ -539,15 +539,15 @@ func Test_caHandler_SSHCheckHost(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ checkSSHHost: func(ctx context.Context, principal, token string) (bool, error) { return tt.exists, tt.err }, - }).(*caHandler) + }) req := httptest.NewRequest("GET", "http://example.com/ssh/check-host", strings.NewReader(tt.req)) w := httptest.NewRecorder() - h.SSHCheckHost(logging.NewResponseLogger(w), req) + SSHCheckHost(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -568,7 +568,7 @@ func Test_caHandler_SSHCheckHost(t *testing.T) { } } -func Test_caHandler_SSHGetHosts(t *testing.T) { +func Test_SSHGetHosts(t *testing.T) { hosts := []authority.Host{ {HostID: "1", HostTags: []authority.HostTag{{ID: "1", Name: "group", Value: "1"}}, Hostname: "host1"}, {HostID: "2", HostTags: []authority.HostTag{{ID: "1", Name: "group", Value: "1"}, {ID: "2", Name: "group", Value: "2"}}, Hostname: "host2"}, @@ -590,15 +590,15 @@ func Test_caHandler_SSHGetHosts(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ getSSHHosts: func(context.Context, *x509.Certificate) ([]authority.Host, error) { return tt.hosts, tt.err }, - }).(*caHandler) + }) req := httptest.NewRequest("GET", "http://example.com/ssh/host", http.NoBody) w := httptest.NewRecorder() - h.SSHGetHosts(logging.NewResponseLogger(w), req) + SSHGetHosts(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -619,7 +619,7 @@ func Test_caHandler_SSHGetHosts(t *testing.T) { } } -func Test_caHandler_SSHBastion(t *testing.T) { +func Test_SSHBastion(t *testing.T) { bastion := &authority.Bastion{ Hostname: "bastion.local", } @@ -645,15 +645,15 @@ func Test_caHandler_SSHBastion(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + mockMustAuthority(t, &mockAuthority{ getSSHBastion: func(ctx context.Context, user, hostname string) (*authority.Bastion, error) { return tt.bastion, tt.bastionErr }, - }).(*caHandler) + }) req := httptest.NewRequest("POST", "http://example.com/ssh/bastion", bytes.NewReader(tt.req)) w := httptest.NewRecorder() - h.SSHBastion(logging.NewResponseLogger(w), req) + SSHBastion(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { From d5070ecf31b5c1c268a8e9f0243d6f240c6e3738 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 11:06:55 -0700 Subject: [PATCH 153/241] Use server BaseContext Instead of using the authority middleware this change adds the authority in the base context of the server. --- ca/ca.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/ca/ca.go b/ca/ca.go index 24da6311..795fa77a 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -1,10 +1,12 @@ package ca import ( + "context" "crypto/tls" "crypto/x509" "fmt" "log" + "net" "net/http" "net/url" "reflect" @@ -279,10 +281,12 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } // Add authority handler - handler = auth.Middleware(handler) - insecureHandler = auth.Middleware(insecureHandler) + baseContext := buildContext(auth) ca.srv = server.New(cfg.Address, handler, tlsConfig) + ca.srv.BaseContext = func(net.Listener) context.Context { + return baseContext + } // only start the insecure server if the insecure address is configured // and, currently, also only when it should serve SCEP endpoints. @@ -292,11 +296,20 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { // will probably introduce more complexity in terms of graceful // reload. ca.insecureSrv = server.New(cfg.InsecureAddress, insecureHandler, nil) + ca.insecureSrv.BaseContext = func(net.Listener) context.Context { + return baseContext + } } return ca, nil } +func buildContext(a *authority.Authority) context.Context { + ctx := authority.NewContext(context.Background(), a) + + return ctx +} + // Run starts the CA calling to the server ListenAndServe method. func (ca *CA) Run() error { var wg sync.WaitGroup From 48e2fabeb828b42c043820e6bc010db08b765b96 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 11:38:06 -0700 Subject: [PATCH 154/241] Add authority.MustFromContext --- api/api.go | 8 +------- authority/authority.go | 15 ++++++++------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/api/api.go b/api/api.go index e5f4f944..0ca4a5ef 100644 --- a/api/api.go +++ b/api/api.go @@ -52,15 +52,9 @@ type Authority interface { Version() authority.Version } -var errAuthority = errors.New("authority is not in context") - // mustAuthority will be replaced on unit tests. var mustAuthority = func(ctx context.Context) Authority { - a, ok := authority.FromContext(ctx) - if !ok { - panic(errAuthority) - } - return a + return authority.MustFromContext(ctx) } // TimeDuration is an alias of provisioner.TimeDuration diff --git a/authority/authority.go b/authority/authority.go index 091a01ae..92ed6b31 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -7,7 +7,6 @@ import ( "crypto/x509" "encoding/hex" "log" - "net/http" "strings" "sync" "time" @@ -167,12 +166,14 @@ func FromContext(ctx context.Context) (a *Authority, ok bool) { return } -// Middleware adds the current authority to the request context. -func (a *Authority) Middleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := NewContext(r.Context(), a) - next.ServeHTTP(w, r.WithContext(ctx)) - }) +// MustFromContext returns the current authority from the given context. It will +// panic if the authority is not in the context. +func MustFromContext(ctx context.Context) *Authority { + if a, ok := FromContext(ctx); !ok { + panic("authority is not in the context") + } else { + return a + } } // reloadAdminResources reloads admins and provisioners from the DB. From 88a1bf17cf0dc3e69938eb28f03a02ffeecdf3e7 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 27 Apr 2022 11:40:43 -0700 Subject: [PATCH 155/241] 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 623c2965557a1aeadff32c1f0b6293d2277fb9e8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 11:58:52 -0700 Subject: [PATCH 156/241] Create context methods from admin database --- authority/admin/db.go | 23 +++++++++++++++++++++++ ca/ca.go | 8 +++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/authority/admin/db.go b/authority/admin/db.go index bf34a3c2..2da1a59a 100644 --- a/authority/admin/db.go +++ b/authority/admin/db.go @@ -71,6 +71,29 @@ type DB interface { DeleteAdmin(ctx context.Context, id string) error } +type dbKey struct{} + +// NewContext adds the given admin database to the context. +func NewContext(ctx context.Context, db DB) context.Context { + return context.WithValue(ctx, dbKey{}, db) +} + +// FromContext returns the current admin database from the given context. +func FromContext(ctx context.Context) (db DB, ok bool) { + db, ok = ctx.Value(dbKey{}).(DB) + return +} + +// MustFromContext returns the current admin database from the given context. It +// will panic if it's not in the context. +func MustFromContext(ctx context.Context) DB { + if db, ok := FromContext(ctx); !ok { + panic("admin database is not in the context") + } else { + return db + } +} + // MockDB is an implementation of the DB interface that should only be used as // a mock in tests. type MockDB struct { diff --git a/ca/ca.go b/ca/ca.go index 795fa77a..2df52555 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -20,6 +20,7 @@ import ( acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/admin" adminAPI "github.com/smallstep/certificates/authority/admin/api" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/db" @@ -280,7 +281,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { insecureHandler = logger.Middleware(insecureHandler) } - // Add authority handler + // Create context with all the necessary values. baseContext := buildContext(auth) ca.srv = server.New(cfg.Address, handler, tlsConfig) @@ -304,9 +305,14 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { return ca, nil } +// buildContext builds the server base context. func buildContext(a *authority.Authority) context.Context { ctx := authority.NewContext(context.Background(), a) + if db := a.GetAdminDatabase(); db != nil { + ctx = admin.NewContext(ctx, db) + } + return ctx } From 00f181dec3aa66962fb35788f9bf433e7b48c781 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 11:59:32 -0700 Subject: [PATCH 157/241] Use contexts in admin api handlers --- authority/admin/api/acme.go | 14 +++++--- authority/admin/api/admin.go | 25 ++++++------- authority/admin/api/handler.go | 56 +++++++++++++++++------------ authority/admin/api/middleware.go | 14 ++++---- authority/admin/api/provisioner.go | 58 +++++++++++++++++------------- 5 files changed, 95 insertions(+), 72 deletions(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 21a7229d..2c189624 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -40,11 +40,11 @@ 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 requireEABEnabled(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() provName := chi.URLParam(r, "provisionerName") - eabEnabled, prov, err := h.provisionerHasEABEnabled(ctx, provName) + eabEnabled, prov, err := provisionerHasEABEnabled(ctx, provName) if err != nil { render.Error(w, err) return @@ -60,16 +60,20 @@ func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { // 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) { +func 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 { + + auth := mustAuthority(ctx) + db := admin.MustFromContext(ctx) + + if p, err = auth.LoadProvisionerByName(provisionerName); err != nil { return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName) } - prov, err := h.adminDB.GetProvisioner(ctx, p.GetID()) + prov, err := db.GetProvisioner(ctx, p.GetID()) if err != nil { return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID()) } diff --git a/authority/admin/api/admin.go b/authority/admin/api/admin.go index 5e4b9c30..6ef6f0eb 100644 --- a/authority/admin/api/admin.go +++ b/authority/admin/api/admin.go @@ -81,10 +81,10 @@ type DeleteResponse struct { } // GetAdmin returns the requested admin, or an error. -func (h *Handler) GetAdmin(w http.ResponseWriter, r *http.Request) { +func GetAdmin(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") - adm, ok := h.auth.LoadAdminByID(id) + adm, ok := mustAuthority(r.Context()).LoadAdminByID(id) if !ok { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "admin %s not found", id)) @@ -94,7 +94,7 @@ func (h *Handler) GetAdmin(w http.ResponseWriter, r *http.Request) { } // GetAdmins returns a segment of admins associated with the authority. -func (h *Handler) GetAdmins(w http.ResponseWriter, r *http.Request) { +func GetAdmins(w http.ResponseWriter, r *http.Request) { cursor, limit, err := api.ParseCursor(r) if err != nil { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, @@ -102,7 +102,7 @@ func (h *Handler) GetAdmins(w http.ResponseWriter, r *http.Request) { return } - admins, nextCursor, err := h.auth.GetAdmins(cursor, limit) + admins, nextCursor, err := mustAuthority(r.Context()).GetAdmins(cursor, limit) if err != nil { render.Error(w, admin.WrapErrorISE(err, "error retrieving paginated admins")) return @@ -114,7 +114,7 @@ func (h *Handler) GetAdmins(w http.ResponseWriter, r *http.Request) { } // CreateAdmin creates a new admin. -func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request) { +func CreateAdmin(w http.ResponseWriter, r *http.Request) { var body CreateAdminRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body")) @@ -126,7 +126,8 @@ func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request) { return } - p, err := h.auth.LoadProvisionerByName(body.Provisioner) + auth := mustAuthority(r.Context()) + p, err := auth.LoadProvisionerByName(body.Provisioner) if err != nil { render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", body.Provisioner)) return @@ -137,7 +138,7 @@ func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request) { Type: body.Type, } // Store to authority collection. - if err := h.auth.StoreAdmin(r.Context(), adm, p); err != nil { + if err := auth.StoreAdmin(r.Context(), adm, p); err != nil { render.Error(w, admin.WrapErrorISE(err, "error storing admin")) return } @@ -146,10 +147,10 @@ func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request) { } // DeleteAdmin deletes admin. -func (h *Handler) DeleteAdmin(w http.ResponseWriter, r *http.Request) { +func DeleteAdmin(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") - if err := h.auth.RemoveAdmin(r.Context(), id); err != nil { + if err := mustAuthority(r.Context()).RemoveAdmin(r.Context(), id); err != nil { render.Error(w, admin.WrapErrorISE(err, "error deleting admin %s", id)) return } @@ -158,7 +159,7 @@ func (h *Handler) DeleteAdmin(w http.ResponseWriter, r *http.Request) { } // UpdateAdmin updates an existing admin. -func (h *Handler) UpdateAdmin(w http.ResponseWriter, r *http.Request) { +func UpdateAdmin(w http.ResponseWriter, r *http.Request) { var body UpdateAdminRequest if err := read.JSON(r.Body, &body); err != nil { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body")) @@ -171,8 +172,8 @@ func (h *Handler) UpdateAdmin(w http.ResponseWriter, r *http.Request) { } id := chi.URLParam(r, "id") - - adm, err := h.auth.UpdateAdmin(r.Context(), id, &linkedca.Admin{Type: body.Type}) + auth := mustAuthority(r.Context()) + adm, err := auth.UpdateAdmin(r.Context(), id, &linkedca.Admin{Type: body.Type}) if err != nil { render.Error(w, admin.WrapErrorISE(err, "error updating admin %s", id)) return diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 99e74c88..0acd3ca9 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -1,56 +1,66 @@ package api import ( + "context" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/admin" ) // Handler is the Admin API request handler. type Handler struct { - adminDB admin.DB - auth adminAuthority - acmeDB acme.DB acmeResponder acmeAdminResponderInterface } +// Route traffic and implement the Router interface. +// +// Deprecated: use Route(r api.Router, acmeResponder acmeAdminResponderInterface) +func (h *Handler) Route(r api.Router) { + Route(r, h.acmeResponder) +} + // NewHandler returns a new Authority Config Handler. +// +// Deprecated: use Route(r api.Router, acmeResponder acmeAdminResponderInterface) func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface) api.RouterHandler { return &Handler{ - auth: auth, - adminDB: adminDB, - acmeDB: acmeDB, acmeResponder: acmeResponder, } } +var mustAuthority = func(ctx context.Context) adminAuthority { + return authority.MustFromContext(ctx) +} + // Route traffic and implement the Router interface. -func (h *Handler) Route(r api.Router) { +func Route(r api.Router, acmeResponder acmeAdminResponderInterface) { authnz := func(next nextHTTP) nextHTTP { - return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) + return extractAuthorizeTokenAdmin(requireAPIEnabled(next)) } requireEABEnabled := func(next nextHTTP) nextHTTP { - return h.requireEABEnabled(next) + return requireEABEnabled(next) } // Provisioners - r.MethodFunc("GET", "/provisioners/{name}", authnz(h.GetProvisioner)) - r.MethodFunc("GET", "/provisioners", authnz(h.GetProvisioners)) - r.MethodFunc("POST", "/provisioners", authnz(h.CreateProvisioner)) - r.MethodFunc("PUT", "/provisioners/{name}", authnz(h.UpdateProvisioner)) - r.MethodFunc("DELETE", "/provisioners/{name}", authnz(h.DeleteProvisioner)) + r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner)) + r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners)) + r.MethodFunc("POST", "/provisioners", authnz(CreateProvisioner)) + r.MethodFunc("PUT", "/provisioners/{name}", authnz(UpdateProvisioner)) + r.MethodFunc("DELETE", "/provisioners/{name}", authnz(DeleteProvisioner)) // Admins - r.MethodFunc("GET", "/admins/{id}", authnz(h.GetAdmin)) - r.MethodFunc("GET", "/admins", authnz(h.GetAdmins)) - r.MethodFunc("POST", "/admins", authnz(h.CreateAdmin)) - r.MethodFunc("PATCH", "/admins/{id}", authnz(h.UpdateAdmin)) - r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin)) + r.MethodFunc("GET", "/admins/{id}", authnz(GetAdmin)) + r.MethodFunc("GET", "/admins", authnz(GetAdmins)) + r.MethodFunc("POST", "/admins", authnz(CreateAdmin)) + r.MethodFunc("PATCH", "/admins/{id}", authnz(UpdateAdmin)) + r.MethodFunc("DELETE", "/admins/{id}", authnz(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(requireEABEnabled(acmeResponder.GetExternalAccountKeys))) + r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(acmeResponder.GetExternalAccountKeys))) + r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(acmeResponder.CreateExternalAccountKey))) + r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(acmeResponder.DeleteExternalAccountKey))) } diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index b57dd6eb..9bd6c698 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -12,11 +12,10 @@ 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 requireAPIEnabled(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - if !h.auth.IsAdminAPIEnabled() { - render.Error(w, admin.NewError(admin.ErrorNotImplementedType, - "administration API not enabled")) + if !mustAuthority(r.Context()).IsAdminAPIEnabled() { + render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "administration API not enabled")) return } next(w, r) @@ -24,7 +23,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 extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { tok := r.Header.Get("Authorization") if tok == "" { @@ -33,13 +32,14 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { return } - adm, err := h.auth.AuthorizeAdminToken(r, tok) + ctx := r.Context() + adm, err := mustAuthority(ctx).AuthorizeAdminToken(r, tok) if err != nil { render.Error(w, err) return } - ctx := context.WithValue(r.Context(), adminContextKey, adm) + ctx = context.WithValue(ctx, adminContextKey, adm) next(w, r.WithContext(ctx)) } } diff --git a/authority/admin/api/provisioner.go b/authority/admin/api/provisioner.go index 1cad62dd..149f2c6a 100644 --- a/authority/admin/api/provisioner.go +++ b/authority/admin/api/provisioner.go @@ -23,29 +23,31 @@ type GetProvisionersResponse struct { } // GetProvisioner returns the requested provisioner, or an error. -func (h *Handler) GetProvisioner(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - id := r.URL.Query().Get("id") - name := chi.URLParam(r, "name") - +func GetProvisioner(w http.ResponseWriter, r *http.Request) { var ( p provisioner.Interface err error ) + + ctx := r.Context() + id := r.URL.Query().Get("id") + name := chi.URLParam(r, "name") + auth := mustAuthority(ctx) + db := admin.MustFromContext(ctx) + if len(id) > 0 { - if p, err = h.auth.LoadProvisionerByID(id); err != nil { + if p, err = auth.LoadProvisionerByID(id); err != nil { render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", id)) return } } else { - if p, err = h.auth.LoadProvisionerByName(name); err != nil { + if p, err = auth.LoadProvisionerByName(name); err != nil { render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) return } } - prov, err := h.adminDB.GetProvisioner(ctx, p.GetID()) + prov, err := db.GetProvisioner(ctx, p.GetID()) if err != nil { render.Error(w, err) return @@ -54,7 +56,7 @@ func (h *Handler) GetProvisioner(w http.ResponseWriter, r *http.Request) { } // GetProvisioners returns the given segment of provisioners associated with the authority. -func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) { +func GetProvisioners(w http.ResponseWriter, r *http.Request) { cursor, limit, err := api.ParseCursor(r) if err != nil { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, @@ -62,7 +64,7 @@ func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) { return } - p, next, err := h.auth.GetProvisioners(cursor, limit) + p, next, err := mustAuthority(r.Context()).GetProvisioners(cursor, limit) if err != nil { render.Error(w, errs.InternalServerErr(err)) return @@ -74,7 +76,7 @@ func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) { } // CreateProvisioner creates a new prov. -func (h *Handler) CreateProvisioner(w http.ResponseWriter, r *http.Request) { +func CreateProvisioner(w http.ResponseWriter, r *http.Request) { var prov = new(linkedca.Provisioner) if err := read.ProtoJSON(r.Body, prov); err != nil { render.Error(w, err) @@ -87,7 +89,7 @@ func (h *Handler) CreateProvisioner(w http.ResponseWriter, r *http.Request) { return } - if err := h.auth.StoreProvisioner(r.Context(), prov); err != nil { + if err := mustAuthority(r.Context()).StoreProvisioner(r.Context(), prov); err != nil { render.Error(w, admin.WrapErrorISE(err, "error storing provisioner %s", prov.Name)) return } @@ -95,27 +97,29 @@ func (h *Handler) CreateProvisioner(w http.ResponseWriter, r *http.Request) { } // DeleteProvisioner deletes a provisioner. -func (h *Handler) DeleteProvisioner(w http.ResponseWriter, r *http.Request) { - id := r.URL.Query().Get("id") - name := chi.URLParam(r, "name") - +func DeleteProvisioner(w http.ResponseWriter, r *http.Request) { var ( p provisioner.Interface err error ) + + id := r.URL.Query().Get("id") + name := chi.URLParam(r, "name") + auth := mustAuthority(r.Context()) + if len(id) > 0 { - if p, err = h.auth.LoadProvisionerByID(id); err != nil { + if p, err = auth.LoadProvisionerByID(id); err != nil { render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", id)) return } } else { - if p, err = h.auth.LoadProvisionerByName(name); err != nil { + if p, err = auth.LoadProvisionerByName(name); err != nil { render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) return } } - if err := h.auth.RemoveProvisioner(r.Context(), p.GetID()); err != nil { + if err := auth.RemoveProvisioner(r.Context(), p.GetID()); err != nil { render.Error(w, admin.WrapErrorISE(err, "error removing provisioner %s", p.GetName())) return } @@ -124,23 +128,27 @@ func (h *Handler) DeleteProvisioner(w http.ResponseWriter, r *http.Request) { } // UpdateProvisioner updates an existing prov. -func (h *Handler) UpdateProvisioner(w http.ResponseWriter, r *http.Request) { +func UpdateProvisioner(w http.ResponseWriter, r *http.Request) { var nu = new(linkedca.Provisioner) if err := read.ProtoJSON(r.Body, nu); err != nil { render.Error(w, err) return } + ctx := r.Context() name := chi.URLParam(r, "name") - _old, err := h.auth.LoadProvisionerByName(name) + auth := mustAuthority(ctx) + db := admin.MustFromContext(ctx) + + p, err := auth.LoadProvisionerByName(name) if err != nil { render.Error(w, admin.WrapErrorISE(err, "error loading provisioner from cached configuration '%s'", name)) return } - old, err := h.adminDB.GetProvisioner(r.Context(), _old.GetID()) + old, err := db.GetProvisioner(r.Context(), p.GetID()) if err != nil { - render.Error(w, admin.WrapErrorISE(err, "error loading provisioner from db '%s'", _old.GetID())) + render.Error(w, admin.WrapErrorISE(err, "error loading provisioner from db '%s'", p.GetID())) return } @@ -171,7 +179,7 @@ func (h *Handler) UpdateProvisioner(w http.ResponseWriter, r *http.Request) { return } - if err := h.auth.UpdateProvisioner(r.Context(), nu); err != nil { + if err := auth.UpdateProvisioner(r.Context(), nu); err != nil { render.Error(w, err) return } From 0446e823208559907ab89f6efe5ac88f2ba43edf Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 12:05:19 -0700 Subject: [PATCH 158/241] Add context methods for the authority database --- ca/ca.go | 9 +++++---- db/db.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/ca/ca.go b/ca/ca.go index 2df52555..f5cf30db 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -308,11 +308,12 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { // buildContext builds the server base context. func buildContext(a *authority.Authority) context.Context { ctx := authority.NewContext(context.Background(), a) - - if db := a.GetAdminDatabase(); db != nil { - ctx = admin.NewContext(ctx, db) + if authDB := a.GetDatabase(); authDB != nil { + ctx = db.NewContext(ctx, authDB) + } + if adminDB := a.GetAdminDatabase(); adminDB != nil { + ctx = admin.NewContext(ctx, adminDB) } - return ctx } diff --git a/db/db.go b/db/db.go index eccaf801..c4b1c8a7 100644 --- a/db/db.go +++ b/db/db.go @@ -1,6 +1,7 @@ package db import ( + "context" "crypto/x509" "encoding/json" "strconv" @@ -58,6 +59,29 @@ type AuthDB interface { Shutdown() error } +type dbKey struct{} + +// NewContext adds the given authority database to the context. +func NewContext(ctx context.Context, db AuthDB) context.Context { + return context.WithValue(ctx, dbKey{}, db) +} + +// FromContext returns the current authority database from the given context. +func FromContext(ctx context.Context) (db AuthDB, ok bool) { + db, ok = ctx.Value(dbKey{}).(AuthDB) + return +} + +// MustFromContext returns the current database from the given context. It +// will panic if it's not in the context. +func MustFromContext(ctx context.Context) AuthDB { + if db, ok := FromContext(ctx); !ok { + panic("authority database is not in the context") + } else { + return db + } +} + // DB is a wrapper over the nosql.DB interface. type DB struct { nosql.DB From bd412c9f4285aeaec0f0a1b9488492e9516d52d6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 12:11:00 -0700 Subject: [PATCH 159/241] Add context methods for the acme database --- acme/db.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/acme/db.go b/acme/db.go index 412276fd..a8637f57 100644 --- a/acme/db.go +++ b/acme/db.go @@ -48,6 +48,29 @@ type DB interface { UpdateOrder(ctx context.Context, o *Order) error } +type dbKey struct{} + +// NewContext adds the given acme database to the context. +func NewContext(ctx context.Context, db DB) context.Context { + return context.WithValue(ctx, dbKey{}, db) +} + +// FromContext returns the current acme database from the given context. +func FromContext(ctx context.Context) (db DB, ok bool) { + db, ok = ctx.Value(dbKey{}).(DB) + return +} + +// MustFromContext returns the current database from the given context. It +// will panic if it's not in the context. +func MustFromContext(ctx context.Context) DB { + if db, ok := FromContext(ctx); !ok { + panic("acme database is not in the context") + } else { + return db + } +} + // MockDB is an implementation of the DB interface that should only be used as // a mock in tests. type MockDB struct { From 8bd4e1d73e3886894f5a667cb41aea630c3ade0f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 12:13:16 -0700 Subject: [PATCH 160/241] Inject the acme database in the context --- ca/ca.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ca/ca.go b/ca/ca.go index f5cf30db..80756559 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -282,7 +282,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } // Create context with all the necessary values. - baseContext := buildContext(auth) + baseContext := buildContext(auth, acmeDB) ca.srv = server.New(cfg.Address, handler, tlsConfig) ca.srv.BaseContext = func(net.Listener) context.Context { @@ -306,7 +306,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } // buildContext builds the server base context. -func buildContext(a *authority.Authority) context.Context { +func buildContext(a *authority.Authority, acmeDB acme.DB) context.Context { ctx := authority.NewContext(context.Background(), a) if authDB := a.GetDatabase(); authDB != nil { ctx = db.NewContext(ctx, authDB) @@ -314,6 +314,9 @@ func buildContext(a *authority.Authority) context.Context { if adminDB := a.GetAdminDatabase(); adminDB != nil { ctx = admin.NewContext(ctx, adminDB) } + if acmeDB != nil { + ctx = acme.NewContext(ctx, acmeDB) + } return ctx } From 439cb81b133d994ecfc6a34b03c2067c4bb02d76 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 12:16:16 -0700 Subject: [PATCH 161/241] Use admin Route function --- ca/ca.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ca/ca.go b/ca/ca.go index 80756559..783255ce 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -221,9 +221,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) mux.Route("/admin", func(r chi.Router) { - adminHandler.Route(r) + adminAPI.Route(r, acmeAdminResponder) }) } } From d13537d426cce5a121115b40758105f8f46380ce Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 15:42:26 -0700 Subject: [PATCH 162/241] Use context in the acme handlers. --- acme/api/account.go | 33 +++--- acme/api/eab.go | 5 +- acme/api/handler.go | 234 +++++++++++++++++++++++++++-------------- acme/api/middleware.go | 80 ++++++++------ acme/api/order.go | 47 +++++---- acme/api/revoke.go | 18 ++-- 6 files changed, 267 insertions(+), 150 deletions(-) diff --git a/acme/api/account.go b/acme/api/account.go index ade51aef..8c8c4d97 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -67,7 +67,7 @@ func (u *UpdateAccountRequest) Validate() error { } // NewAccount is the handler resource for creating new ACME accounts. -func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { +func NewAccount(w http.ResponseWriter, r *http.Request) { ctx := r.Context() payload, err := payloadFromContext(ctx) if err != nil { @@ -114,18 +114,19 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } - eak, err := h.validateExternalAccountBinding(ctx, &nar) + eak, err := validateExternalAccountBinding(ctx, &nar) if err != nil { render.Error(w, err) return } + db := acme.MustFromContext(ctx) acc = &acme.Account{ Key: jwk, Contact: nar.Contact, Status: acme.StatusValid, } - if err := h.db.CreateAccount(ctx, acc); err != nil { + if err := db.CreateAccount(ctx, acc); err != nil { render.Error(w, acme.WrapErrorISE(err, "error creating account")) return } @@ -136,7 +137,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { render.Error(w, err) return } - if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil { + if err := db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key")) return } @@ -147,14 +148,15 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { httpStatus = http.StatusOK } - h.linker.LinkAccount(ctx, acc) + o := optionsFromContext(ctx) + o.linker.LinkAccount(ctx, acc) - w.Header().Set("Location", h.linker.GetLink(r.Context(), AccountLinkType, acc.ID)) + w.Header().Set("Location", o.linker.GetLink(r.Context(), AccountLinkType, acc.ID)) render.JSONStatus(w, acc, httpStatus) } // GetOrUpdateAccount is the api for updating an ACME account. -func (h *Handler) GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) { +func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := accountFromContext(ctx) if err != nil { @@ -187,16 +189,18 @@ func (h *Handler) GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) { acc.Contact = uar.Contact } - if err := h.db.UpdateAccount(ctx, acc); err != nil { + db := acme.MustFromContext(ctx) + if err := db.UpdateAccount(ctx, acc); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating account")) return } } } - h.linker.LinkAccount(ctx, acc) + o := optionsFromContext(ctx) + o.linker.LinkAccount(ctx, acc) - w.Header().Set("Location", h.linker.GetLink(ctx, AccountLinkType, acc.ID)) + w.Header().Set("Location", o.linker.GetLink(ctx, AccountLinkType, acc.ID)) render.JSON(w, acc) } @@ -210,7 +214,7 @@ func logOrdersByAccount(w http.ResponseWriter, oids []string) { } // GetOrdersByAccountID ACME api for retrieving the list of order urls belonging to an account. -func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { +func GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := accountFromContext(ctx) if err != nil { @@ -222,13 +226,16 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account ID '%s' does not match url param '%s'", acc.ID, accID)) return } - orders, err := h.db.GetOrdersByAccountID(ctx, acc.ID) + + db := acme.MustFromContext(ctx) + orders, err := db.GetOrdersByAccountID(ctx, acc.ID) if err != nil { render.Error(w, err) return } - h.linker.LinkOrdersByAccountID(ctx, orders) + o := optionsFromContext(ctx) + o.linker.LinkOrdersByAccountID(ctx, orders) render.JSON(w, orders) logOrdersByAccount(w, orders) diff --git a/acme/api/eab.go b/acme/api/eab.go index 3660d066..2c94a4ed 100644 --- a/acme/api/eab.go +++ b/acme/api/eab.go @@ -16,7 +16,7 @@ type ExternalAccountBinding struct { } // validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account. -func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) { +func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) { acmeProv, err := acmeProvisionerFromContext(ctx) if err != nil { return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context") @@ -47,7 +47,8 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acmeErr } - externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.ID, keyID) + db := acme.MustFromContext(ctx) + externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID) if err != nil { if _, ok := err.(*acme.Error); ok { return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") diff --git a/acme/api/handler.go b/acme/api/handler.go index 10eb22cb..04680656 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -16,6 +16,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" ) @@ -39,38 +40,89 @@ type payloadInfo struct { isEmptyJSON bool } -// Handler is the ACME API request handler. -type Handler struct { - db acme.DB - backdate provisioner.Duration - ca acme.CertificateAuthority - linker Linker - validateChallengeOptions *acme.ValidateChallengeOptions - prerequisitesChecker func(ctx context.Context) (bool, error) -} - // HandlerOptions required to create a new ACME API request handler. type HandlerOptions struct { - Backdate provisioner.Duration // DB storage backend that impements the acme.DB interface. + // + // Deprecated: use acme.NewContex(context.Context, acme.DB) DB acme.DB + + // CA is the certificate authority interface. + // + // Deprecated: use authority.NewContext(context.Context, *authority.Authority) + CA acme.CertificateAuthority + + // Backdate is the duration that the CA will substract from the current time + // to set the NotBefore in the certificate. + Backdate provisioner.Duration + // DNS the host used to generate accurate ACME links. By default the authority // will use the Host from the request, so this value will only be used if // request.Host is empty. DNS string + // Prefix is a URL path prefix under which the ACME api is served. This // prefix is required to generate accurate ACME links. // E.g. https://ca.smallstep.com/acme/my-acme-provisioner/new-account -- // "acme" is the prefix from which the ACME api is accessed. Prefix string - CA acme.CertificateAuthority + // PrerequisitesChecker checks if all prerequisites for serving ACME are // met by the CA configuration. PrerequisitesChecker func(ctx context.Context) (bool, error) + + linker Linker + validateChallengeOptions *acme.ValidateChallengeOptions +} + +type optionsKey struct{} + +func newOptionsContext(ctx context.Context, o *HandlerOptions) context.Context { + return context.WithValue(ctx, optionsKey{}, o) +} + +func optionsFromContext(ctx context.Context) *HandlerOptions { + o, ok := ctx.Value(optionsKey{}).(*HandlerOptions) + if !ok { + panic("handler options are not in the context") + } + return o +} + +var mustAuthority = func(ctx context.Context) acme.CertificateAuthority { + return authority.MustFromContext(ctx) +} + +// Handler is the ACME API request handler. +type Handler struct { + opts *HandlerOptions +} + +// Route traffic and implement the Router interface. +// +// Deprecated: Use api.Route(r Router, opts *HandlerOptions) +func (h *Handler) Route(r api.Router) { + Route(r, h.opts) } // NewHandler returns a new ACME API handler. +// +// Deprecated: Use api.Route(r Router, opts *HandlerOptions) func NewHandler(ops HandlerOptions) api.RouterHandler { + return &Handler{ + opts: &ops, + } +} + +// Route traffic and implement the Router interface. +func Route(r api.Router, opts *HandlerOptions) { + // by default all prerequisites are met + if opts.PrerequisitesChecker == nil { + opts.PrerequisitesChecker = func(ctx context.Context) (bool, error) { + return true, nil + } + } + transport := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, @@ -83,67 +135,85 @@ func NewHandler(ops HandlerOptions) api.RouterHandler { dialer := &net.Dialer{ Timeout: 30 * time.Second, } - prerequisitesChecker := func(ctx context.Context) (bool, error) { - // by default all prerequisites are met - return true, nil - } - if ops.PrerequisitesChecker != nil { - prerequisitesChecker = ops.PrerequisitesChecker - } - return &Handler{ - ca: ops.CA, - db: ops.DB, - backdate: ops.Backdate, - linker: NewLinker(ops.DNS, ops.Prefix), - validateChallengeOptions: &acme.ValidateChallengeOptions{ - HTTPGet: client.Get, - LookupTxt: net.LookupTXT, - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { - return tls.DialWithDialer(dialer, network, addr, config) - }, + + opts.linker = NewLinker(opts.DNS, opts.Prefix) + opts.validateChallengeOptions = &acme.ValidateChallengeOptions{ + HTTPGet: client.Get, + LookupTxt: net.LookupTXT, + TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + return tls.DialWithDialer(dialer, network, addr, config) }, - prerequisitesChecker: prerequisitesChecker, } -} -// Route traffic and implement the Router interface. -func (h *Handler) Route(r api.Router) { - getPath := h.linker.GetUnescapedPathSuffix - // Standard ACME API - r.MethodFunc("GET", getPath(NewNonceLinkType, "{provisionerID}"), h.baseURLFromRequest(h.lookupProvisioner(h.checkPrerequisites(h.addNonce(h.addDirLink(h.GetNonce)))))) - r.MethodFunc("HEAD", getPath(NewNonceLinkType, "{provisionerID}"), h.baseURLFromRequest(h.lookupProvisioner(h.checkPrerequisites(h.addNonce(h.addDirLink(h.GetNonce)))))) - r.MethodFunc("GET", getPath(DirectoryLinkType, "{provisionerID}"), h.baseURLFromRequest(h.lookupProvisioner(h.checkPrerequisites(h.GetDirectory)))) - r.MethodFunc("HEAD", getPath(DirectoryLinkType, "{provisionerID}"), h.baseURLFromRequest(h.lookupProvisioner(h.checkPrerequisites(h.GetDirectory)))) + withOptions := func(next nextHTTP) nextHTTP { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // For backward compatibility with NewHandler. + if ca, ok := opts.CA.(*authority.Authority); ok && ca != nil { + ctx = authority.NewContext(ctx, ca) + } + if opts.DB != nil { + ctx = acme.NewContext(ctx, opts.DB) + } + + ctx = newOptionsContext(ctx, opts) + next(w, r.WithContext(ctx)) + } + } validatingMiddleware := func(next nextHTTP) nextHTTP { - return h.baseURLFromRequest(h.lookupProvisioner(h.checkPrerequisites(h.addNonce(h.addDirLink(h.verifyContentType(h.parseJWS(h.validateJWS(next)))))))) + return withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next))))))))) } extractPayloadByJWK := func(next nextHTTP) nextHTTP { - return validatingMiddleware(h.extractJWK(h.verifyAndExtractJWSPayload(next))) + return withOptions(validatingMiddleware(extractJWK(verifyAndExtractJWSPayload(next)))) } extractPayloadByKid := func(next nextHTTP) nextHTTP { - return validatingMiddleware(h.lookupJWK(h.verifyAndExtractJWSPayload(next))) + return withOptions(validatingMiddleware(lookupJWK(verifyAndExtractJWSPayload(next)))) } extractPayloadByKidOrJWK := func(next nextHTTP) nextHTTP { - return validatingMiddleware(h.extractOrLookupJWK(h.verifyAndExtractJWSPayload(next))) + return withOptions(validatingMiddleware(extractOrLookupJWK(verifyAndExtractJWSPayload(next)))) } - r.MethodFunc("POST", getPath(NewAccountLinkType, "{provisionerID}"), extractPayloadByJWK(h.NewAccount)) - r.MethodFunc("POST", getPath(AccountLinkType, "{provisionerID}", "{accID}"), extractPayloadByKid(h.GetOrUpdateAccount)) - r.MethodFunc("POST", getPath(KeyChangeLinkType, "{provisionerID}", "{accID}"), extractPayloadByKid(h.NotImplemented)) - r.MethodFunc("POST", getPath(NewOrderLinkType, "{provisionerID}"), extractPayloadByKid(h.NewOrder)) - r.MethodFunc("POST", getPath(OrderLinkType, "{provisionerID}", "{ordID}"), extractPayloadByKid(h.isPostAsGet(h.GetOrder))) - r.MethodFunc("POST", getPath(OrdersByAccountLinkType, "{provisionerID}", "{accID}"), extractPayloadByKid(h.isPostAsGet(h.GetOrdersByAccountID))) - r.MethodFunc("POST", getPath(FinalizeLinkType, "{provisionerID}", "{ordID}"), extractPayloadByKid(h.FinalizeOrder)) - r.MethodFunc("POST", getPath(AuthzLinkType, "{provisionerID}", "{authzID}"), extractPayloadByKid(h.isPostAsGet(h.GetAuthorization))) - r.MethodFunc("POST", getPath(ChallengeLinkType, "{provisionerID}", "{authzID}", "{chID}"), extractPayloadByKid(h.GetChallenge)) - r.MethodFunc("POST", getPath(CertificateLinkType, "{provisionerID}", "{certID}"), extractPayloadByKid(h.isPostAsGet(h.GetCertificate))) - r.MethodFunc("POST", getPath(RevokeCertLinkType, "{provisionerID}"), extractPayloadByKidOrJWK(h.RevokeCert)) + getPath := opts.linker.GetUnescapedPathSuffix + + // Standard ACME API + r.MethodFunc("GET", getPath(NewNonceLinkType, "{provisionerID}"), + withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(addNonce(addDirLink(GetNonce))))))) + r.MethodFunc("HEAD", getPath(NewNonceLinkType, "{provisionerID}"), + withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(addNonce(addDirLink(GetNonce))))))) + r.MethodFunc("GET", getPath(DirectoryLinkType, "{provisionerID}"), + withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(GetDirectory))))) + r.MethodFunc("HEAD", getPath(DirectoryLinkType, "{provisionerID}"), + withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(GetDirectory))))) + + r.MethodFunc("POST", getPath(NewAccountLinkType, "{provisionerID}"), + extractPayloadByJWK(NewAccount)) + r.MethodFunc("POST", getPath(AccountLinkType, "{provisionerID}", "{accID}"), + extractPayloadByKid(GetOrUpdateAccount)) + r.MethodFunc("POST", getPath(KeyChangeLinkType, "{provisionerID}", "{accID}"), + extractPayloadByKid(NotImplemented)) + r.MethodFunc("POST", getPath(NewOrderLinkType, "{provisionerID}"), + extractPayloadByKid(NewOrder)) + r.MethodFunc("POST", getPath(OrderLinkType, "{provisionerID}", "{ordID}"), + extractPayloadByKid(isPostAsGet(GetOrder))) + r.MethodFunc("POST", getPath(OrdersByAccountLinkType, "{provisionerID}", "{accID}"), + extractPayloadByKid(isPostAsGet(GetOrdersByAccountID))) + r.MethodFunc("POST", getPath(FinalizeLinkType, "{provisionerID}", "{ordID}"), + extractPayloadByKid(FinalizeOrder)) + r.MethodFunc("POST", getPath(AuthzLinkType, "{provisionerID}", "{authzID}"), + extractPayloadByKid(isPostAsGet(GetAuthorization))) + r.MethodFunc("POST", getPath(ChallengeLinkType, "{provisionerID}", "{authzID}", "{chID}"), + extractPayloadByKid(GetChallenge)) + r.MethodFunc("POST", getPath(CertificateLinkType, "{provisionerID}", "{certID}"), + extractPayloadByKid(isPostAsGet(GetCertificate))) + r.MethodFunc("POST", getPath(RevokeCertLinkType, "{provisionerID}"), + extractPayloadByKidOrJWK(RevokeCert)) } // GetNonce just sets the right header since a Nonce is added to each response // by middleware by default. -func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) { +func GetNonce(w http.ResponseWriter, r *http.Request) { if r.Method == "HEAD" { w.WriteHeader(http.StatusOK) } else { @@ -179,8 +249,10 @@ func (d *Directory) ToLog() (interface{}, error) { // GetDirectory is the ACME resource for returning a directory configuration // for client configuration. -func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { +func GetDirectory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + o := optionsFromContext(ctx) + acmeProv, err := acmeProvisionerFromContext(ctx) if err != nil { render.Error(w, err) @@ -188,11 +260,11 @@ func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { } render.JSON(w, &Directory{ - NewNonce: h.linker.GetLink(ctx, NewNonceLinkType), - NewAccount: h.linker.GetLink(ctx, NewAccountLinkType), - NewOrder: h.linker.GetLink(ctx, NewOrderLinkType), - RevokeCert: h.linker.GetLink(ctx, RevokeCertLinkType), - KeyChange: h.linker.GetLink(ctx, KeyChangeLinkType), + NewNonce: o.linker.GetLink(ctx, NewNonceLinkType), + NewAccount: o.linker.GetLink(ctx, NewAccountLinkType), + NewOrder: o.linker.GetLink(ctx, NewOrderLinkType), + RevokeCert: o.linker.GetLink(ctx, RevokeCertLinkType), + KeyChange: o.linker.GetLink(ctx, KeyChangeLinkType), Meta: Meta{ ExternalAccountRequired: acmeProv.RequireEAB, }, @@ -201,19 +273,22 @@ func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { // NotImplemented returns a 501 and is generally a placeholder for functionality which // MAY be added at some point in the future but is not in any way a guarantee of such. -func (h *Handler) NotImplemented(w http.ResponseWriter, r *http.Request) { +func NotImplemented(w http.ResponseWriter, r *http.Request) { render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented")) } // GetAuthorization ACME api for retrieving an Authz. -func (h *Handler) GetAuthorization(w http.ResponseWriter, r *http.Request) { +func GetAuthorization(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + o := optionsFromContext(ctx) + db := acme.MustFromContext(ctx) + acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } - az, err := h.db.GetAuthorization(ctx, chi.URLParam(r, "authzID")) + az, err := db.GetAuthorization(ctx, chi.URLParam(r, "authzID")) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving authorization")) return @@ -223,20 +298,23 @@ func (h *Handler) GetAuthorization(w http.ResponseWriter, r *http.Request) { "account '%s' does not own authorization '%s'", acc.ID, az.ID)) return } - if err = az.UpdateStatus(ctx, h.db); err != nil { + if err = az.UpdateStatus(ctx, db); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating authorization status")) return } - h.linker.LinkAuthorization(ctx, az) + o.linker.LinkAuthorization(ctx, az) - w.Header().Set("Location", h.linker.GetLink(ctx, AuthzLinkType, az.ID)) + w.Header().Set("Location", o.linker.GetLink(ctx, AuthzLinkType, az.ID)) render.JSON(w, az) } // GetChallenge ACME api for retrieving a Challenge. -func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) { +func GetChallenge(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + o := optionsFromContext(ctx) + db := acme.MustFromContext(ctx) + acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) @@ -257,7 +335,7 @@ func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) { // we'll just ignore the body. azID := chi.URLParam(r, "authzID") - ch, err := h.db.GetChallenge(ctx, chi.URLParam(r, "chID"), azID) + ch, err := db.GetChallenge(ctx, chi.URLParam(r, "chID"), azID) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving challenge")) return @@ -273,29 +351,31 @@ func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) { render.Error(w, err) return } - if err = ch.Validate(ctx, h.db, jwk, h.validateChallengeOptions); err != nil { + if err = ch.Validate(ctx, db, jwk, o.validateChallengeOptions); err != nil { render.Error(w, acme.WrapErrorISE(err, "error validating challenge")) return } - h.linker.LinkChallenge(ctx, ch, azID) + o.linker.LinkChallenge(ctx, ch, azID) - w.Header().Add("Link", link(h.linker.GetLink(ctx, AuthzLinkType, azID), "up")) - w.Header().Set("Location", h.linker.GetLink(ctx, ChallengeLinkType, azID, ch.ID)) + w.Header().Add("Link", link(o.linker.GetLink(ctx, AuthzLinkType, azID), "up")) + w.Header().Set("Location", o.linker.GetLink(ctx, ChallengeLinkType, azID, ch.ID)) render.JSON(w, ch) } // GetCertificate ACME api for retrieving a Certificate. -func (h *Handler) GetCertificate(w http.ResponseWriter, r *http.Request) { +func GetCertificate(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + db := acme.MustFromContext(ctx) + acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } - certID := chi.URLParam(r, "certID") - cert, err := h.db.GetCertificate(ctx, certID) + certID := chi.URLParam(r, "certID") + cert, err := db.GetCertificate(ctx, certID) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving certificate")) return diff --git a/acme/api/middleware.go b/acme/api/middleware.go index 10f7841f..564a16f5 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -31,15 +31,15 @@ func logNonce(w http.ResponseWriter, nonce string) { } } -// baseURLFromRequest determines the base URL which should be used for +// getBaseURLFromRequest determines the base URL which should be used for // constructing link URLs in e.g. the ACME directory result by taking the // request Host into consideration. // // If the Request.Host is an empty string, we return an empty string, to // indicate that the configured URL values should be used instead. If this -// function returns a non-empty result, then this should be used in -// constructing ACME link URLs. -func baseURLFromRequest(r *http.Request) *url.URL { +// function returns a non-empty result, then this should be used in constructing +// ACME link URLs. +func getBaseURLFromRequest(r *http.Request) *url.URL { // NOTE: See https://github.com/letsencrypt/boulder/blob/master/web/relative.go // for an implementation that allows HTTP requests using the x-forwarded-proto // header. @@ -53,17 +53,18 @@ func baseURLFromRequest(r *http.Request) *url.URL { // baseURLFromRequest is a middleware that extracts and caches the baseURL // from the request. // E.g. https://ca.smallstep.com/ -func (h *Handler) baseURLFromRequest(next nextHTTP) nextHTTP { +func baseURLFromRequest(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), baseURLContextKey, baseURLFromRequest(r)) + ctx := context.WithValue(r.Context(), baseURLContextKey, getBaseURLFromRequest(r)) next(w, r.WithContext(ctx)) } } // addNonce is a middleware that adds a nonce to the response header. -func (h *Handler) addNonce(next nextHTTP) nextHTTP { +func addNonce(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - nonce, err := h.db.CreateNonce(r.Context()) + db := acme.MustFromContext(r.Context()) + nonce, err := db.CreateNonce(r.Context()) if err != nil { render.Error(w, err) return @@ -77,25 +78,31 @@ func (h *Handler) addNonce(next nextHTTP) nextHTTP { // addDirLink is a middleware that adds a 'Link' response reader with the // directory index url. -func (h *Handler) addDirLink(next nextHTTP) nextHTTP { +func addDirLink(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Link", link(h.linker.GetLink(r.Context(), DirectoryLinkType), "index")) + ctx := r.Context() + opts := optionsFromContext(ctx) + + w.Header().Add("Link", link(opts.linker.GetLink(ctx, DirectoryLinkType), "index")) next(w, r) } } // verifyContentType is a middleware that verifies that content type is // application/jose+json. -func (h *Handler) verifyContentType(next nextHTTP) nextHTTP { +func verifyContentType(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { var expected []string - p, err := provisionerFromContext(r.Context()) + ctx := r.Context() + opts := optionsFromContext(ctx) + + p, err := provisionerFromContext(ctx) if err != nil { render.Error(w, err) return } - u := url.URL{Path: h.linker.GetUnescapedPathSuffix(CertificateLinkType, p.GetName(), "")} + u := url.URL{Path: opts.linker.GetUnescapedPathSuffix(CertificateLinkType, p.GetName(), "")} if strings.Contains(r.URL.String(), u.EscapedPath()) { // GET /certificate requests allow a greater range of content types. expected = []string{"application/jose+json", "application/pkix-cert", "application/pkcs7-mime"} @@ -117,7 +124,7 @@ func (h *Handler) verifyContentType(next nextHTTP) nextHTTP { } // parseJWS is a middleware that parses a request body into a JSONWebSignature struct. -func (h *Handler) parseJWS(next nextHTTP) nextHTTP { +func parseJWS(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { @@ -149,10 +156,12 @@ func (h *Handler) parseJWS(next nextHTTP) nextHTTP { // * “nonce” (defined in Section 6.5) // * “url” (defined in Section 6.4) // * Either “jwk” (JSON Web Key) or “kid” (Key ID) as specified below -func (h *Handler) validateJWS(next nextHTTP) nextHTTP { +func validateJWS(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - jws, err := jwsFromContext(r.Context()) + db := acme.MustFromContext(ctx) + + jws, err := jwsFromContext(ctx) if err != nil { render.Error(w, err) return @@ -202,7 +211,7 @@ func (h *Handler) validateJWS(next nextHTTP) nextHTTP { } // Check the validity/freshness of the Nonce. - if err := h.db.DeleteNonce(ctx, acme.Nonce(hdr.Nonce)); err != nil { + if err := db.DeleteNonce(ctx, acme.Nonce(hdr.Nonce)); err != nil { render.Error(w, err) return } @@ -235,10 +244,12 @@ func (h *Handler) validateJWS(next nextHTTP) nextHTTP { // extractJWK is a middleware that extracts the JWK from the JWS and saves it // in the context. Make sure to parse and validate the JWS before running this // middleware. -func (h *Handler) extractJWK(next nextHTTP) nextHTTP { +func extractJWK(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - jws, err := jwsFromContext(r.Context()) + db := acme.MustFromContext(ctx) + + jws, err := jwsFromContext(ctx) if err != nil { render.Error(w, err) return @@ -264,7 +275,7 @@ func (h *Handler) extractJWK(next nextHTTP) nextHTTP { ctx = context.WithValue(ctx, jwkContextKey, jwk) // Get Account OR continue to generate a new one OR continue Revoke with certificate private key - acc, err := h.db.GetAccountByKeyID(ctx, jwk.KeyID) + acc, err := db.GetAccountByKeyID(ctx, jwk.KeyID) switch { case errors.Is(err, acme.ErrNotFound): // For NewAccount and Revoke requests ... @@ -285,7 +296,7 @@ func (h *Handler) extractJWK(next nextHTTP) nextHTTP { // lookupProvisioner loads the provisioner associated with the request. // Responds 404 if the provisioner does not exist. -func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { +func lookupProvisioner(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() nameEscaped := chi.URLParam(r, "provisionerID") @@ -294,7 +305,7 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { render.Error(w, acme.WrapErrorISE(err, "error url unescaping provisioner name '%s'", nameEscaped)) return } - p, err := h.ca.LoadProvisionerByName(name) + p, err := mustAuthority(r.Context()).LoadProvisionerByName(name) if err != nil { render.Error(w, err) return @@ -311,10 +322,12 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { // checkPrerequisites checks if all prerequisites for serving ACME // are met by the CA configuration. -func (h *Handler) checkPrerequisites(next nextHTTP) nextHTTP { +func checkPrerequisites(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - ok, err := h.prerequisitesChecker(ctx) + opts := optionsFromContext(ctx) + + ok, err := opts.PrerequisitesChecker(ctx) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error checking acme provisioner prerequisites")) return @@ -330,16 +343,19 @@ func (h *Handler) checkPrerequisites(next nextHTTP) nextHTTP { // lookupJWK loads the JWK associated with the acme account referenced by the // kid parameter of the signed payload. // Make sure to parse and validate the JWS before running this middleware. -func (h *Handler) lookupJWK(next nextHTTP) nextHTTP { +func lookupJWK(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + opts := optionsFromContext(ctx) + db := acme.MustFromContext(ctx) + jws, err := jwsFromContext(ctx) if err != nil { render.Error(w, err) return } - kidPrefix := h.linker.GetLink(ctx, AccountLinkType, "") + kidPrefix := opts.linker.GetLink(ctx, AccountLinkType, "") kid := jws.Signatures[0].Protected.KeyID if !strings.HasPrefix(kid, kidPrefix) { render.Error(w, acme.NewError(acme.ErrorMalformedType, @@ -349,7 +365,7 @@ func (h *Handler) lookupJWK(next nextHTTP) nextHTTP { } accID := strings.TrimPrefix(kid, kidPrefix) - acc, err := h.db.GetAccount(ctx, accID) + acc, err := db.GetAccount(ctx, accID) switch { case nosql.IsErrNotFound(err): render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType, "account with ID '%s' not found", accID)) @@ -372,7 +388,7 @@ func (h *Handler) lookupJWK(next nextHTTP) nextHTTP { // extractOrLookupJWK forwards handling to either extractJWK or // lookupJWK based on the presence of a JWK or a KID, respectively. -func (h *Handler) extractOrLookupJWK(next nextHTTP) nextHTTP { +func extractOrLookupJWK(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() jws, err := jwsFromContext(ctx) @@ -385,13 +401,13 @@ func (h *Handler) extractOrLookupJWK(next nextHTTP) nextHTTP { // and it can be used to check if a JWK exists. This flow is used when the ACME client // signed the payload with a certificate private key. if canExtractJWKFrom(jws) { - h.extractJWK(next)(w, r) + extractJWK(next)(w, r) return } // default to looking up the JWK based on KeyID. This flow is used when the ACME client // signed the payload with an account private key. - h.lookupJWK(next)(w, r) + lookupJWK(next)(w, r) } } @@ -408,7 +424,7 @@ func canExtractJWKFrom(jws *jose.JSONWebSignature) bool { // verifyAndExtractJWSPayload extracts the JWK from the JWS and saves it in the context. // Make sure to parse and validate the JWS before running this middleware. -func (h *Handler) verifyAndExtractJWSPayload(next nextHTTP) nextHTTP { +func verifyAndExtractJWSPayload(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() jws, err := jwsFromContext(ctx) @@ -440,7 +456,7 @@ func (h *Handler) verifyAndExtractJWSPayload(next nextHTTP) nextHTTP { } // isPostAsGet asserts that the request is a PostAsGet (empty JWS payload). -func (h *Handler) isPostAsGet(next nextHTTP) nextHTTP { +func isPostAsGet(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { payload, err := payloadFromContext(r.Context()) if err != nil { diff --git a/acme/api/order.go b/acme/api/order.go index 99eb0e95..ebd0c7f5 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -68,7 +68,7 @@ var defaultOrderExpiry = time.Hour * 24 var defaultOrderBackdate = time.Minute // NewOrder ACME api for creating a new order. -func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { +func NewOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := accountFromContext(ctx) if err != nil { @@ -117,7 +117,7 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { ExpiresAt: o.ExpiresAt, Status: acme.StatusPending, } - if err := h.newAuthorization(ctx, az); err != nil { + if err := newAuthorization(ctx, az); err != nil { render.Error(w, err) return } @@ -136,18 +136,20 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { o.NotBefore = o.NotBefore.Add(-defaultOrderBackdate) } - if err := h.db.CreateOrder(ctx, o); err != nil { + db := acme.MustFromContext(ctx) + if err := db.CreateOrder(ctx, o); err != nil { render.Error(w, acme.WrapErrorISE(err, "error creating order")) return } - h.linker.LinkOrder(ctx, o) + opts := optionsFromContext(ctx) + opts.linker.LinkOrder(ctx, o) - w.Header().Set("Location", h.linker.GetLink(ctx, OrderLinkType, o.ID)) + w.Header().Set("Location", opts.linker.GetLink(ctx, OrderLinkType, o.ID)) render.JSONStatus(w, o, http.StatusCreated) } -func (h *Handler) newAuthorization(ctx context.Context, az *acme.Authorization) error { +func newAuthorization(ctx context.Context, az *acme.Authorization) error { if strings.HasPrefix(az.Identifier.Value, "*.") { az.Wildcard = true az.Identifier = acme.Identifier{ @@ -163,6 +165,8 @@ func (h *Handler) newAuthorization(ctx context.Context, az *acme.Authorization) if err != nil { return acme.WrapErrorISE(err, "error generating random alphanumeric ID") } + + db := acme.MustFromContext(ctx) az.Challenges = make([]*acme.Challenge, len(chTypes)) for i, typ := range chTypes { ch := &acme.Challenge{ @@ -172,19 +176,19 @@ func (h *Handler) newAuthorization(ctx context.Context, az *acme.Authorization) Token: az.Token, Status: acme.StatusPending, } - if err := h.db.CreateChallenge(ctx, ch); err != nil { + if err := db.CreateChallenge(ctx, ch); err != nil { return acme.WrapErrorISE(err, "error creating challenge") } az.Challenges[i] = ch } - if err = h.db.CreateAuthorization(ctx, az); err != nil { + if err = db.CreateAuthorization(ctx, az); err != nil { return acme.WrapErrorISE(err, "error creating authorization") } return nil } // GetOrder ACME api for retrieving an order. -func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) { +func GetOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := accountFromContext(ctx) if err != nil { @@ -196,7 +200,9 @@ func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) { render.Error(w, err) return } - o, err := h.db.GetOrder(ctx, chi.URLParam(r, "ordID")) + + db := acme.MustFromContext(ctx) + o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID")) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving order")) return @@ -211,19 +217,20 @@ func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) { "provisioner '%s' does not own order '%s'", prov.GetID(), o.ID)) return } - if err = o.UpdateStatus(ctx, h.db); err != nil { + if err = o.UpdateStatus(ctx, db); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating order status")) return } - h.linker.LinkOrder(ctx, o) + opts := optionsFromContext(ctx) + opts.linker.LinkOrder(ctx, o) - w.Header().Set("Location", h.linker.GetLink(ctx, OrderLinkType, o.ID)) + w.Header().Set("Location", opts.linker.GetLink(ctx, OrderLinkType, o.ID)) render.JSON(w, o) } // FinalizeOrder attemptst to finalize an order and create a certificate. -func (h *Handler) FinalizeOrder(w http.ResponseWriter, r *http.Request) { +func FinalizeOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := accountFromContext(ctx) if err != nil { @@ -251,7 +258,8 @@ func (h *Handler) FinalizeOrder(w http.ResponseWriter, r *http.Request) { return } - o, err := h.db.GetOrder(ctx, chi.URLParam(r, "ordID")) + db := acme.MustFromContext(ctx) + o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID")) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving order")) return @@ -266,14 +274,17 @@ func (h *Handler) FinalizeOrder(w http.ResponseWriter, r *http.Request) { "provisioner '%s' does not own order '%s'", prov.GetID(), o.ID)) return } - if err = o.Finalize(ctx, h.db, fr.csr, h.ca, prov); err != nil { + + ca := mustAuthority(ctx) + if err = o.Finalize(ctx, db, fr.csr, ca, prov); err != nil { render.Error(w, acme.WrapErrorISE(err, "error finalizing order")) return } - h.linker.LinkOrder(ctx, o) + opts := optionsFromContext(ctx) + opts.linker.LinkOrder(ctx, o) - w.Header().Set("Location", h.linker.GetLink(ctx, OrderLinkType, o.ID)) + w.Header().Set("Location", opts.linker.GetLink(ctx, OrderLinkType, o.ID)) render.JSON(w, o) } diff --git a/acme/api/revoke.go b/acme/api/revoke.go index 4b71bc22..55774aea 100644 --- a/acme/api/revoke.go +++ b/acme/api/revoke.go @@ -26,8 +26,7 @@ type revokePayload struct { } // RevokeCert attempts to revoke a certificate. -func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { - +func RevokeCert(w http.ResponseWriter, r *http.Request) { ctx := r.Context() jws, err := jwsFromContext(ctx) if err != nil { @@ -68,8 +67,9 @@ func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { return } + db := acme.MustFromContext(ctx) serial := certToBeRevoked.SerialNumber.String() - dbCert, err := h.db.GetCertificateBySerial(ctx, serial) + dbCert, err := db.GetCertificateBySerial(ctx, serial) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving certificate by serial")) return @@ -87,7 +87,7 @@ func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { render.Error(w, err) return } - acmeErr := h.isAccountAuthorized(ctx, dbCert, certToBeRevoked, account) + acmeErr := isAccountAuthorized(ctx, dbCert, certToBeRevoked, account) if acmeErr != nil { render.Error(w, acmeErr) return @@ -103,7 +103,8 @@ func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { } } - hasBeenRevokedBefore, err := h.ca.IsRevoked(serial) + ca := mustAuthority(ctx) + hasBeenRevokedBefore, err := ca.IsRevoked(serial) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving revocation status of certificate")) return @@ -130,14 +131,15 @@ func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { } options := revokeOptions(serial, certToBeRevoked, reasonCode) - err = h.ca.Revoke(ctx, options) + err = ca.Revoke(ctx, options) if err != nil { render.Error(w, wrapRevokeErr(err)) return } logRevoke(w, options) - w.Header().Add("Link", link(h.linker.GetLink(ctx, DirectoryLinkType), "index")) + o := optionsFromContext(ctx) + w.Header().Add("Link", link(o.linker.GetLink(ctx, DirectoryLinkType), "index")) w.Write(nil) } @@ -148,7 +150,7 @@ func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { // the identifiers in the certificate are extracted and compared against the (valid) Authorizations // that are stored for the ACME Account. If these sets match, the Account is considered authorized // to revoke the certificate. If this check fails, the client will receive an unauthorized error. -func (h *Handler) isAccountAuthorized(ctx context.Context, dbCert *acme.Certificate, certToBeRevoked *x509.Certificate, account *acme.Account) *acme.Error { +func isAccountAuthorized(ctx context.Context, dbCert *acme.Certificate, certToBeRevoked *x509.Certificate, account *acme.Account) *acme.Error { if !account.IsValid() { return wrapUnauthorizedError(certToBeRevoked, nil, fmt.Sprintf("account '%s' has status '%s'", account.ID, account.Status), nil) } From 216d8f0efbb95336c948faec338db1c5cd56e97c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 15:44:41 -0700 Subject: [PATCH 163/241] Handle acme requests with the new api --- ca/ca.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ca/ca.go b/ca/ca.go index 783255ce..933db275 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -200,20 +200,18 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { return nil, errors.Wrap(err, "error configuring ACME DB interface") } } - acmeHandler := acmeAPI.NewHandler(acmeAPI.HandlerOptions{ + acmeOptions := &acmeAPI.HandlerOptions{ Backdate: *cfg.AuthorityConfig.Backdate, - DB: acmeDB, DNS: dns, Prefix: prefix, - CA: auth, - }) + } mux.Route("/"+prefix, func(r chi.Router) { - acmeHandler.Route(r) + acmeAPI.Route(r, acmeOptions) }) // Use 2.0 because, at the moment, our ACME api is only compatible with v2.0 // of the ACME spec. mux.Route("/2.0/"+prefix, func(r chi.Router) { - acmeHandler.Route(r) + acmeAPI.Route(r, acmeOptions) }) // Admin API Router From 688f9ceb5648805502d21db6f285c9453395b767 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 18:02:37 -0700 Subject: [PATCH 164/241] Add scep authority to context. --- ca/ca.go | 10 +++++++--- scep/authority.go | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/ca/ca.go b/ca/ca.go index 933db275..a8ecbb05 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -225,9 +225,10 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } } + var scepAuthority *scep.Authority if ca.shouldServeSCEPEndpoints() { scepPrefix := "scep" - scepAuthority, err := scep.New(auth, scep.AuthorityOptions{ + scepAuthority, err = scep.New(auth, scep.AuthorityOptions{ Service: auth.GetSCEPService(), DNS: dns, Prefix: scepPrefix, @@ -279,7 +280,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } // Create context with all the necessary values. - baseContext := buildContext(auth, acmeDB) + baseContext := buildContext(auth, scepAuthority, acmeDB) ca.srv = server.New(cfg.Address, handler, tlsConfig) ca.srv.BaseContext = func(net.Listener) context.Context { @@ -303,7 +304,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } // buildContext builds the server base context. -func buildContext(a *authority.Authority, acmeDB acme.DB) context.Context { +func buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB acme.DB) context.Context { ctx := authority.NewContext(context.Background(), a) if authDB := a.GetDatabase(); authDB != nil { ctx = db.NewContext(ctx, authDB) @@ -311,6 +312,9 @@ func buildContext(a *authority.Authority, acmeDB acme.DB) context.Context { if adminDB := a.GetAdminDatabase(); adminDB != nil { ctx = admin.NewContext(ctx, adminDB) } + if scepAuthority != nil { + ctx = scep.NewContext(ctx, scepAuthority) + } if acmeDB != nil { ctx = acme.NewContext(ctx, acmeDB) } diff --git a/scep/authority.go b/scep/authority.go index 71f92152..946fa948 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -27,6 +27,29 @@ type Authority struct { signAuth SignAuthority } +type authorityKey struct{} + +// NewContext adds the given authority to the context. +func NewContext(ctx context.Context, a *Authority) context.Context { + return context.WithValue(ctx, authorityKey{}, a) +} + +// FromContext returns the current authority from the given context. +func FromContext(ctx context.Context) (a *Authority, ok bool) { + a, ok = ctx.Value(authorityKey{}).(*Authority) + return +} + +// MustFromContext returns the current authority from the given context. It will +// panic if the authority is not in the context. +func MustFromContext(ctx context.Context) *Authority { + if a, ok := FromContext(ctx); !ok { + panic("scep authority is not in the context") + } else { + return a + } +} + // AuthorityOptions required to create a new SCEP Authority. type AuthorityOptions struct { // Service provides the certificate chain, the signer and the decrypter to the Authority @@ -40,6 +63,20 @@ type AuthorityOptions struct { Prefix string } +type optionsKey struct{} + +func newOptionsContext(ctx context.Context, o *AuthorityOptions) context.Context { + return context.WithValue(ctx, optionsKey{}, o) +} + +func optionsFromContext(ctx context.Context) *AuthorityOptions { + o, ok := ctx.Value(optionsKey{}).(*AuthorityOptions) + if !ok { + panic("scep options are not in the context") + } + return o +} + // SignAuthority is the interface for a signing authority type SignAuthority interface { Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) @@ -163,7 +200,6 @@ func (a *Authority) GetCACertificates(ctx context.Context) ([]*x509.Certificate, // DecryptPKIEnvelope decrypts an enveloped message func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error { - p7c, err := pkcs7.Parse(msg.P7.Content) if err != nil { return fmt.Errorf("error parsing pkcs7 content: %w", err) @@ -210,7 +246,6 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err // SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials // returns a new PKIMessage with CertRep data func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error) { - // TODO: intermediate storage of the request? In SCEP it's possible to request a csr/certificate // to be signed, which can be performed asynchronously / out-of-band. In that case a client can // poll for the status. It seems to be similar as what can happen in ACME, so might want to model From 42435ace642edb1fd7abc0232c8afc4dc66f9287 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 18:06:27 -0700 Subject: [PATCH 165/241] Use scep authority from context This commit also converts all the methods from the handler to functions. --- acme/api/handler.go | 2 +- scep/api/api.go | 82 ++++++++++++++++++++++----------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/acme/api/handler.go b/acme/api/handler.go index 04680656..4b916404 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -84,7 +84,7 @@ func newOptionsContext(ctx context.Context, o *HandlerOptions) context.Context { func optionsFromContext(ctx context.Context) *HandlerOptions { o, ok := ctx.Value(optionsKey{}).(*HandlerOptions) if !ok { - panic("handler options are not in the context") + panic("acme options are not in the context") } return o } diff --git a/scep/api/api.go b/scep/api/api.go index 31f0f10d..0d62904d 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -48,29 +48,32 @@ type response struct { } // handler is the SCEP request handler. -type handler struct { - auth *scep.Authority +type handler struct{} + +// Route traffic and implement the Router interface. +// +// Deprecated: use scep.Route(r api.Router) +func (h *handler) Route(r api.Router) { + Route(r) } // New returns a new SCEP API router. +// +// Deprecated: use scep.Route(r api.Router) func New(auth *scep.Authority) api.RouterHandler { - return &handler{ - auth: auth, - } + return &handler{} } // Route traffic and implement the Router interface. -func (h *handler) Route(r api.Router) { - getLink := h.auth.GetLinkExplicit - r.MethodFunc(http.MethodGet, getLink("{provisionerName}/*", false, nil), h.lookupProvisioner(h.Get)) - r.MethodFunc(http.MethodGet, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Get)) - r.MethodFunc(http.MethodPost, getLink("{provisionerName}/*", false, nil), h.lookupProvisioner(h.Post)) - r.MethodFunc(http.MethodPost, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Post)) +func Route(r api.Router) { + r.MethodFunc(http.MethodGet, "/{provisionerName}/*", lookupProvisioner(Get)) + r.MethodFunc(http.MethodGet, "/{provisionerName}", lookupProvisioner(Get)) + r.MethodFunc(http.MethodPost, "/{provisionerName}/*", lookupProvisioner(Post)) + r.MethodFunc(http.MethodPost, "/{provisionerName}", lookupProvisioner(Post)) } // Get handles all SCEP GET requests -func (h *handler) Get(w http.ResponseWriter, r *http.Request) { - +func Get(w http.ResponseWriter, r *http.Request) { req, err := decodeRequest(r) if err != nil { fail(w, fmt.Errorf("invalid scep get request: %w", err)) @@ -82,9 +85,9 @@ func (h *handler) Get(w http.ResponseWriter, r *http.Request) { switch req.Operation { case opnGetCACert: - res, err = h.GetCACert(ctx) + res, err = GetCACert(ctx) case opnGetCACaps: - res, err = h.GetCACaps(ctx) + res, err = GetCACaps(ctx) case opnPKIOperation: // TODO: implement the GET for PKI operation? Default CACAPS doesn't specify this is in use, though default: @@ -100,20 +103,17 @@ func (h *handler) Get(w http.ResponseWriter, r *http.Request) { } // Post handles all SCEP POST requests -func (h *handler) Post(w http.ResponseWriter, r *http.Request) { - +func Post(w http.ResponseWriter, r *http.Request) { req, err := decodeRequest(r) if err != nil { fail(w, fmt.Errorf("invalid scep post request: %w", err)) return } - ctx := r.Context() var res response - switch req.Operation { case opnPKIOperation: - res, err = h.PKIOperation(ctx, req) + res, err = PKIOperation(r.Context(), req) default: err = fmt.Errorf("unknown operation: %s", req.Operation) } @@ -127,7 +127,6 @@ func (h *handler) Post(w http.ResponseWriter, r *http.Request) { } func decodeRequest(r *http.Request) (request, error) { - defer r.Body.Close() method := r.Method @@ -179,9 +178,8 @@ func decodeRequest(r *http.Request) (request, error) { // lookupProvisioner loads the provisioner associated with the request. // Responds 404 if the provisioner does not exist. -func (h *handler) lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { +func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - name := chi.URLParam(r, "provisionerName") provisionerName, err := url.PathUnescape(name) if err != nil { @@ -189,7 +187,9 @@ func (h *handler) lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { return } - p, err := h.auth.LoadProvisionerByName(provisionerName) + ctx := r.Context() + auth := scep.MustFromContext(ctx) + p, err := auth.LoadProvisionerByName(provisionerName) if err != nil { fail(w, err) return @@ -201,16 +201,15 @@ func (h *handler) lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { return } - ctx := r.Context() ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(prov)) next(w, r.WithContext(ctx)) } } // GetCACert returns the CA certificates in a SCEP response -func (h *handler) GetCACert(ctx context.Context) (response, error) { - - certs, err := h.auth.GetCACertificates(ctx) +func GetCACert(ctx context.Context) (response, error) { + auth := scep.MustFromContext(ctx) + certs, err := auth.GetCACertificates(ctx) if err != nil { return response{}, err } @@ -241,9 +240,9 @@ func (h *handler) GetCACert(ctx context.Context) (response, error) { } // GetCACaps returns the CA capabilities in a SCEP response -func (h *handler) GetCACaps(ctx context.Context) (response, error) { - - caps := h.auth.GetCACaps(ctx) +func GetCACaps(ctx context.Context) (response, error) { + auth := scep.MustFromContext(ctx) + caps := auth.GetCACaps(ctx) res := response{ Operation: opnGetCACaps, @@ -254,8 +253,7 @@ func (h *handler) GetCACaps(ctx context.Context) (response, error) { } // PKIOperation performs PKI operations and returns a SCEP response -func (h *handler) PKIOperation(ctx context.Context, req request) (response, error) { - +func PKIOperation(ctx context.Context, req request) (response, error) { // parse the message using microscep implementation microMsg, err := microscep.ParsePKIMessage(req.Message) if err != nil { @@ -280,7 +278,8 @@ func (h *handler) PKIOperation(ctx context.Context, req request) (response, erro P7: p7, } - if err := h.auth.DecryptPKIEnvelope(ctx, msg); err != nil { + auth := scep.MustFromContext(ctx) + if err := auth.DecryptPKIEnvelope(ctx, msg); err != nil { return response{}, err } @@ -293,13 +292,13 @@ func (h *handler) PKIOperation(ctx context.Context, req request) (response, erro // a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients. // We'll have to see how it works out. if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq { - challengeMatches, err := h.auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword) + challengeMatches, err := auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword) if err != nil { - return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("error when checking password")) + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("error when checking password")) } if !challengeMatches { // TODO: can this be returned safely to the client? In the end, if the password was correct, that gains a bit of info too. - return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("wrong password provided")) + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("wrong password provided")) } } @@ -311,9 +310,9 @@ func (h *handler) PKIOperation(ctx context.Context, req request) (response, erro // Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification // of the client cert is not. - certRep, err := h.auth.SignCSR(ctx, csr, msg) + certRep, err := auth.SignCSR(ctx, csr, msg) if err != nil { - return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err)) + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err)) } res := response{ @@ -350,8 +349,9 @@ func fail(w http.ResponseWriter, err error) { http.Error(w, err.Error(), http.StatusInternalServerError) } -func (h *handler) createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (response, error) { - certRepMsg, err := h.auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error()) +func createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (response, error) { + auth := scep.MustFromContext(ctx) + certRepMsg, err := auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error()) if err != nil { return response{}, err } From bb8d85a20128ce772f9f6709abe8e0af0ae37a85 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Apr 2022 19:08:16 -0700 Subject: [PATCH 166/241] Fix unit tests - work in progress --- acme/api/account_test.go | 12 +++---- acme/api/eab_test.go | 8 ++--- acme/api/handler_test.go | 21 ++++++------ acme/api/middleware_test.go | 64 ++++++++++++++++++------------------- acme/api/order_test.go | 16 +++++----- acme/api/revoke_test.go | 8 ++--- 6 files changed, 65 insertions(+), 64 deletions(-) diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 4c3404ec..3fbabfe5 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -315,11 +315,11 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetOrdersByAccountID(w, req) + GetOrdersByAccountID(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -759,11 +759,11 @@ func TestHandler_NewAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} req := httptest.NewRequest("GET", "/foo/bar", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.NewAccount(w, req) + NewAccount(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -959,11 +959,11 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} req := httptest.NewRequest("GET", "/foo/bar", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetOrUpdateAccount(w, req) + GetOrUpdateAccount(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) diff --git a/acme/api/eab_test.go b/acme/api/eab_test.go index dce9f36d..1c76618b 100644 --- a/acme/api/eab_test.go +++ b/acme/api/eab_test.go @@ -762,10 +762,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - db: tc.db, - } - got, err := h.validateExternalAccountBinding(tc.ctx, tc.nar) + // h := &Handler{ + // db: tc.db, + // } + got, err := validateExternalAccountBinding(tc.ctx, tc.nar) wantErr := tc.err != nil gotErr := err != nil if wantErr != gotErr { diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index 67f7df30..fcc33a87 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -38,10 +38,10 @@ func TestHandler_GetNonce(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := &Handler{} + // h := &Handler{} w := httptest.NewRecorder() req.Method = tt.name - h.GetNonce(w, req) + GetNonce(w, req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -53,6 +53,7 @@ func TestHandler_GetNonce(t *testing.T) { func TestHandler_GetDirectory(t *testing.T) { linker := NewLinker("ca.smallstep.com", "acme") + _ = linker type test struct { ctx context.Context statusCode int @@ -130,11 +131,11 @@ func TestHandler_GetDirectory(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{linker: linker} + // h := &Handler{linker: linker} req := httptest.NewRequest("GET", "/foo/bar", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetDirectory(w, req) + GetDirectory(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -304,11 +305,11 @@ func TestHandler_GetAuthorization(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} req := httptest.NewRequest("GET", "/foo/bar", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetAuthorization(w, req) + GetAuthorization(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -447,11 +448,11 @@ func TestHandler_GetCertificate(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db} + // h := &Handler{db: tc.db} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetCertificate(w, req) + GetCertificate(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -703,11 +704,11 @@ func TestHandler_GetChallenge(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db, linker: NewLinker("dns", "acme"), validateChallengeOptions: tc.vco} + // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme"), validateChallengeOptions: tc.vco} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetChallenge(w, req) + GetChallenge(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) diff --git a/acme/api/middleware_test.go b/acme/api/middleware_test.go index 8003fa16..f192e67e 100644 --- a/acme/api/middleware_test.go +++ b/acme/api/middleware_test.go @@ -70,7 +70,7 @@ func Test_baseURLFromRequest(t *testing.T) { if tc.requestPreparer != nil { tc.requestPreparer(request) } - result := baseURLFromRequest(request) + result := getBaseURLFromRequest(request) if result == nil || tc.expectedResult == nil { assert.Equals(t, result, tc.expectedResult) } else if result.String() != tc.expectedResult.String() { @@ -81,7 +81,7 @@ func Test_baseURLFromRequest(t *testing.T) { } func TestHandler_baseURLFromRequest(t *testing.T) { - h := &Handler{} + // h := &Handler{} req := httptest.NewRequest("GET", "/foo", nil) req.Host = "test.ca.smallstep.com:8080" w := httptest.NewRecorder() @@ -94,7 +94,7 @@ func TestHandler_baseURLFromRequest(t *testing.T) { } } - h.baseURLFromRequest(next)(w, req) + baseURLFromRequest(next)(w, req) req = httptest.NewRequest("GET", "/foo", nil) req.Host = "" @@ -103,7 +103,7 @@ func TestHandler_baseURLFromRequest(t *testing.T) { assert.Equals(t, baseURLFromContext(r.Context()), nil) } - h.baseURLFromRequest(next)(w, req) + baseURLFromRequest(next)(w, req) } func TestHandler_addNonce(t *testing.T) { @@ -139,10 +139,10 @@ func TestHandler_addNonce(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db} + // h := &Handler{db: tc.db} req := httptest.NewRequest("GET", u, nil) w := httptest.NewRecorder() - h.addNonce(testNext)(w, req) + addNonce(testNext)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -195,11 +195,11 @@ func TestHandler_addDirLink(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{linker: tc.linker} + // h := &Handler{linker: tc.linker} req := httptest.NewRequest("GET", "/foo", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.addDirLink(testNext)(w, req) + addDirLink(testNext)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -242,7 +242,7 @@ func TestHandler_verifyContentType(t *testing.T) { "fail/provisioner-not-set": func(t *testing.T) test { return test{ h: Handler{ - linker: NewLinker("dns", "acme"), + // linker: NewLinker("dns", "acme"), }, url: u, ctx: context.Background(), @@ -254,7 +254,7 @@ func TestHandler_verifyContentType(t *testing.T) { "fail/general-bad-content-type": func(t *testing.T) test { return test{ h: Handler{ - linker: NewLinker("dns", "acme"), + // linker: NewLinker("dns", "acme"), }, url: u, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), @@ -266,7 +266,7 @@ func TestHandler_verifyContentType(t *testing.T) { "fail/certificate-bad-content-type": func(t *testing.T) test { return test{ h: Handler{ - linker: NewLinker("dns", "acme"), + // linker: NewLinker("dns", "acme"), }, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), contentType: "foo", @@ -277,7 +277,7 @@ func TestHandler_verifyContentType(t *testing.T) { "ok": func(t *testing.T) test { return test{ h: Handler{ - linker: NewLinker("dns", "acme"), + // linker: NewLinker("dns", "acme"), }, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), contentType: "application/jose+json", @@ -287,7 +287,7 @@ func TestHandler_verifyContentType(t *testing.T) { "ok/certificate/pkix-cert": func(t *testing.T) test { return test{ h: Handler{ - linker: NewLinker("dns", "acme"), + // linker: NewLinker("dns", "acme"), }, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), contentType: "application/pkix-cert", @@ -297,7 +297,7 @@ func TestHandler_verifyContentType(t *testing.T) { "ok/certificate/jose+json": func(t *testing.T) test { return test{ h: Handler{ - linker: NewLinker("dns", "acme"), + // linker: NewLinker("dns", "acme"), }, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), contentType: "application/jose+json", @@ -307,7 +307,7 @@ func TestHandler_verifyContentType(t *testing.T) { "ok/certificate/pkcs7-mime": func(t *testing.T) test { return test{ h: Handler{ - linker: NewLinker("dns", "acme"), + // linker: NewLinker("dns", "acme"), }, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), contentType: "application/pkcs7-mime", @@ -326,7 +326,7 @@ func TestHandler_verifyContentType(t *testing.T) { req = req.WithContext(tc.ctx) req.Header.Add("Content-Type", tc.contentType) w := httptest.NewRecorder() - tc.h.verifyContentType(testNext)(w, req) + verifyContentType(testNext)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -390,11 +390,11 @@ func TestHandler_isPostAsGet(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{} + // h := &Handler{} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.isPostAsGet(testNext)(w, req) + isPostAsGet(testNext)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -481,10 +481,10 @@ func TestHandler_parseJWS(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{} + // h := &Handler{} req := httptest.NewRequest("GET", u, tc.body) w := httptest.NewRecorder() - h.parseJWS(tc.next)(w, req) + parseJWS(tc.next)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -679,11 +679,11 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{} + // h := &Handler{} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.verifyAndExtractJWSPayload(tc.next)(w, req) + verifyAndExtractJWSPayload(tc.next)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -881,11 +881,11 @@ func TestHandler_lookupJWK(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db, linker: tc.linker} + // h := &Handler{db: tc.db, linker: tc.linker} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.lookupJWK(tc.next)(w, req) + lookupJWK(tc.next)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -1077,11 +1077,11 @@ func TestHandler_extractJWK(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db} + // h := &Handler{db: tc.db} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.extractJWK(tc.next)(w, req) + extractJWK(tc.next)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -1444,11 +1444,11 @@ func TestHandler_validateJWS(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db} + // h := &Handler{db: tc.db} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.validateJWS(tc.next)(w, req) + validateJWS(tc.next)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -1628,11 +1628,11 @@ func TestHandler_extractOrLookupJWK(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db, linker: tc.linker} + // h := &Handler{db: tc.db, linker: tc.linker} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.extractOrLookupJWK(tc.next)(w, req) + extractOrLookupJWK(tc.next)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -1717,11 +1717,11 @@ func TestHandler_checkPrerequisites(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: nil, linker: tc.linker, prerequisitesChecker: tc.prerequisitesChecker} + // h := &Handler{db: nil, linker: tc.linker, prerequisitesChecker: tc.prerequisitesChecker} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.checkPrerequisites(tc.next)(w, req) + checkPrerequisites(tc.next)(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) diff --git a/acme/api/order_test.go b/acme/api/order_test.go index 1ce034e7..f0a2d1d4 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -421,11 +421,11 @@ func TestHandler_GetOrder(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} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetOrder(w, req) + GetOrder(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -636,8 +636,8 @@ func TestHandler_newAuthorization(t *testing.T) { for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) - h := &Handler{db: tc.db} - if err := h.newAuthorization(context.Background(), tc.az); err != nil { + // h := &Handler{db: tc.db} + if err := newAuthorization(context.Background(), tc.az); err != nil { if assert.NotNil(t, tc.err) { switch k := err.(type) { case *acme.Error: @@ -1334,11 +1334,11 @@ 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} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.NewOrder(w, req) + NewOrder(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -1624,11 +1624,11 @@ func TestHandler_FinalizeOrder(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} req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.FinalizeOrder(w, req) + FinalizeOrder(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) diff --git a/acme/api/revoke_test.go b/acme/api/revoke_test.go index 4ff54405..3a0ba70d 100644 --- a/acme/api/revoke_test.go +++ b/acme/api/revoke_test.go @@ -1057,11 +1057,11 @@ func TestHandler_RevokeCert(t *testing.T) { for name, setup := range tests { tc := setup(t) t.Run(name, func(t *testing.T) { - h := &Handler{linker: NewLinker("dns", "acme"), db: tc.db, ca: tc.ca} + // h := &Handler{linker: NewLinker("dns", "acme"), db: tc.db, ca: tc.ca} req := httptest.NewRequest("POST", revokeURL, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.RevokeCert(w, req) + RevokeCert(w, req) res := w.Result() assert.Equals(t, res.StatusCode, tc.statusCode) @@ -1198,8 +1198,8 @@ func TestHandler_isAccountAuthorized(t *testing.T) { for name, setup := range tests { tc := setup(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db} - acmeErr := h.isAccountAuthorized(tc.ctx, tc.existingCert, tc.certToBeRevoked, tc.account) + // h := &Handler{db: tc.db} + acmeErr := isAccountAuthorized(tc.ctx, tc.existingCert, tc.certToBeRevoked, tc.account) expectError := tc.err != nil gotError := acmeErr != nil From 2b7f6931f3fc8d5e9010a758c07bfeb0657bb36e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 28 Apr 2022 14:49:23 +0200 Subject: [PATCH 167/241] 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 55b0f7282144f1c5a343d0c623248c9108acf706 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 28 Apr 2022 15:14:15 -0700 Subject: [PATCH 168/241] Add context methods for the acme linker. --- acme/api/linker.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/acme/api/linker.go b/acme/api/linker.go index a605ffc3..114ba698 100644 --- a/acme/api/linker.go +++ b/acme/api/linker.go @@ -41,6 +41,29 @@ type Linker interface { LinkOrdersByAccountID(ctx context.Context, orders []string) } +type linkerKey struct{} + +// NewLinkerContext adds the given linker to the context. +func NewLinkerContext(ctx context.Context, v Linker) context.Context { + return context.WithValue(ctx, linkerKey{}, v) +} + +// LinkerFromContext returns the current linker from the given context. +func LinkerFromContext(ctx context.Context) (v Linker, ok bool) { + v, ok = ctx.Value(linkerKey{}).(Linker) + return +} + +// MustLinkerFromContext returns the current linker from the given context. It +// will panic if it's not in the context. +func MustLinkerFromContext(ctx context.Context) Linker { + if v, ok := LinkerFromContext(ctx); !ok { + panic("acme linker is not the context") + } else { + return v + } +} + // linker generates ACME links. type linker struct { prefix string From fddd6f7d9542c85b050c2d5ac04a8cfb07a24af4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 28 Apr 2022 15:15:50 -0700 Subject: [PATCH 169/241] Move linker to the acme package. --- acme/{api => }/linker.go | 2 +- acme/{api => }/linker_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename acme/{api => }/linker.go (99%) rename acme/{api => }/linker_test.go (99%) diff --git a/acme/api/linker.go b/acme/linker.go similarity index 99% rename from acme/api/linker.go rename to acme/linker.go index 114ba698..8dc87b14 100644 --- a/acme/api/linker.go +++ b/acme/linker.go @@ -1,4 +1,4 @@ -package api +package acme import ( "context" diff --git a/acme/api/linker_test.go b/acme/linker_test.go similarity index 99% rename from acme/api/linker_test.go rename to acme/linker_test.go index 74c2c8b0..a8612e6b 100644 --- a/acme/api/linker_test.go +++ b/acme/linker_test.go @@ -1,4 +1,4 @@ -package api +package acme import ( "context" From d1f75f172078370776d74edde2086d94122ccbd9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 28 Apr 2022 19:15:18 -0700 Subject: [PATCH 170/241] Refactor ACME api. --- acme/api/account.go | 25 ++-- acme/api/eab.go | 3 +- acme/api/handler.go | 201 ++++++++++++++------------------ acme/api/middleware.go | 106 +++++------------ acme/api/order.go | 47 +++----- acme/api/revoke.go | 14 +-- acme/challenge.go | 35 +++--- acme/client.go | 79 +++++++++++++ acme/common.go | 72 +++++++++++- acme/db.go | 16 +-- acme/linker.go | 257 ++++++++++++++++++++++++----------------- acme/linker_test.go | 43 ++++--- ca/ca.go | 36 +++--- 13 files changed, 503 insertions(+), 431 deletions(-) create mode 100644 acme/client.go diff --git a/acme/api/account.go b/acme/api/account.go index 8c8c4d97..d88c7066 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -69,6 +69,9 @@ func (u *UpdateAccountRequest) Validate() error { // NewAccount is the handler resource for creating new ACME accounts. func NewAccount(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) + payload, err := payloadFromContext(ctx) if err != nil { render.Error(w, err) @@ -120,7 +123,6 @@ func NewAccount(w http.ResponseWriter, r *http.Request) { return } - db := acme.MustFromContext(ctx) acc = &acme.Account{ Key: jwk, Contact: nar.Contact, @@ -148,16 +150,18 @@ func NewAccount(w http.ResponseWriter, r *http.Request) { httpStatus = http.StatusOK } - o := optionsFromContext(ctx) - o.linker.LinkAccount(ctx, acc) + linker.LinkAccount(ctx, acc) - w.Header().Set("Location", o.linker.GetLink(r.Context(), AccountLinkType, acc.ID)) + w.Header().Set("Location", linker.GetLink(r.Context(), acme.AccountLinkType, acc.ID)) render.JSONStatus(w, acc, httpStatus) } // GetOrUpdateAccount is the api for updating an ACME account. func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) + acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) @@ -189,7 +193,6 @@ func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) { acc.Contact = uar.Contact } - db := acme.MustFromContext(ctx) if err := db.UpdateAccount(ctx, acc); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating account")) return @@ -197,10 +200,9 @@ func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) { } } - o := optionsFromContext(ctx) - o.linker.LinkAccount(ctx, acc) + linker.LinkAccount(ctx, acc) - w.Header().Set("Location", o.linker.GetLink(ctx, AccountLinkType, acc.ID)) + w.Header().Set("Location", linker.GetLink(ctx, acme.AccountLinkType, acc.ID)) render.JSON(w, acc) } @@ -216,6 +218,9 @@ func logOrdersByAccount(w http.ResponseWriter, oids []string) { // GetOrdersByAccountID ACME api for retrieving the list of order urls belonging to an account. func GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) + acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) @@ -227,15 +232,13 @@ func GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { return } - db := acme.MustFromContext(ctx) orders, err := db.GetOrdersByAccountID(ctx, acc.ID) if err != nil { render.Error(w, err) return } - o := optionsFromContext(ctx) - o.linker.LinkOrdersByAccountID(ctx, orders) + linker.LinkOrdersByAccountID(ctx, orders) render.JSON(w, orders) logOrdersByAccount(w, orders) diff --git a/acme/api/eab.go b/acme/api/eab.go index 2c94a4ed..13928ac4 100644 --- a/acme/api/eab.go +++ b/acme/api/eab.go @@ -47,7 +47,7 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) return nil, acmeErr } - db := acme.MustFromContext(ctx) + db := acme.MustDatabaseFromContext(ctx) externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID) if err != nil { if _, ok := err.(*acme.Error); ok { @@ -103,7 +103,6 @@ func keysAreEqual(x, y *jose.JSONWebKey) bool { // o The "nonce" field MUST NOT be present // o The "url" field MUST be set to the same value as the outer JWS func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) { - if jws == nil { return "", acme.NewErrorISE("no JWS provided") } diff --git a/acme/api/handler.go b/acme/api/handler.go index 4b916404..efe2b780 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -2,12 +2,10 @@ package api import ( "context" - "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" "fmt" - "net" "net/http" "time" @@ -70,144 +68,117 @@ type HandlerOptions struct { // PrerequisitesChecker checks if all prerequisites for serving ACME are // met by the CA configuration. PrerequisitesChecker func(ctx context.Context) (bool, error) - - linker Linker - validateChallengeOptions *acme.ValidateChallengeOptions -} - -type optionsKey struct{} - -func newOptionsContext(ctx context.Context, o *HandlerOptions) context.Context { - return context.WithValue(ctx, optionsKey{}, o) -} - -func optionsFromContext(ctx context.Context) *HandlerOptions { - o, ok := ctx.Value(optionsKey{}).(*HandlerOptions) - if !ok { - panic("acme options are not in the context") - } - return o } var mustAuthority = func(ctx context.Context) acme.CertificateAuthority { return authority.MustFromContext(ctx) } -// Handler is the ACME API request handler. -type Handler struct { +// handler is the ACME API request handler. +type handler struct { opts *HandlerOptions } // Route traffic and implement the Router interface. -// -// Deprecated: Use api.Route(r Router, opts *HandlerOptions) -func (h *Handler) Route(r api.Router) { - Route(r, h.opts) +func (h *handler) Route(r api.Router) { + route(r, h.opts) } // NewHandler returns a new ACME API handler. -// -// Deprecated: Use api.Route(r Router, opts *HandlerOptions) -func NewHandler(ops HandlerOptions) api.RouterHandler { - return &Handler{ - opts: &ops, +func NewHandler(opts HandlerOptions) api.RouterHandler { + return &handler{ + opts: &opts, } } -// Route traffic and implement the Router interface. -func Route(r api.Router, opts *HandlerOptions) { - // by default all prerequisites are met - if opts.PrerequisitesChecker == nil { - opts.PrerequisitesChecker = func(ctx context.Context) (bool, error) { - return true, nil - } - } - - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - client := http.Client{ - Timeout: 30 * time.Second, - Transport: transport, - } - dialer := &net.Dialer{ - Timeout: 30 * time.Second, - } - - opts.linker = NewLinker(opts.DNS, opts.Prefix) - opts.validateChallengeOptions = &acme.ValidateChallengeOptions{ - HTTPGet: client.Get, - LookupTxt: net.LookupTXT, - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { - return tls.DialWithDialer(dialer, network, addr, config) - }, - } - - withOptions := func(next nextHTTP) nextHTTP { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() +// Route traffic and implement the Router interface. This method requires that +// all the acme components, authority, db, client, linker, and prerequisite +// checker to be present in the context. +func Route(r api.Router) { + route(r, nil) +} - // For backward compatibility with NewHandler. - if ca, ok := opts.CA.(*authority.Authority); ok && ca != nil { - ctx = authority.NewContext(ctx, ca) +func route(r api.Router, opts *HandlerOptions) { + var withContext func(next nextHTTP) nextHTTP + + // For backward compatibility this block adds will add a new middleware that + // will set the ACME components to the context. + if opts != nil { + client := acme.NewClient() + linker := acme.NewLinker(opts.DNS, opts.Prefix) + + withContext = func(next nextHTTP) nextHTTP { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if ca, ok := opts.CA.(*authority.Authority); ok && ca != nil { + ctx = authority.NewContext(ctx, ca) + } + ctx = acme.NewContext(ctx, opts.DB, client, linker, opts.PrerequisitesChecker) + next(w, r.WithContext(ctx)) } - if opts.DB != nil { - ctx = acme.NewContext(ctx, opts.DB) + } + } else { + withContext = func(next nextHTTP) nextHTTP { + return func(w http.ResponseWriter, r *http.Request) { + next(w, r) } - - ctx = newOptionsContext(ctx, opts) - next(w, r.WithContext(ctx)) } } + commonMiddleware := func(next nextHTTP) nextHTTP { + return withContext(func(w http.ResponseWriter, r *http.Request) { + // Linker middleware gets the provisioner and current url from the + // request and sets them in the context. + linker := acme.MustLinkerFromContext(r.Context()) + linker.Middleware(http.HandlerFunc(checkPrerequisites(next))).ServeHTTP(w, r) + }) + } validatingMiddleware := func(next nextHTTP) nextHTTP { - return withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next))))))))) + return commonMiddleware(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next)))))) } extractPayloadByJWK := func(next nextHTTP) nextHTTP { - return withOptions(validatingMiddleware(extractJWK(verifyAndExtractJWSPayload(next)))) + return validatingMiddleware(extractJWK(verifyAndExtractJWSPayload(next))) } extractPayloadByKid := func(next nextHTTP) nextHTTP { - return withOptions(validatingMiddleware(lookupJWK(verifyAndExtractJWSPayload(next)))) + return validatingMiddleware(lookupJWK(verifyAndExtractJWSPayload(next))) } extractPayloadByKidOrJWK := func(next nextHTTP) nextHTTP { - return withOptions(validatingMiddleware(extractOrLookupJWK(verifyAndExtractJWSPayload(next)))) + return validatingMiddleware(extractOrLookupJWK(verifyAndExtractJWSPayload(next))) } - getPath := opts.linker.GetUnescapedPathSuffix + getPath := acme.GetUnescapedPathSuffix // Standard ACME API - r.MethodFunc("GET", getPath(NewNonceLinkType, "{provisionerID}"), - withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(addNonce(addDirLink(GetNonce))))))) - r.MethodFunc("HEAD", getPath(NewNonceLinkType, "{provisionerID}"), - withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(addNonce(addDirLink(GetNonce))))))) - r.MethodFunc("GET", getPath(DirectoryLinkType, "{provisionerID}"), - withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(GetDirectory))))) - r.MethodFunc("HEAD", getPath(DirectoryLinkType, "{provisionerID}"), - withOptions(baseURLFromRequest(lookupProvisioner(checkPrerequisites(GetDirectory))))) - - r.MethodFunc("POST", getPath(NewAccountLinkType, "{provisionerID}"), + r.MethodFunc("GET", getPath(acme.NewNonceLinkType, "{provisionerID}"), + commonMiddleware(addNonce(addDirLink(GetNonce)))) + r.MethodFunc("HEAD", getPath(acme.NewNonceLinkType, "{provisionerID}"), + commonMiddleware(addNonce(addDirLink(GetNonce)))) + r.MethodFunc("GET", getPath(acme.DirectoryLinkType, "{provisionerID}"), + commonMiddleware(GetDirectory)) + r.MethodFunc("HEAD", getPath(acme.DirectoryLinkType, "{provisionerID}"), + commonMiddleware(GetDirectory)) + + r.MethodFunc("POST", getPath(acme.NewAccountLinkType, "{provisionerID}"), extractPayloadByJWK(NewAccount)) - r.MethodFunc("POST", getPath(AccountLinkType, "{provisionerID}", "{accID}"), + r.MethodFunc("POST", getPath(acme.AccountLinkType, "{provisionerID}", "{accID}"), extractPayloadByKid(GetOrUpdateAccount)) - r.MethodFunc("POST", getPath(KeyChangeLinkType, "{provisionerID}", "{accID}"), + r.MethodFunc("POST", getPath(acme.KeyChangeLinkType, "{provisionerID}", "{accID}"), extractPayloadByKid(NotImplemented)) - r.MethodFunc("POST", getPath(NewOrderLinkType, "{provisionerID}"), + r.MethodFunc("POST", getPath(acme.NewOrderLinkType, "{provisionerID}"), extractPayloadByKid(NewOrder)) - r.MethodFunc("POST", getPath(OrderLinkType, "{provisionerID}", "{ordID}"), + r.MethodFunc("POST", getPath(acme.OrderLinkType, "{provisionerID}", "{ordID}"), extractPayloadByKid(isPostAsGet(GetOrder))) - r.MethodFunc("POST", getPath(OrdersByAccountLinkType, "{provisionerID}", "{accID}"), + r.MethodFunc("POST", getPath(acme.OrdersByAccountLinkType, "{provisionerID}", "{accID}"), extractPayloadByKid(isPostAsGet(GetOrdersByAccountID))) - r.MethodFunc("POST", getPath(FinalizeLinkType, "{provisionerID}", "{ordID}"), + r.MethodFunc("POST", getPath(acme.FinalizeLinkType, "{provisionerID}", "{ordID}"), extractPayloadByKid(FinalizeOrder)) - r.MethodFunc("POST", getPath(AuthzLinkType, "{provisionerID}", "{authzID}"), + r.MethodFunc("POST", getPath(acme.AuthzLinkType, "{provisionerID}", "{authzID}"), extractPayloadByKid(isPostAsGet(GetAuthorization))) - r.MethodFunc("POST", getPath(ChallengeLinkType, "{provisionerID}", "{authzID}", "{chID}"), + r.MethodFunc("POST", getPath(acme.ChallengeLinkType, "{provisionerID}", "{authzID}", "{chID}"), extractPayloadByKid(GetChallenge)) - r.MethodFunc("POST", getPath(CertificateLinkType, "{provisionerID}", "{certID}"), + r.MethodFunc("POST", getPath(acme.CertificateLinkType, "{provisionerID}", "{certID}"), extractPayloadByKid(isPostAsGet(GetCertificate))) - r.MethodFunc("POST", getPath(RevokeCertLinkType, "{provisionerID}"), + r.MethodFunc("POST", getPath(acme.RevokeCertLinkType, "{provisionerID}"), extractPayloadByKidOrJWK(RevokeCert)) } @@ -251,20 +222,20 @@ func (d *Directory) ToLog() (interface{}, error) { // for client configuration. func GetDirectory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - o := optionsFromContext(ctx) - acmeProv, err := acmeProvisionerFromContext(ctx) + fmt.Println(acmeProv, err) if err != nil { render.Error(w, err) return } + linker := acme.MustLinkerFromContext(ctx) render.JSON(w, &Directory{ - NewNonce: o.linker.GetLink(ctx, NewNonceLinkType), - NewAccount: o.linker.GetLink(ctx, NewAccountLinkType), - NewOrder: o.linker.GetLink(ctx, NewOrderLinkType), - RevokeCert: o.linker.GetLink(ctx, RevokeCertLinkType), - KeyChange: o.linker.GetLink(ctx, KeyChangeLinkType), + NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType), + NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType), + NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType), + RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType), + KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType), Meta: Meta{ ExternalAccountRequired: acmeProv.RequireEAB, }, @@ -280,8 +251,8 @@ func NotImplemented(w http.ResponseWriter, r *http.Request) { // GetAuthorization ACME api for retrieving an Authz. func GetAuthorization(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - o := optionsFromContext(ctx) - db := acme.MustFromContext(ctx) + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) acc, err := accountFromContext(ctx) if err != nil { @@ -303,17 +274,17 @@ func GetAuthorization(w http.ResponseWriter, r *http.Request) { return } - o.linker.LinkAuthorization(ctx, az) + linker.LinkAuthorization(ctx, az) - w.Header().Set("Location", o.linker.GetLink(ctx, AuthzLinkType, az.ID)) + w.Header().Set("Location", linker.GetLink(ctx, acme.AuthzLinkType, az.ID)) render.JSON(w, az) } // GetChallenge ACME api for retrieving a Challenge. func GetChallenge(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - o := optionsFromContext(ctx) - db := acme.MustFromContext(ctx) + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) acc, err := accountFromContext(ctx) if err != nil { @@ -351,22 +322,22 @@ func GetChallenge(w http.ResponseWriter, r *http.Request) { render.Error(w, err) return } - if err = ch.Validate(ctx, db, jwk, o.validateChallengeOptions); err != nil { + if err = ch.Validate(ctx, db, jwk); err != nil { render.Error(w, acme.WrapErrorISE(err, "error validating challenge")) return } - o.linker.LinkChallenge(ctx, ch, azID) + linker.LinkChallenge(ctx, ch, azID) - w.Header().Add("Link", link(o.linker.GetLink(ctx, AuthzLinkType, azID), "up")) - w.Header().Set("Location", o.linker.GetLink(ctx, ChallengeLinkType, azID, ch.ID)) + w.Header().Add("Link", link(linker.GetLink(ctx, acme.AuthzLinkType, azID), "up")) + w.Header().Set("Location", linker.GetLink(ctx, acme.ChallengeLinkType, azID, ch.ID)) render.JSON(w, ch) } // GetCertificate ACME api for retrieving a Certificate. func GetCertificate(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - db := acme.MustFromContext(ctx) + db := acme.MustDatabaseFromContext(ctx) acc, err := accountFromContext(ctx) if err != nil { diff --git a/acme/api/middleware.go b/acme/api/middleware.go index 564a16f5..09e88b8d 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -31,39 +31,10 @@ func logNonce(w http.ResponseWriter, nonce string) { } } -// getBaseURLFromRequest determines the base URL which should be used for -// constructing link URLs in e.g. the ACME directory result by taking the -// request Host into consideration. -// -// If the Request.Host is an empty string, we return an empty string, to -// indicate that the configured URL values should be used instead. If this -// function returns a non-empty result, then this should be used in constructing -// ACME link URLs. -func getBaseURLFromRequest(r *http.Request) *url.URL { - // NOTE: See https://github.com/letsencrypt/boulder/blob/master/web/relative.go - // for an implementation that allows HTTP requests using the x-forwarded-proto - // header. - - if r.Host == "" { - return nil - } - return &url.URL{Scheme: "https", Host: r.Host} -} - -// baseURLFromRequest is a middleware that extracts and caches the baseURL -// from the request. -// E.g. https://ca.smallstep.com/ -func baseURLFromRequest(next nextHTTP) nextHTTP { - return func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), baseURLContextKey, getBaseURLFromRequest(r)) - next(w, r.WithContext(ctx)) - } -} - // addNonce is a middleware that adds a nonce to the response header. func addNonce(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - db := acme.MustFromContext(r.Context()) + db := acme.MustDatabaseFromContext(r.Context()) nonce, err := db.CreateNonce(r.Context()) if err != nil { render.Error(w, err) @@ -81,9 +52,9 @@ func addNonce(next nextHTTP) nextHTTP { func addDirLink(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - opts := optionsFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) - w.Header().Add("Link", link(opts.linker.GetLink(ctx, DirectoryLinkType), "index")) + w.Header().Add("Link", link(linker.GetLink(ctx, acme.DirectoryLinkType), "index")) next(w, r) } } @@ -92,17 +63,12 @@ func addDirLink(next nextHTTP) nextHTTP { // application/jose+json. func verifyContentType(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - var expected []string - ctx := r.Context() - opts := optionsFromContext(ctx) - - p, err := provisionerFromContext(ctx) - if err != nil { - render.Error(w, err) - return + p := acme.MustProvisionerFromContext(r.Context()) + u := &url.URL{ + Path: acme.GetUnescapedPathSuffix(acme.CertificateLinkType, p.GetName(), ""), } - u := url.URL{Path: opts.linker.GetUnescapedPathSuffix(CertificateLinkType, p.GetName(), "")} + var expected []string if strings.Contains(r.URL.String(), u.EscapedPath()) { // GET /certificate requests allow a greater range of content types. expected = []string{"application/jose+json", "application/pkix-cert", "application/pkcs7-mime"} @@ -159,7 +125,7 @@ func parseJWS(next nextHTTP) nextHTTP { func validateJWS(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - db := acme.MustFromContext(ctx) + db := acme.MustDatabaseFromContext(ctx) jws, err := jwsFromContext(ctx) if err != nil { @@ -247,7 +213,7 @@ func validateJWS(next nextHTTP) nextHTTP { func extractJWK(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - db := acme.MustFromContext(ctx) + db := acme.MustDatabaseFromContext(ctx) jws, err := jwsFromContext(ctx) if err != nil { @@ -325,18 +291,20 @@ func lookupProvisioner(next nextHTTP) nextHTTP { func checkPrerequisites(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - opts := optionsFromContext(ctx) - - ok, err := opts.PrerequisitesChecker(ctx) - if err != nil { - render.Error(w, acme.WrapErrorISE(err, "error checking acme provisioner prerequisites")) - return - } - if !ok { - render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "acme provisioner configuration lacks prerequisites")) - return + // If the function is not set assume that all prerequisites are met. + checkFunc, ok := acme.PrerequisitesCheckerFromContext(ctx) + if ok { + ok, err := checkFunc(ctx) + if err != nil { + render.Error(w, acme.WrapErrorISE(err, "error checking acme provisioner prerequisites")) + return + } + if !ok { + render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "acme provisioner configuration lacks prerequisites")) + return + } } - next(w, r.WithContext(ctx)) + next(w, r) } } @@ -346,8 +314,8 @@ func checkPrerequisites(next nextHTTP) nextHTTP { func lookupJWK(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - opts := optionsFromContext(ctx) - db := acme.MustFromContext(ctx) + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) jws, err := jwsFromContext(ctx) if err != nil { @@ -355,7 +323,7 @@ func lookupJWK(next nextHTTP) nextHTTP { return } - kidPrefix := opts.linker.GetLink(ctx, AccountLinkType, "") + kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "") kid := jws.Signatures[0].Protected.KeyID if !strings.HasPrefix(kid, kidPrefix) { render.Error(w, acme.NewError(acme.ErrorMalformedType, @@ -527,32 +495,14 @@ func jwsFromContext(ctx context.Context) (*jose.JSONWebSignature, error) { return val, nil } -// provisionerFromContext searches the context for a provisioner. Returns the -// provisioner or an error. -func provisionerFromContext(ctx context.Context) (acme.Provisioner, error) { - val := ctx.Value(provisionerContextKey) - if val == nil { - return nil, acme.NewErrorISE("provisioner expected in request context") - } - pval, ok := val.(acme.Provisioner) - if !ok || pval == nil { - return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner") - } - return pval, nil -} - // acmeProvisionerFromContext searches the context for an ACME provisioner. Returns // pointer to an ACME provisioner or an error. func acmeProvisionerFromContext(ctx context.Context) (*provisioner.ACME, error) { - prov, err := provisionerFromContext(ctx) - if err != nil { - return nil, err - } - acmeProv, ok := prov.(*provisioner.ACME) - if !ok || acmeProv == nil { + p, ok := acme.MustProvisionerFromContext(ctx).(*provisioner.ACME) + if !ok { return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner") } - return acmeProv, nil + return p, nil } // payloadFromContext searches the context for a payload. Returns the payload diff --git a/acme/api/order.go b/acme/api/order.go index ebd0c7f5..2b9f912e 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -70,16 +70,15 @@ var defaultOrderBackdate = time.Minute // NewOrder ACME api for creating a new order. func NewOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) + prov := acme.MustProvisionerFromContext(ctx) + acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } - prov, err := provisionerFromContext(ctx) - if err != nil { - render.Error(w, err) - return - } payload, err := payloadFromContext(ctx) if err != nil { render.Error(w, err) @@ -136,16 +135,14 @@ func NewOrder(w http.ResponseWriter, r *http.Request) { o.NotBefore = o.NotBefore.Add(-defaultOrderBackdate) } - db := acme.MustFromContext(ctx) if err := db.CreateOrder(ctx, o); err != nil { render.Error(w, acme.WrapErrorISE(err, "error creating order")) return } - opts := optionsFromContext(ctx) - opts.linker.LinkOrder(ctx, o) + linker.LinkOrder(ctx, o) - w.Header().Set("Location", opts.linker.GetLink(ctx, OrderLinkType, o.ID)) + w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID)) render.JSONStatus(w, o, http.StatusCreated) } @@ -166,7 +163,7 @@ func newAuthorization(ctx context.Context, az *acme.Authorization) error { return acme.WrapErrorISE(err, "error generating random alphanumeric ID") } - db := acme.MustFromContext(ctx) + db := acme.MustDatabaseFromContext(ctx) az.Challenges = make([]*acme.Challenge, len(chTypes)) for i, typ := range chTypes { ch := &acme.Challenge{ @@ -190,18 +187,16 @@ func newAuthorization(ctx context.Context, az *acme.Authorization) error { // GetOrder ACME api for retrieving an order. func GetOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) + prov := acme.MustProvisionerFromContext(ctx) + acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } - prov, err := provisionerFromContext(ctx) - if err != nil { - render.Error(w, err) - return - } - db := acme.MustFromContext(ctx) o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID")) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving order")) @@ -222,26 +217,24 @@ func GetOrder(w http.ResponseWriter, r *http.Request) { return } - opts := optionsFromContext(ctx) - opts.linker.LinkOrder(ctx, o) + linker.LinkOrder(ctx, o) - w.Header().Set("Location", opts.linker.GetLink(ctx, OrderLinkType, o.ID)) + w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID)) render.JSON(w, o) } // FinalizeOrder attemptst to finalize an order and create a certificate. func FinalizeOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) + prov := acme.MustProvisionerFromContext(ctx) + acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } - prov, err := provisionerFromContext(ctx) - if err != nil { - render.Error(w, err) - return - } payload, err := payloadFromContext(ctx) if err != nil { render.Error(w, err) @@ -258,7 +251,6 @@ func FinalizeOrder(w http.ResponseWriter, r *http.Request) { return } - db := acme.MustFromContext(ctx) o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID")) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving order")) @@ -281,10 +273,9 @@ func FinalizeOrder(w http.ResponseWriter, r *http.Request) { return } - opts := optionsFromContext(ctx) - opts.linker.LinkOrder(ctx, o) + linker.LinkOrder(ctx, o) - w.Header().Set("Location", opts.linker.GetLink(ctx, OrderLinkType, o.ID)) + w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID)) render.JSON(w, o) } diff --git a/acme/api/revoke.go b/acme/api/revoke.go index 55774aea..584ed27e 100644 --- a/acme/api/revoke.go +++ b/acme/api/revoke.go @@ -28,13 +28,11 @@ type revokePayload struct { // RevokeCert attempts to revoke a certificate. func RevokeCert(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - jws, err := jwsFromContext(ctx) - if err != nil { - render.Error(w, err) - return - } + db := acme.MustDatabaseFromContext(ctx) + linker := acme.MustLinkerFromContext(ctx) + prov := acme.MustProvisionerFromContext(ctx) - prov, err := provisionerFromContext(ctx) + jws, err := jwsFromContext(ctx) if err != nil { render.Error(w, err) return @@ -67,7 +65,6 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) { return } - db := acme.MustFromContext(ctx) serial := certToBeRevoked.SerialNumber.String() dbCert, err := db.GetCertificateBySerial(ctx, serial) if err != nil { @@ -138,8 +135,7 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) { } logRevoke(w, options) - o := optionsFromContext(ctx) - w.Header().Add("Link", link(o.linker.GetLink(ctx, DirectoryLinkType), "index")) + w.Header().Add("Link", link(linker.GetLink(ctx, acme.DirectoryLinkType), "index")) w.Write(nil) } diff --git a/acme/challenge.go b/acme/challenge.go index 9f08bae5..8d8466bd 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -14,7 +14,6 @@ import ( "fmt" "io" "net" - "net/http" "net/url" "reflect" "strings" @@ -61,27 +60,28 @@ func (ch *Challenge) ToLog() (interface{}, error) { // type using the DB interface. // satisfactorily validated, the 'status' and 'validated' attributes are // updated. -func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { +func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey) error { // If already valid or invalid then return without performing validation. if ch.Status != StatusPending { return nil } switch ch.Type { case HTTP01: - return http01Validate(ctx, ch, db, jwk, vo) + return http01Validate(ctx, ch, db, jwk) case DNS01: - return dns01Validate(ctx, ch, db, jwk, vo) + return dns01Validate(ctx, ch, db, jwk) case TLSALPN01: - return tlsalpn01Validate(ctx, ch, db, jwk, vo) + return tlsalpn01Validate(ctx, ch, db, jwk) default: return NewErrorISE("unexpected challenge type '%s'", ch.Type) } } -func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { +func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error { u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} - resp, err := vo.HTTPGet(u.String()) + vc := MustClientFromContext(ctx) + resp, err := vc.Get(u.String()) if err != nil { return storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err, "error doing http GET for url %s", u)) @@ -141,7 +141,7 @@ func tlsAlert(err error) uint8 { return 0 } -func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { +func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error { config := &tls.Config{ NextProtos: []string{"acme-tls/1"}, // https://tools.ietf.org/html/rfc8737#section-4 @@ -154,7 +154,8 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON hostPort := net.JoinHostPort(ch.Value, "443") - conn, err := vo.TLSDial("tcp", hostPort, config) + vc := MustClientFromContext(ctx) + conn, err := vc.TLSDial("tcp", hostPort, config) if err != nil { // With Go 1.17+ tls.Dial fails if there's no overlap between configured // client and server protocols. When this happens the connection is @@ -253,14 +254,15 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON "incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension")) } -func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { +func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error { // Normalize domain for wildcard DNS names // This is done to avoid making TXT lookups for domains like // _acme-challenge.*.example.com // Instead perform txt lookup for _acme-challenge.example.com domain := strings.TrimPrefix(ch.Value, "*.") - txtRecords, err := vo.LookupTxt("_acme-challenge." + domain) + vc := MustClientFromContext(ctx) + txtRecords, err := vc.LookupTxt("_acme-challenge." + domain) if err != nil { return storeError(ctx, db, ch, false, WrapError(ErrorDNSType, err, "error looking up TXT records for domain %s", domain)) @@ -376,14 +378,3 @@ func storeError(ctx context.Context, db DB, ch *Challenge, markInvalid bool, err } return nil } - -type httpGetter func(string) (*http.Response, error) -type lookupTxt func(string) ([]string, error) -type tlsDialer func(network, addr string, config *tls.Config) (*tls.Conn, error) - -// ValidateChallengeOptions are ACME challenge validator functions. -type ValidateChallengeOptions struct { - HTTPGet httpGetter - LookupTxt lookupTxt - TLSDial tlsDialer -} diff --git a/acme/client.go b/acme/client.go new file mode 100644 index 00000000..2b200e45 --- /dev/null +++ b/acme/client.go @@ -0,0 +1,79 @@ +package acme + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "time" +) + +// Client is the interface used to verify ACME challenges. +type Client interface { + // Get issues an HTTP GET to the specified URL. + Get(url string) (*http.Response, error) + + // LookupTXT returns the DNS TXT records for the given domain name. + LookupTxt(name string) ([]string, error) + + // TLSDial connects to the given network address using net.Dialer and then + // initiates a TLS handshake, returning the resulting TLS connection. + TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) +} + +type clientKey struct{} + +// NewClientContext adds the given client to the context. +func NewClientContext(ctx context.Context, c Client) context.Context { + return context.WithValue(ctx, clientKey{}, c) +} + +// ClientFromContext returns the current client from the given context. +func ClientFromContext(ctx context.Context) (c Client, ok bool) { + c, ok = ctx.Value(clientKey{}).(Client) + return +} + +// MustClientFromContext returns the current client from the given context. It will +// return a new instance of the client if it does not exist. +func MustClientFromContext(ctx context.Context) Client { + if c, ok := ClientFromContext(ctx); !ok { + return NewClient() + } else { + return c + } +} + +type client struct { + http *http.Client + dialer *net.Dialer +} + +// NewClient returns an implementation of Client for verifying ACME challenges. +func NewClient() Client { + return &client{ + http: &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + }, + dialer: &net.Dialer{ + Timeout: 30 * time.Second, + }, + } +} + +func (c *client) Get(url string) (*http.Response, error) { + return c.http.Get(url) +} + +func (c *client) LookupTxt(name string) ([]string, error) { + return net.LookupTXT(name) +} + +func (c *client) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) { + return tls.DialWithDialer(c.dialer, network, addr, config) +} diff --git a/acme/common.go b/acme/common.go index 0c9e83dc..5290c06d 100644 --- a/acme/common.go +++ b/acme/common.go @@ -9,6 +9,16 @@ import ( "github.com/smallstep/certificates/authority/provisioner" ) +// Clock that returns time in UTC rounded to seconds. +type Clock struct{} + +// Now returns the UTC time rounded to seconds. +func (c *Clock) Now() time.Time { + return time.Now().UTC().Truncate(time.Second) +} + +var clock Clock + // 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) @@ -17,15 +27,42 @@ type CertificateAuthority interface { LoadProvisionerByName(string) (provisioner.Interface, error) } -// Clock that returns time in UTC rounded to seconds. -type Clock struct{} +// NewContext adds the given acme components to the context. +func NewContext(ctx context.Context, db DB, client Client, linker Linker, fn PrerequisitesChecker) context.Context { + ctx = NewDatabaseContext(ctx, db) + ctx = NewClientContext(ctx, client) + ctx = NewLinkerContext(ctx, linker) + // Prerequisite checker is optional. + if fn != nil { + ctx = NewPrerequisitesCheckerContext(ctx, fn) + } + return ctx +} -// Now returns the UTC time rounded to seconds. -func (c *Clock) Now() time.Time { - return time.Now().UTC().Truncate(time.Second) +// PrerequisitesChecker is a function that checks if all prerequisites for +// serving ACME are met by the CA configuration. +type PrerequisitesChecker func(ctx context.Context) (bool, error) + +// DefaultPrerequisitesChecker is the default PrerequisiteChecker and returns +// always true. +func DefaultPrerequisitesChecker(ctx context.Context) (bool, error) { + return true, nil } -var clock Clock +type prerequisitesKey struct{} + +// NewPrerequisitesCheckerContext adds the given PrerequisitesChecker to the +// context. +func NewPrerequisitesCheckerContext(ctx context.Context, fn PrerequisitesChecker) context.Context { + return context.WithValue(ctx, prerequisitesKey{}, fn) +} + +// PrerequisitesCheckerFromContext returns the PrerequisitesChecker in the +// context. +func PrerequisitesCheckerFromContext(ctx context.Context) (PrerequisitesChecker, bool) { + fn, ok := ctx.Value(prerequisitesKey{}).(PrerequisitesChecker) + return fn, ok && fn != nil +} // Provisioner is an interface that implements a subset of the provisioner.Interface -- // only those methods required by the ACME api/authority. @@ -38,6 +75,29 @@ type Provisioner interface { GetOptions() *provisioner.Options } +type provisionerKey struct{} + +// NewProvisionerContext adds the given provisioner to the context. +func NewProvisionerContext(ctx context.Context, v Provisioner) context.Context { + return context.WithValue(ctx, provisionerKey{}, v) +} + +// ProvisionerFromContext returns the current provisioner from the given context. +func ProvisionerFromContext(ctx context.Context) (v Provisioner, ok bool) { + v, ok = ctx.Value(provisionerKey{}).(Provisioner) + return +} + +// MustLinkerFromContext returns the current provisioner from the given context. +// It will panic if it's not in the context. +func MustProvisionerFromContext(ctx context.Context) Provisioner { + if v, ok := ProvisionerFromContext(ctx); !ok { + panic("acme provisioner is not the context") + } else { + return v + } +} + // MockProvisioner for testing type MockProvisioner struct { Mret1 interface{} diff --git a/acme/db.go b/acme/db.go index a8637f57..3d781156 100644 --- a/acme/db.go +++ b/acme/db.go @@ -50,21 +50,21 @@ type DB interface { type dbKey struct{} -// NewContext adds the given acme database to the context. -func NewContext(ctx context.Context, db DB) context.Context { +// NewDatabaseContext adds the given acme database to the context. +func NewDatabaseContext(ctx context.Context, db DB) context.Context { return context.WithValue(ctx, dbKey{}, db) } -// FromContext returns the current acme database from the given context. -func FromContext(ctx context.Context) (db DB, ok bool) { +// DatabaseFromContext returns the current acme database from the given context. +func DatabaseFromContext(ctx context.Context) (db DB, ok bool) { db, ok = ctx.Value(dbKey{}).(DB) return } -// MustFromContext returns the current database from the given context. It -// will panic if it's not in the context. -func MustFromContext(ctx context.Context) DB { - if db, ok := FromContext(ctx); !ok { +// MustDatabaseFromContext returns the current database from the given context. +// It will panic if it's not in the context. +func MustDatabaseFromContext(ctx context.Context) DB { + if db, ok := DatabaseFromContext(ctx); !ok { panic("acme database is not in the context") } else { return db diff --git a/acme/linker.go b/acme/linker.go index 8dc87b14..6e9110c2 100644 --- a/acme/linker.go +++ b/acme/linker.go @@ -4,12 +4,98 @@ import ( "context" "fmt" "net" + "net/http" "net/url" "strings" - "github.com/smallstep/certificates/acme" + "github.com/go-chi/chi" + "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" ) +// LinkType captures the link type. +type LinkType int + +const ( + // NewNonceLinkType new-nonce + NewNonceLinkType LinkType = iota + // NewAccountLinkType new-account + NewAccountLinkType + // AccountLinkType account + AccountLinkType + // OrderLinkType order + OrderLinkType + // NewOrderLinkType new-order + NewOrderLinkType + // OrdersByAccountLinkType list of orders owned by account + OrdersByAccountLinkType + // FinalizeLinkType finalize order + FinalizeLinkType + // NewAuthzLinkType authz + NewAuthzLinkType + // AuthzLinkType new-authz + AuthzLinkType + // ChallengeLinkType challenge + ChallengeLinkType + // CertificateLinkType certificate + CertificateLinkType + // DirectoryLinkType directory + DirectoryLinkType + // RevokeCertLinkType revoke certificate + RevokeCertLinkType + // KeyChangeLinkType key rollover + KeyChangeLinkType +) + +func (l LinkType) String() string { + switch l { + case NewNonceLinkType: + return "new-nonce" + case NewAccountLinkType: + return "new-account" + case AccountLinkType: + return "account" + case NewOrderLinkType: + return "new-order" + case OrderLinkType: + return "order" + case NewAuthzLinkType: + return "new-authz" + case AuthzLinkType: + return "authz" + case ChallengeLinkType: + return "challenge" + case CertificateLinkType: + return "certificate" + case DirectoryLinkType: + return "directory" + case RevokeCertLinkType: + return "revoke-cert" + case KeyChangeLinkType: + return "key-change" + default: + return fmt.Sprintf("unexpected LinkType '%d'", int(l)) + } +} + +func GetUnescapedPathSuffix(typ LinkType, provisionerName string, inputs ...string) string { + switch typ { + case NewNonceLinkType, NewAccountLinkType, NewOrderLinkType, NewAuthzLinkType, DirectoryLinkType, KeyChangeLinkType, RevokeCertLinkType: + return fmt.Sprintf("/%s/%s", provisionerName, typ) + case AccountLinkType, OrderLinkType, AuthzLinkType, CertificateLinkType: + return fmt.Sprintf("/%s/%s/%s", provisionerName, typ, inputs[0]) + case ChallengeLinkType: + return fmt.Sprintf("/%s/%s/%s/%s", provisionerName, typ, inputs[0], inputs[1]) + case OrdersByAccountLinkType: + return fmt.Sprintf("/%s/%s/%s/orders", provisionerName, AccountLinkType, inputs[0]) + case FinalizeLinkType: + return fmt.Sprintf("/%s/%s/%s/finalize", provisionerName, OrderLinkType, inputs[0]) + default: + return "" + } +} + // NewLinker returns a new Directory type. func NewLinker(dns, prefix string) Linker { _, _, err := net.SplitHostPort(dns) @@ -32,12 +118,11 @@ func NewLinker(dns, prefix string) Linker { // Linker interface for generating links for ACME resources. type Linker interface { GetLink(ctx context.Context, typ LinkType, inputs ...string) string - GetUnescapedPathSuffix(typ LinkType, provName string, inputs ...string) string - - LinkOrder(ctx context.Context, o *acme.Order) - LinkAccount(ctx context.Context, o *acme.Account) - LinkChallenge(ctx context.Context, o *acme.Challenge, azID string) - LinkAuthorization(ctx context.Context, o *acme.Authorization) + Middleware(http.Handler) http.Handler + LinkOrder(ctx context.Context, o *Order) + LinkAccount(ctx context.Context, o *Account) + LinkChallenge(ctx context.Context, o *Challenge, azID string) + LinkAuthorization(ctx context.Context, o *Authorization) LinkOrdersByAccountID(ctx context.Context, orders []string) } @@ -64,127 +149,81 @@ func MustLinkerFromContext(ctx context.Context) Linker { } } +type baseURLKey struct{} + +func newBaseURLContext(ctx context.Context, r *http.Request) context.Context { + var u *url.URL + if r.Host != "" { + u = &url.URL{Scheme: "https", Host: r.Host} + } + return context.WithValue(ctx, baseURLKey{}, u) +} + +func baseURLFromContext(ctx context.Context) *url.URL { + if u, ok := ctx.Value(baseURLKey{}).(*url.URL); ok { + return u + } + return nil +} + // linker generates ACME links. type linker struct { prefix string dns string } -func (l *linker) GetUnescapedPathSuffix(typ LinkType, provisionerName string, inputs ...string) string { - switch typ { - case NewNonceLinkType, NewAccountLinkType, NewOrderLinkType, NewAuthzLinkType, DirectoryLinkType, KeyChangeLinkType, RevokeCertLinkType: - return fmt.Sprintf("/%s/%s", provisionerName, typ) - case AccountLinkType, OrderLinkType, AuthzLinkType, CertificateLinkType: - return fmt.Sprintf("/%s/%s/%s", provisionerName, typ, inputs[0]) - case ChallengeLinkType: - return fmt.Sprintf("/%s/%s/%s/%s", provisionerName, typ, inputs[0], inputs[1]) - case OrdersByAccountLinkType: - return fmt.Sprintf("/%s/%s/%s/orders", provisionerName, AccountLinkType, inputs[0]) - case FinalizeLinkType: - return fmt.Sprintf("/%s/%s/%s/finalize", provisionerName, OrderLinkType, inputs[0]) - default: - return "" - } +// Middleware gets the provisioner and current url from the request and sets +// them in the context so we can use the linker to create ACME links. +func (l *linker) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Add base url to the context. + ctx := newBaseURLContext(r.Context(), r) + + // Add provisioner to the context. + nameEscaped := chi.URLParam(r, "provisionerID") + name, err := url.PathUnescape(nameEscaped) + if err != nil { + render.Error(w, WrapErrorISE(err, "error url unescaping provisioner name '%s'", nameEscaped)) + return + } + + p, err := authority.MustFromContext(ctx).LoadProvisionerByName(name) + if err != nil { + render.Error(w, err) + return + } + + acmeProv, ok := p.(*provisioner.ACME) + if !ok { + render.Error(w, NewError(ErrorAccountDoesNotExistType, "provisioner must be of type ACME")) + return + } + + ctx = NewProvisionerContext(ctx, Provisioner(acmeProv)) + next.ServeHTTP(w, r.WithContext(ctx)) + }) } -// GetLink is a helper for GetLinkExplicit +// GetLink is a helper for GetLinkExplicit. func (l *linker) GetLink(ctx context.Context, typ LinkType, inputs ...string) string { - var ( - provName string - baseURL = baseURLFromContext(ctx) - u = url.URL{} - ) - if p, err := provisionerFromContext(ctx); err == nil && p != nil { - provName = p.GetName() - } - // Copy the baseURL value from the pointer. https://github.com/golang/go/issues/38351 - if baseURL != nil { + var u url.URL + if baseURL := baseURLFromContext(ctx); baseURL != nil { u = *baseURL } - - u.Path = l.GetUnescapedPathSuffix(typ, provName, inputs...) - - // If no Scheme is set, then default to https. if u.Scheme == "" { u.Scheme = "https" } - - // If no Host is set, then use the default (first DNS attr in the ca.json). if u.Host == "" { u.Host = l.dns } - u.Path = l.prefix + u.Path + p := MustProvisionerFromContext(ctx) + u.Path = l.prefix + GetUnescapedPathSuffix(typ, p.GetName(), inputs...) return u.String() } -// LinkType captures the link type. -type LinkType int - -const ( - // NewNonceLinkType new-nonce - NewNonceLinkType LinkType = iota - // NewAccountLinkType new-account - NewAccountLinkType - // AccountLinkType account - AccountLinkType - // OrderLinkType order - OrderLinkType - // NewOrderLinkType new-order - NewOrderLinkType - // OrdersByAccountLinkType list of orders owned by account - OrdersByAccountLinkType - // FinalizeLinkType finalize order - FinalizeLinkType - // NewAuthzLinkType authz - NewAuthzLinkType - // AuthzLinkType new-authz - AuthzLinkType - // ChallengeLinkType challenge - ChallengeLinkType - // CertificateLinkType certificate - CertificateLinkType - // DirectoryLinkType directory - DirectoryLinkType - // RevokeCertLinkType revoke certificate - RevokeCertLinkType - // KeyChangeLinkType key rollover - KeyChangeLinkType -) - -func (l LinkType) String() string { - switch l { - case NewNonceLinkType: - return "new-nonce" - case NewAccountLinkType: - return "new-account" - case AccountLinkType: - return "account" - case NewOrderLinkType: - return "new-order" - case OrderLinkType: - return "order" - case NewAuthzLinkType: - return "new-authz" - case AuthzLinkType: - return "authz" - case ChallengeLinkType: - return "challenge" - case CertificateLinkType: - return "certificate" - case DirectoryLinkType: - return "directory" - case RevokeCertLinkType: - return "revoke-cert" - case KeyChangeLinkType: - return "key-change" - default: - return fmt.Sprintf("unexpected LinkType '%d'", int(l)) - } -} - // LinkOrder sets the ACME links required by an ACME order. -func (l *linker) LinkOrder(ctx context.Context, o *acme.Order) { +func (l *linker) LinkOrder(ctx context.Context, o *Order) { o.AuthorizationURLs = make([]string, len(o.AuthorizationIDs)) for i, azID := range o.AuthorizationIDs { o.AuthorizationURLs[i] = l.GetLink(ctx, AuthzLinkType, azID) @@ -196,17 +235,17 @@ func (l *linker) LinkOrder(ctx context.Context, o *acme.Order) { } // LinkAccount sets the ACME links required by an ACME account. -func (l *linker) LinkAccount(ctx context.Context, acc *acme.Account) { +func (l *linker) LinkAccount(ctx context.Context, acc *Account) { acc.OrdersURL = l.GetLink(ctx, OrdersByAccountLinkType, acc.ID) } // LinkChallenge sets the ACME links required by an ACME challenge. -func (l *linker) LinkChallenge(ctx context.Context, ch *acme.Challenge, azID string) { +func (l *linker) LinkChallenge(ctx context.Context, ch *Challenge, azID string) { ch.URL = l.GetLink(ctx, ChallengeLinkType, azID, ch.ID) } // LinkAuthorization sets the ACME links required by an ACME authorization. -func (l *linker) LinkAuthorization(ctx context.Context, az *acme.Authorization) { +func (l *linker) LinkAuthorization(ctx context.Context, az *Authorization) { for _, ch := range az.Challenges { l.LinkChallenge(ctx, ch, az.ID) } diff --git a/acme/linker_test.go b/acme/linker_test.go index a8612e6b..1946dd88 100644 --- a/acme/linker_test.go +++ b/acme/linker_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/smallstep/assert" - "github.com/smallstep/certificates/acme" ) func TestLinker_GetUnescapedPathSuffix(t *testing.T) { @@ -173,27 +172,27 @@ func TestLinker_LinkOrder(t *testing.T) { linkerPrefix := "acme" l := NewLinker("dns", linkerPrefix) type test struct { - o *acme.Order - validate func(o *acme.Order) + o *Order + validate func(o *Order) } var tests = map[string]test{ "no-authz-and-no-cert": { - o: &acme.Order{ + o: &Order{ ID: oid, }, - validate: func(o *acme.Order) { + validate: func(o *Order) { assert.Equals(t, o.FinalizeURL, fmt.Sprintf("%s/%s/%s/order/%s/finalize", baseURL, linkerPrefix, provName, oid)) assert.Equals(t, o.AuthorizationURLs, []string{}) assert.Equals(t, o.CertificateURL, "") }, }, "one-authz-and-cert": { - o: &acme.Order{ + o: &Order{ ID: oid, CertificateID: certID, AuthorizationIDs: []string{"foo"}, }, - validate: func(o *acme.Order) { + validate: func(o *Order) { assert.Equals(t, o.FinalizeURL, fmt.Sprintf("%s/%s/%s/order/%s/finalize", baseURL, linkerPrefix, provName, oid)) assert.Equals(t, o.AuthorizationURLs, []string{ fmt.Sprintf("%s/%s/%s/authz/%s", baseURL, linkerPrefix, provName, "foo"), @@ -202,12 +201,12 @@ func TestLinker_LinkOrder(t *testing.T) { }, }, "many-authz": { - o: &acme.Order{ + o: &Order{ ID: oid, CertificateID: certID, AuthorizationIDs: []string{"foo", "bar", "zap"}, }, - validate: func(o *acme.Order) { + validate: func(o *Order) { assert.Equals(t, o.FinalizeURL, fmt.Sprintf("%s/%s/%s/order/%s/finalize", baseURL, linkerPrefix, provName, oid)) assert.Equals(t, o.AuthorizationURLs, []string{ fmt.Sprintf("%s/%s/%s/authz/%s", baseURL, linkerPrefix, provName, "foo"), @@ -237,15 +236,15 @@ func TestLinker_LinkAccount(t *testing.T) { linkerPrefix := "acme" l := NewLinker("dns", linkerPrefix) type test struct { - a *acme.Account - validate func(o *acme.Account) + a *Account + validate func(o *Account) } var tests = map[string]test{ "ok": { - a: &acme.Account{ + a: &Account{ ID: accID, }, - validate: func(a *acme.Account) { + validate: func(a *Account) { assert.Equals(t, a.OrdersURL, fmt.Sprintf("%s/%s/%s/account/%s/orders", baseURL, linkerPrefix, provName, accID)) }, }, @@ -270,15 +269,15 @@ func TestLinker_LinkChallenge(t *testing.T) { linkerPrefix := "acme" l := NewLinker("dns", linkerPrefix) type test struct { - ch *acme.Challenge - validate func(o *acme.Challenge) + ch *Challenge + validate func(o *Challenge) } var tests = map[string]test{ "ok": { - ch: &acme.Challenge{ + ch: &Challenge{ ID: chID, }, - validate: func(ch *acme.Challenge) { + validate: func(ch *Challenge) { assert.Equals(t, ch.URL, fmt.Sprintf("%s/%s/%s/challenge/%s/%s", baseURL, linkerPrefix, provName, azID, ch.ID)) }, }, @@ -305,20 +304,20 @@ func TestLinker_LinkAuthorization(t *testing.T) { linkerPrefix := "acme" l := NewLinker("dns", linkerPrefix) type test struct { - az *acme.Authorization - validate func(o *acme.Authorization) + az *Authorization + validate func(o *Authorization) } var tests = map[string]test{ "ok": { - az: &acme.Authorization{ + az: &Authorization{ ID: azID, - Challenges: []*acme.Challenge{ + Challenges: []*Challenge{ {ID: chID0}, {ID: chID1}, {ID: chID2}, }, }, - validate: func(az *acme.Authorization) { + validate: func(az *Authorization) { assert.Equals(t, az.Challenges[0].URL, fmt.Sprintf("%s/%s/%s/challenge/%s/%s", baseURL, linkerPrefix, provName, az.ID, chID0)) assert.Equals(t, az.Challenges[1].URL, fmt.Sprintf("%s/%s/%s/challenge/%s/%s", baseURL, linkerPrefix, provName, az.ID, chID1)) assert.Equals(t, az.Challenges[2].URL, fmt.Sprintf("%s/%s/%s/challenge/%s/%s", baseURL, linkerPrefix, provName, az.ID, chID2)) diff --git a/ca/ca.go b/ca/ca.go index a8ecbb05..e910da74 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -189,30 +189,24 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { dns = fmt.Sprintf("%s:%s", dns, port) } - // ACME Router - prefix := "acme" + // ACME Router is only available if we have a database. var acmeDB acme.DB - if cfg.DB == nil { - acmeDB = nil - } else { + var acmeLinker acme.Linker + if cfg.DB != nil { acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB)) if err != nil { return nil, errors.Wrap(err, "error configuring ACME DB interface") } + acmeLinker = acme.NewLinker(dns, "acme") + mux.Route("/acme", func(r chi.Router) { + acmeAPI.Route(r) + }) + // Use 2.0 because, at the moment, our ACME api is only compatible with v2.0 + // of the ACME spec. + mux.Route("/2.0/acme", func(r chi.Router) { + acmeAPI.Route(r) + }) } - acmeOptions := &acmeAPI.HandlerOptions{ - Backdate: *cfg.AuthorityConfig.Backdate, - DNS: dns, - Prefix: prefix, - } - mux.Route("/"+prefix, func(r chi.Router) { - acmeAPI.Route(r, acmeOptions) - }) - // Use 2.0 because, at the moment, our ACME api is only compatible with v2.0 - // of the ACME spec. - mux.Route("/2.0/"+prefix, func(r chi.Router) { - acmeAPI.Route(r, acmeOptions) - }) // Admin API Router if cfg.AuthorityConfig.EnableAdmin { @@ -280,7 +274,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } // Create context with all the necessary values. - baseContext := buildContext(auth, scepAuthority, acmeDB) + baseContext := buildContext(auth, scepAuthority, acmeDB, acmeLinker) ca.srv = server.New(cfg.Address, handler, tlsConfig) ca.srv.BaseContext = func(net.Listener) context.Context { @@ -304,7 +298,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } // buildContext builds the server base context. -func buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB acme.DB) context.Context { +func buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB acme.DB, acmeLinker acme.Linker) context.Context { ctx := authority.NewContext(context.Background(), a) if authDB := a.GetDatabase(); authDB != nil { ctx = db.NewContext(ctx, authDB) @@ -316,7 +310,7 @@ func buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB ctx = scep.NewContext(ctx, scepAuthority) } if acmeDB != nil { - ctx = acme.NewContext(ctx, acmeDB) + ctx = acme.NewContext(ctx, acmeDB, acme.NewClient(), acmeLinker, nil) } return ctx } From d82e51b74820bcc20ffef6862feb13b2614f715a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 29 Apr 2022 15:08:19 +0200 Subject: [PATCH 171/241] 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 4cb74e7d8ba5a70b2b5c6c00f3907ad0899524fe Mon Sep 17 00:00:00 2001 From: max furman Date: Sat, 30 Apr 2022 13:08:28 -0700 Subject: [PATCH 172/241] fix linter warnings --- authority/authority.go | 2 +- docs/GETTING_STARTED.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 2c10b626..63375351 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -116,7 +116,7 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) { } // FromOptions creates an Authority exclusively using the passed in options -// and does not intialize the Authority. +// and does not initialize the Authority. func FromOptions(opts ...Option) (*Authority, error) { var a = new(Authority) diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 84e968ab..67c5673d 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -654,7 +654,7 @@ preferably not all - meaning it never leaves the server on which it was created. ### Passwords -When you intialize your PKI (`step ca init`) the root and intermediate +When you initialize your PKI (`step ca init`) the root and intermediate private keys will be encrypted with the same password. We recommend that you change the password with which the intermediate is encrypted at your earliest convenience. @@ -681,7 +681,7 @@ to divide the root private key password across a handful of trusted parties. ### Provisioners -When you intialize your PKI (`step ca init`) a default provisioner will be created +When you initialize your PKI (`step ca init`) a default provisioner will be created and it's private key will be encrypted using the same password used to encrypt the root private key. Before deploying the Step CA you should remove this provisioner and add new ones that are encrypted with new, secure, random passwords. From 13173ec8a2d3fdc4150304aeb60937bb4ddf20df Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sun, 1 May 2022 22:29:17 +0200 Subject: [PATCH 173/241] Fix SCEP GET requests --- scep/api/api.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scep/api/api.go b/scep/api/api.go index 31f0f10d..fcabfc58 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -86,7 +86,7 @@ func (h *handler) Get(w http.ResponseWriter, r *http.Request) { case opnGetCACaps: res, err = h.GetCACaps(ctx) case opnPKIOperation: - // TODO: implement the GET for PKI operation? Default CACAPS doesn't specify this is in use, though + res, err = h.PKIOperation(ctx, req) default: err = fmt.Errorf("unknown operation: %s", req.Operation) } @@ -151,8 +151,8 @@ func decodeRequest(r *http.Request) (request, error) { if _, ok := query["message"]; ok { message = query.Get("message") } - // TODO: verify this; it seems like it should be StdEncoding instead of URLEncoding - decodedMessage, err := base64.URLEncoding.DecodeString(message) + // TODO: verify this; right type of encoding? Needs additional transformations? + decodedMessage, err := base64.StdEncoding.DecodeString(message) if err != nil { return request{}, err } From 77893ea55c6aeeac4f469de725c4e63d71df6782 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 2 May 2022 15:55:26 +0200 Subject: [PATCH 174/241] 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 6f9d847bc6489f7669997edd0e6db5dcb0b9e2d1 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 2 May 2022 17:35:35 -0700 Subject: [PATCH 175/241] Fix panic in acme/api tests. --- acme/api/account_test.go | 78 ++++++------ acme/api/eab_test.go | 48 +++---- acme/api/handler.go | 1 - acme/api/handler_test.go | 93 ++++++++------ acme/api/middleware.go | 66 ++++------ acme/api/middleware_test.go | 241 ++++++++++++------------------------ acme/api/order.go | 18 ++- acme/api/order_test.go | 112 +++++++++-------- acme/api/revoke.go | 7 +- acme/api/revoke_test.go | 62 +++++----- 10 files changed, 333 insertions(+), 393 deletions(-) diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 3fbabfe5..18d24ab6 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -296,10 +296,9 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{ID: accID} - ctx := context.WithValue(context.Background(), accContextKey, acc) - ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = acme.NewProvisionerContext(ctx, prov) + ctx = context.WithValue(ctx, accContextKey, acc) return test{ db: &acme.MockDB{ MockGetOrdersByAccountID: func(ctx context.Context, id string) ([]string, error) { @@ -315,9 +314,9 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() GetOrdersByAccountID(w, req) res := w.Result() @@ -363,6 +362,7 @@ func TestHandler_NewAccount(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-payload": func(t *testing.T) test { return test{ + db: &acme.MockDB{}, ctx: context.Background(), statusCode: 500, err: acme.NewErrorISE("payload expected in request context"), @@ -371,6 +371,7 @@ func TestHandler_NewAccount(t *testing.T) { "fail/nil-payload": func(t *testing.T) test { ctx := context.WithValue(context.Background(), payloadContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload expected in request context"), @@ -379,6 +380,7 @@ func TestHandler_NewAccount(t *testing.T) { "fail/unmarshal-payload-error": func(t *testing.T) test { ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "failed to "+ @@ -393,6 +395,7 @@ func TestHandler_NewAccount(t *testing.T) { assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "contact cannot be empty string"), @@ -405,8 +408,9 @@ func TestHandler_NewAccount(t *testing.T) { b, err := json.Marshal(nar) assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), @@ -418,9 +422,10 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("jwk expected in request context"), @@ -432,10 +437,11 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("jwk expected in request context"), @@ -454,9 +460,9 @@ func TestHandler_NewAccount(t *testing.T) { prov.RequireEAB = true ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided"), @@ -471,7 +477,7 @@ func TestHandler_NewAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwkContextKey, jwk) return test{ db: &acme.MockDB{ @@ -510,9 +516,9 @@ func TestHandler_NewAccount(t *testing.T) { } ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, scepProvisioner) + ctx = acme.NewProvisionerContext(ctx, scepProvisioner) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewError(acme.ErrorServerInternalType, "provisioner in context is not an ACME provisioner"), @@ -551,8 +557,7 @@ func TestHandler_NewAccount(t *testing.T) { prov.RequireEAB = true ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) eak := &acme.ExternalAccountKey{ ID: "eakID", @@ -599,8 +604,7 @@ func TestHandler_NewAccount(t *testing.T) { assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) return test{ db: &acme.MockDB{ MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { @@ -635,11 +639,11 @@ func TestHandler_NewAccount(t *testing.T) { Status: acme.StatusValid, Contact: []string{"foo", "bar"}, } - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, accContextKey, acc) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ + db: &acme.MockDB{}, ctx: ctx, acc: acc, statusCode: 200, @@ -664,8 +668,7 @@ func TestHandler_NewAccount(t *testing.T) { prov.RequireEAB = false ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) return test{ db: &acme.MockDB{ MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { @@ -719,8 +722,7 @@ func TestHandler_NewAccount(t *testing.T) { prov.RequireEAB = true ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -759,9 +761,9 @@ func TestHandler_NewAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil) req := httptest.NewRequest("GET", "/foo/bar", nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() NewAccount(w, req) res := w.Result() @@ -814,6 +816,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-account": func(t *testing.T) test { return test{ + db: &acme.MockDB{}, ctx: context.Background(), statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), @@ -822,6 +825,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { "fail/nil-account": func(t *testing.T) test { ctx := context.WithValue(context.Background(), accContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), @@ -830,6 +834,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { "fail/no-payload": func(t *testing.T) test { ctx := context.WithValue(context.Background(), accContextKey, &acc) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload expected in request context"), @@ -839,6 +844,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { ctx := context.WithValue(context.Background(), accContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload expected in request context"), @@ -848,6 +854,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { ctx := context.WithValue(context.Background(), accContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "failed to unmarshal new-account request payload: unexpected end of JSON input"), @@ -862,6 +869,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { ctx := context.WithValue(context.Background(), accContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "contact cannot be empty string"), @@ -894,10 +902,9 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } b, err := json.Marshal(uar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ db: &acme.MockDB{ MockUpdateAccount: func(ctx context.Context, upd *acme.Account) error { @@ -914,11 +921,11 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { uar := &UpdateAccountRequest{} b, err := json.Marshal(uar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 200, } @@ -929,10 +936,9 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } b, err := json.Marshal(uar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ db: &acme.MockDB{ MockUpdateAccount: func(ctx context.Context, upd *acme.Account) error { @@ -946,11 +952,11 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } }, "ok/post-as-get": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isPostAsGet: true}) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 200, } @@ -959,9 +965,9 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil) req := httptest.NewRequest("GET", "/foo/bar", nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() GetOrUpdateAccount(w, req) res := w.Result() diff --git a/acme/api/eab_test.go b/acme/api/eab_test.go index 1c76618b..ae47a1b9 100644 --- a/acme/api/eab_test.go +++ b/acme/api/eab_test.go @@ -98,8 +98,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { assert.FatalError(t, err) prov := newACMEProv(t) ctx := context.WithValue(context.Background(), jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -143,8 +142,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) createdAt := time.Now() return test{ @@ -198,8 +196,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { } ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, scepProvisioner) + ctx = acme.NewProvisionerContext(ctx, scepProvisioner) return test{ ctx: ctx, err: acme.NewError(acme.ErrorServerInternalType, "could not load ACME provisioner from context: provisioner in context is not an ACME provisioner"), @@ -218,8 +215,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -264,8 +260,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{}, @@ -310,8 +305,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -358,8 +352,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -408,8 +401,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -458,8 +450,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -506,8 +497,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) createdAt := time.Now() boundAt := time.Now().Add(1 * time.Second) @@ -565,8 +555,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -623,8 +612,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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 = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -678,8 +666,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true - ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -734,8 +721,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, nil) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ @@ -762,10 +748,8 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{ - // db: tc.db, - // } - got, err := validateExternalAccountBinding(tc.ctx, tc.nar) + ctx := acme.NewDatabaseContext(tc.ctx, tc.db) + got, err := validateExternalAccountBinding(ctx, tc.nar) wantErr := tc.err != nil gotErr := err != nil if wantErr != gotErr { diff --git a/acme/api/handler.go b/acme/api/handler.go index efe2b780..f6d79031 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -223,7 +223,6 @@ func (d *Directory) ToLog() (interface{}, error) { func GetDirectory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acmeProv, err := acmeProvisionerFromContext(ctx) - fmt.Println(acmeProv, err) if err != nil { render.Error(w, err) return diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index fcc33a87..2ac41228 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -3,6 +3,7 @@ package api import ( "bytes" "context" + "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" @@ -24,6 +25,29 @@ import ( "go.step.sm/crypto/pemutil" ) +type mockClient struct { + get func(url string) (*http.Response, error) + lookupTxt func(name string) ([]string, error) + tlsDial func(network, addr string, config *tls.Config) (*tls.Conn, error) +} + +func (m *mockClient) Get(url string) (*http.Response, error) { return m.get(url) } +func (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) } +func (m *mockClient) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) { + return m.tlsDial(network, addr, config) +} + +func mockMustAuthority(t *testing.T, a acme.CertificateAuthority) { + t.Helper() + fn := mustAuthority + t.Cleanup(func() { + mustAuthority = fn + }) + mustAuthority = func(ctx context.Context) acme.CertificateAuthority { + return a + } +} + func TestHandler_GetNonce(t *testing.T) { tests := []struct { name string @@ -52,7 +76,7 @@ func TestHandler_GetNonce(t *testing.T) { } func TestHandler_GetDirectory(t *testing.T) { - linker := NewLinker("ca.smallstep.com", "acme") + linker := acme.NewLinker("ca.smallstep.com", "acme") _ = linker type test struct { ctx context.Context @@ -62,13 +86,10 @@ func TestHandler_GetDirectory(t *testing.T) { } var tests = map[string]func(t *testing.T) test{ "fail/no-provisioner": func(t *testing.T) test { - baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - ctx := context.WithValue(context.Background(), provisionerContextKey, nil) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ - ctx: ctx, + ctx: context.Background(), statusCode: 500, - err: acme.NewErrorISE("provisioner in context is not an ACME provisioner"), + err: acme.NewErrorISE("provisioner is not in context"), } }, "fail/different-provisioner": func(t *testing.T) test { @@ -76,9 +97,7 @@ func TestHandler_GetDirectory(t *testing.T) { Type: "SCEP", Name: "test@scep-provisioner.com", } - baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := acme.NewProvisionerContext(context.Background(), prov) return test{ ctx: ctx, statusCode: 500, @@ -89,8 +108,7 @@ func TestHandler_GetDirectory(t *testing.T) { prov := newProv() provName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := acme.NewProvisionerContext(context.Background(), prov) expDir := Directory{ NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName), NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName), @@ -109,8 +127,7 @@ func TestHandler_GetDirectory(t *testing.T) { prov.RequireEAB = true provName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := acme.NewProvisionerContext(context.Background(), prov) expDir := Directory{ NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName), NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName), @@ -131,9 +148,9 @@ func TestHandler_GetDirectory(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{linker: linker} + ctx := acme.NewLinkerContext(tc.ctx, acme.NewLinker("test.ca.smallstep.com", "acme")) req := httptest.NewRequest("GET", "/foo/bar", nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() GetDirectory(w, req) res := w.Result() @@ -220,7 +237,7 @@ func TestHandler_GetAuthorization(t *testing.T) { } }, "fail/nil-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, nil) return test{ db: &acme.MockDB{}, @@ -286,10 +303,9 @@ func TestHandler_GetAuthorization(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ db: &acme.MockDB{ MockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) { @@ -305,9 +321,9 @@ func TestHandler_GetAuthorization(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil) req := httptest.NewRequest("GET", "/foo/bar", nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() GetAuthorization(w, req) res := w.Result() @@ -448,9 +464,9 @@ func TestHandler_GetCertificate(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db} + ctx := acme.NewDatabaseContext(tc.ctx, tc.db) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() GetCertificate(w, req) res := w.Result() @@ -492,7 +508,7 @@ func TestHandler_GetChallenge(t *testing.T) { type test struct { db acme.DB - vco *acme.ValidateChallengeOptions + vc acme.Client ctx context.Context statusCode int ch *acme.Challenge @@ -501,6 +517,7 @@ func TestHandler_GetChallenge(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-account": func(t *testing.T) test { return test{ + db: &acme.MockDB{}, ctx: context.Background(), statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), @@ -508,6 +525,7 @@ func TestHandler_GetChallenge(t *testing.T) { }, "fail/nil-account": func(t *testing.T) test { return test{ + db: &acme.MockDB{}, ctx: context.WithValue(context.Background(), accContextKey, nil), statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), @@ -517,6 +535,7 @@ func TestHandler_GetChallenge(t *testing.T) { acc := &acme.Account{ID: "accID"} ctx := context.WithValue(context.Background(), accContextKey, acc) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload expected in request context"), @@ -524,10 +543,11 @@ func TestHandler_GetChallenge(t *testing.T) { }, "fail/nil-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload expected in request context"), @@ -535,7 +555,7 @@ func TestHandler_GetChallenge(t *testing.T) { }, "fail/db.GetChallenge-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -554,7 +574,7 @@ func TestHandler_GetChallenge(t *testing.T) { }, "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -573,7 +593,7 @@ func TestHandler_GetChallenge(t *testing.T) { }, "fail/no-jwk": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -592,7 +612,7 @@ func TestHandler_GetChallenge(t *testing.T) { }, "fail/nil-jwk": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) ctx = context.WithValue(ctx, jwkContextKey, nil) @@ -612,7 +632,7 @@ func TestHandler_GetChallenge(t *testing.T) { }, "fail/validate-challenge-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) _jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) @@ -640,8 +660,8 @@ func TestHandler_GetChallenge(t *testing.T) { return acme.NewErrorISE("force") }, }, - vco: &acme.ValidateChallengeOptions{ - HTTPGet: func(string) (*http.Response, error) { + vc: &mockClient{ + get: func(string) (*http.Response, error) { return nil, errors.New("force") }, }, @@ -652,14 +672,13 @@ func TestHandler_GetChallenge(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) _jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) _pub := _jwk.Public() ctx = context.WithValue(ctx, jwkContextKey, &_pub) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -691,8 +710,8 @@ func TestHandler_GetChallenge(t *testing.T) { URL: u, Error: acme.NewError(acme.ErrorConnectionType, "force"), }, - vco: &acme.ValidateChallengeOptions{ - HTTPGet: func(string) (*http.Response, error) { + vc: &mockClient{ + get: func(string) (*http.Response, error) { return nil, errors.New("force") }, }, @@ -704,9 +723,9 @@ func TestHandler_GetChallenge(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db, linker: NewLinker("dns", "acme"), validateChallengeOptions: tc.vco} + ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() GetChallenge(w, req) res := w.Result() diff --git a/acme/api/middleware.go b/acme/api/middleware.go index 09e88b8d..a254a83b 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -9,7 +9,6 @@ import ( "net/url" "strings" - "github.com/go-chi/chi" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" @@ -63,7 +62,12 @@ func addDirLink(next nextHTTP) nextHTTP { // application/jose+json. func verifyContentType(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - p := acme.MustProvisionerFromContext(r.Context()) + p, err := provisionerFromContext(r.Context()) + if err != nil { + render.Error(w, err) + return + } + u := &url.URL{ Path: acme.GetUnescapedPathSuffix(acme.CertificateLinkType, p.GetName(), ""), } @@ -260,32 +264,6 @@ func extractJWK(next nextHTTP) nextHTTP { } } -// lookupProvisioner loads the provisioner associated with the request. -// Responds 404 if the provisioner does not exist. -func lookupProvisioner(next nextHTTP) nextHTTP { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - nameEscaped := chi.URLParam(r, "provisionerID") - name, err := url.PathUnescape(nameEscaped) - if err != nil { - render.Error(w, acme.WrapErrorISE(err, "error url unescaping provisioner name '%s'", nameEscaped)) - return - } - p, err := mustAuthority(r.Context()).LoadProvisionerByName(name) - if err != nil { - render.Error(w, err) - return - } - acmeProv, ok := p.(*provisioner.ACME) - if !ok { - render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType, "provisioner must be of type ACME")) - return - } - ctx = context.WithValue(ctx, provisionerContextKey, acme.Provisioner(acmeProv)) - next(w, r.WithContext(ctx)) - } -} - // checkPrerequisites checks if all prerequisites for serving ACME // are met by the CA configuration. func checkPrerequisites(next nextHTTP) nextHTTP { @@ -446,16 +424,12 @@ type ContextKey string const ( // accContextKey account key accContextKey = ContextKey("acc") - // baseURLContextKey baseURL key - baseURLContextKey = ContextKey("baseURL") // jwsContextKey jws key jwsContextKey = ContextKey("jws") // jwkContextKey jwk key jwkContextKey = ContextKey("jwk") // payloadContextKey payload key payloadContextKey = ContextKey("payload") - // provisionerContextKey provisioner key - provisionerContextKey = ContextKey("provisioner") ) // accountFromContext searches the context for an ACME account. Returns the @@ -468,15 +442,6 @@ func accountFromContext(ctx context.Context) (*acme.Account, error) { return val, nil } -// baseURLFromContext returns the baseURL if one is stored in the context. -func baseURLFromContext(ctx context.Context) *url.URL { - val, ok := ctx.Value(baseURLContextKey).(*url.URL) - if !ok || val == nil { - return nil - } - return val -} - // jwkFromContext searches the context for a JWK. Returns the JWK or an error. func jwkFromContext(ctx context.Context) (*jose.JSONWebKey, error) { val, ok := ctx.Value(jwkContextKey).(*jose.JSONWebKey) @@ -495,14 +460,29 @@ func jwsFromContext(ctx context.Context) (*jose.JSONWebSignature, error) { return val, nil } +// provisionerFromContext searches the context for a provisioner. Returns the +// provisioner or an error. +func provisionerFromContext(ctx context.Context) (acme.Provisioner, error) { + p, ok := acme.ProvisionerFromContext(ctx) + if !ok || p == nil { + return nil, acme.NewErrorISE("provisioner expected in request context") + } + return p, nil +} + // acmeProvisionerFromContext searches the context for an ACME provisioner. Returns // pointer to an ACME provisioner or an error. func acmeProvisionerFromContext(ctx context.Context) (*provisioner.ACME, error) { - p, ok := acme.MustProvisionerFromContext(ctx).(*provisioner.ACME) + p, err := provisionerFromContext(ctx) + if err != nil { + return nil, err + } + ap, ok := p.(*provisioner.ACME) if !ok { return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner") } - return p, nil + + return ap, nil } // payloadFromContext searches the context for a payload. Returns the payload diff --git a/acme/api/middleware_test.go b/acme/api/middleware_test.go index f192e67e..39a696ae 100644 --- a/acme/api/middleware_test.go +++ b/acme/api/middleware_test.go @@ -27,83 +27,18 @@ func testNext(w http.ResponseWriter, r *http.Request) { w.Write(testBody) } -func Test_baseURLFromRequest(t *testing.T) { - tests := []struct { - name string - targetURL string - expectedResult *url.URL - requestPreparer func(*http.Request) - }{ - { - "HTTPS host pass-through failed.", - "https://my.dummy.host", - &url.URL{Scheme: "https", Host: "my.dummy.host"}, - nil, - }, - { - "Port pass-through failed", - "https://host.with.port:8080", - &url.URL{Scheme: "https", Host: "host.with.port:8080"}, - nil, - }, - { - "Explicit host from Request.Host was not used.", - "https://some.target.host:8080", - &url.URL{Scheme: "https", Host: "proxied.host"}, - func(r *http.Request) { - r.Host = "proxied.host" - }, - }, - { - "Missing Request.Host value did not result in empty string result.", - "https://some.host", - nil, - func(r *http.Request) { - r.Host = "" - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - request := httptest.NewRequest("GET", tc.targetURL, nil) - if tc.requestPreparer != nil { - tc.requestPreparer(request) - } - result := getBaseURLFromRequest(request) - if result == nil || tc.expectedResult == nil { - assert.Equals(t, result, tc.expectedResult) - } else if result.String() != tc.expectedResult.String() { - t.Errorf("Expected %q, but got %q", tc.expectedResult.String(), result.String()) - } - }) - } -} - -func TestHandler_baseURLFromRequest(t *testing.T) { - // h := &Handler{} - req := httptest.NewRequest("GET", "/foo", nil) - req.Host = "test.ca.smallstep.com:8080" - w := httptest.NewRecorder() - - next := func(w http.ResponseWriter, r *http.Request) { - bu := baseURLFromContext(r.Context()) - if assert.NotNil(t, bu) { - assert.Equals(t, bu.Host, "test.ca.smallstep.com:8080") - assert.Equals(t, bu.Scheme, "https") +func newBaseContext(ctx context.Context, args ...interface{}) context.Context { + for _, a := range args { + switch v := a.(type) { + case acme.DB: + ctx = acme.NewDatabaseContext(ctx, v) + case acme.Linker: + ctx = acme.NewLinkerContext(ctx, v) + case acme.PrerequisitesChecker: + ctx = acme.NewPrerequisitesCheckerContext(ctx, v) } } - - baseURLFromRequest(next)(w, req) - - req = httptest.NewRequest("GET", "/foo", nil) - req.Host = "" - - next = func(w http.ResponseWriter, r *http.Request) { - assert.Equals(t, baseURLFromContext(r.Context()), nil) - } - - baseURLFromRequest(next)(w, req) + return ctx } func TestHandler_addNonce(t *testing.T) { @@ -139,8 +74,8 @@ func TestHandler_addNonce(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db} - req := httptest.NewRequest("GET", u, nil) + ctx := newBaseContext(context.Background(), tc.db) + req := httptest.NewRequest("GET", u, nil).WithContext(ctx) w := httptest.NewRecorder() addNonce(testNext)(w, req) res := w.Result() @@ -175,17 +110,15 @@ func TestHandler_addDirLink(t *testing.T) { baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} type test struct { link string - linker Linker statusCode int ctx context.Context err *acme.Error } var tests = map[string]func(t *testing.T) test{ "ok": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := acme.NewProvisionerContext(context.Background(), prov) + ctx = acme.NewLinkerContext(ctx, acme.NewLinker("test.ca.smallstep.com", "acme")) return test{ - linker: NewLinker("dns", "acme"), ctx: ctx, link: fmt.Sprintf("%s/acme/%s/directory", baseURL.String(), provName), statusCode: 200, @@ -195,7 +128,6 @@ func TestHandler_addDirLink(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{linker: tc.linker} req := httptest.NewRequest("GET", "/foo", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() @@ -231,7 +163,6 @@ func TestHandler_verifyContentType(t *testing.T) { baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} u := fmt.Sprintf("%s/acme/%s/certificate/abc123", baseURL.String(), escProvName) type test struct { - h Handler ctx context.Context contentType string err *acme.Error @@ -241,9 +172,6 @@ func TestHandler_verifyContentType(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/provisioner-not-set": func(t *testing.T) test { return test{ - h: Handler{ - // linker: NewLinker("dns", "acme"), - }, url: u, ctx: context.Background(), contentType: "foo", @@ -253,11 +181,8 @@ func TestHandler_verifyContentType(t *testing.T) { }, "fail/general-bad-content-type": func(t *testing.T) test { return test{ - h: Handler{ - // linker: NewLinker("dns", "acme"), - }, url: u, - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + ctx: acme.NewProvisionerContext(context.Background(), prov), contentType: "foo", statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "expected content-type to be in [application/jose+json], but got foo"), @@ -265,10 +190,7 @@ func TestHandler_verifyContentType(t *testing.T) { }, "fail/certificate-bad-content-type": func(t *testing.T) test { return test{ - h: Handler{ - // linker: NewLinker("dns", "acme"), - }, - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + ctx: acme.NewProvisionerContext(context.Background(), prov), contentType: "foo", statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "expected content-type to be in [application/jose+json application/pkix-cert application/pkcs7-mime], but got foo"), @@ -276,40 +198,28 @@ func TestHandler_verifyContentType(t *testing.T) { }, "ok": func(t *testing.T) test { return test{ - h: Handler{ - // linker: NewLinker("dns", "acme"), - }, - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + ctx: acme.NewProvisionerContext(context.Background(), prov), contentType: "application/jose+json", statusCode: 200, } }, "ok/certificate/pkix-cert": func(t *testing.T) test { return test{ - h: Handler{ - // linker: NewLinker("dns", "acme"), - }, - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + ctx: acme.NewProvisionerContext(context.Background(), prov), contentType: "application/pkix-cert", statusCode: 200, } }, "ok/certificate/jose+json": func(t *testing.T) test { return test{ - h: Handler{ - // linker: NewLinker("dns", "acme"), - }, - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + ctx: acme.NewProvisionerContext(context.Background(), prov), contentType: "application/jose+json", statusCode: 200, } }, "ok/certificate/pkcs7-mime": func(t *testing.T) test { return test{ - h: Handler{ - // linker: NewLinker("dns", "acme"), - }, - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + ctx: acme.NewProvisionerContext(context.Background(), prov), contentType: "application/pkcs7-mime", statusCode: 200, } @@ -733,7 +643,7 @@ func TestHandler_lookupJWK(t *testing.T) { parsedJWS, err := jose.ParseJWS(raw) assert.FatalError(t, err) type test struct { - linker Linker + linker acme.Linker db acme.DB ctx context.Context next func(http.ResponseWriter, *http.Request) @@ -743,15 +653,19 @@ func TestHandler_lookupJWK(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-jws": func(t *testing.T) test { return test{ - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + db: &acme.MockDB{}, + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), + ctx: acme.NewProvisionerContext(context.Background(), prov), statusCode: 500, err: acme.NewErrorISE("jws expected in request context"), } }, "fail/nil-jws": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, nil) return test{ + db: &acme.MockDB{}, + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), ctx: ctx, statusCode: 500, err: acme.NewErrorISE("jws expected in request context"), @@ -765,11 +679,11 @@ func TestHandler_lookupJWK(t *testing.T) { assert.FatalError(t, err) _jws, err := _signer.Sign([]byte("baz")) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, _jws) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ - linker: NewLinker("dns", "acme"), + db: &acme.MockDB{}, + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "kid does not have required prefix; expected %s, but got ", prefix), @@ -789,22 +703,21 @@ func TestHandler_lookupJWK(t *testing.T) { assert.FatalError(t, err) _parsed, err := jose.ParseJWS(_raw) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, _parsed) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ - linker: NewLinker("dns", "acme"), + db: &acme.MockDB{}, + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "kid does not have required prefix; expected %s, but got foo", prefix), } }, "fail/account-not-found": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ - linker: NewLinker("dns", "acme"), + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), db: &acme.MockDB{ MockGetAccount: func(ctx context.Context, accID string) (*acme.Account, error) { assert.Equals(t, accID, accID) @@ -817,11 +730,10 @@ func TestHandler_lookupJWK(t *testing.T) { } }, "fail/GetAccount-error": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ - linker: NewLinker("dns", "acme"), + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), db: &acme.MockDB{ MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) { assert.Equals(t, id, accID) @@ -835,11 +747,10 @@ func TestHandler_lookupJWK(t *testing.T) { }, "fail/account-not-valid": func(t *testing.T) test { acc := &acme.Account{Status: "deactivated"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ - linker: NewLinker("dns", "acme"), + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), db: &acme.MockDB{ MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) { assert.Equals(t, id, accID) @@ -853,11 +764,10 @@ func TestHandler_lookupJWK(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{Status: "valid", Key: jwk} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ - linker: NewLinker("dns", "acme"), + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), db: &acme.MockDB{ MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) { assert.Equals(t, id, accID) @@ -881,9 +791,9 @@ func TestHandler_lookupJWK(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db, linker: tc.linker} + ctx := newBaseContext(tc.ctx, tc.db, tc.linker) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() lookupJWK(tc.next)(w, req) res := w.Result() @@ -945,15 +855,17 @@ func TestHandler_extractJWK(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-jws": func(t *testing.T) test { return test{ - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + db: &acme.MockDB{}, + ctx: acme.NewProvisionerContext(context.Background(), prov), statusCode: 500, err: acme.NewErrorISE("jws expected in request context"), } }, "fail/nil-jws": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("jws expected in request context"), @@ -969,9 +881,10 @@ func TestHandler_extractJWK(t *testing.T) { }, }, } - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, _jws) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "jwk expected in protected header"), @@ -987,16 +900,17 @@ func TestHandler_extractJWK(t *testing.T) { }, }, } - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, _jws) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "invalid jwk in protected header"), } }, "fail/GetAccountByKey-error": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ ctx: ctx, @@ -1012,7 +926,7 @@ func TestHandler_extractJWK(t *testing.T) { }, "fail/account-not-valid": func(t *testing.T) test { acc := &acme.Account{Status: "deactivated"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ ctx: ctx, @@ -1028,7 +942,7 @@ func TestHandler_extractJWK(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{Status: "valid"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ ctx: ctx, @@ -1051,7 +965,7 @@ func TestHandler_extractJWK(t *testing.T) { } }, "ok/no-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ ctx: ctx, @@ -1077,9 +991,9 @@ func TestHandler_extractJWK(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db} + ctx := newBaseContext(tc.ctx, tc.db) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() extractJWK(tc.next)(w, req) res := w.Result() @@ -1118,6 +1032,7 @@ func TestHandler_validateJWS(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-jws": func(t *testing.T) test { return test{ + db: &acme.MockDB{}, ctx: context.Background(), statusCode: 500, err: acme.NewErrorISE("jws expected in request context"), @@ -1125,6 +1040,7 @@ func TestHandler_validateJWS(t *testing.T) { }, "fail/nil-jws": func(t *testing.T) test { return test{ + db: &acme.MockDB{}, ctx: context.WithValue(context.Background(), jwsContextKey, nil), statusCode: 500, err: acme.NewErrorISE("jws expected in request context"), @@ -1132,6 +1048,7 @@ func TestHandler_validateJWS(t *testing.T) { }, "fail/no-signature": func(t *testing.T) test { return test{ + db: &acme.MockDB{}, ctx: context.WithValue(context.Background(), jwsContextKey, &jose.JSONWebSignature{}), statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "request body does not contain a signature"), @@ -1145,6 +1062,7 @@ func TestHandler_validateJWS(t *testing.T) { }, } return test{ + db: &acme.MockDB{}, ctx: context.WithValue(context.Background(), jwsContextKey, jws), statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "request body contains more than one signature"), @@ -1157,6 +1075,7 @@ func TestHandler_validateJWS(t *testing.T) { }, } return test{ + db: &acme.MockDB{}, ctx: context.WithValue(context.Background(), jwsContextKey, jws), statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "unprotected header must not be used"), @@ -1169,6 +1088,7 @@ func TestHandler_validateJWS(t *testing.T) { }, } return test{ + db: &acme.MockDB{}, ctx: context.WithValue(context.Background(), jwsContextKey, jws), statusCode: 400, err: acme.NewError(acme.ErrorBadSignatureAlgorithmType, "unsuitable algorithm: none"), @@ -1181,6 +1101,7 @@ func TestHandler_validateJWS(t *testing.T) { }, } return test{ + db: &acme.MockDB{}, ctx: context.WithValue(context.Background(), jwsContextKey, jws), statusCode: 400, err: acme.NewError(acme.ErrorBadSignatureAlgorithmType, "unsuitable algorithm: %s", jose.HS256), @@ -1444,9 +1365,9 @@ func TestHandler_validateJWS(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db} + ctx := newBaseContext(tc.ctx, tc.db) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() validateJWS(tc.next)(w, req) res := w.Result() @@ -1542,7 +1463,7 @@ func TestHandler_extractOrLookupJWK(t *testing.T) { u := "https://ca.smallstep.com/acme/account" type test struct { db acme.DB - linker Linker + linker acme.Linker statusCode int ctx context.Context err *acme.Error @@ -1570,7 +1491,7 @@ func TestHandler_extractOrLookupJWK(t *testing.T) { parsedJWS, err := jose.ParseJWS(raw) assert.FatalError(t, err) return test{ - linker: NewLinker("dns", "acme"), + linker: acme.NewLinker("dns", "acme"), db: &acme.MockDB{ MockGetAccountByKeyID: func(ctx context.Context, kid string) (*acme.Account, error) { assert.Equals(t, kid, pub.KeyID) @@ -1606,11 +1527,10 @@ func TestHandler_extractOrLookupJWK(t *testing.T) { parsedJWS, err := jose.ParseJWS(raw) assert.FatalError(t, err) acc := &acme.Account{ID: "accID", Key: jwk, Status: "valid"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ - linker: NewLinker("test.ca.smallstep.com", "acme"), + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), db: &acme.MockDB{ MockGetAccount: func(ctx context.Context, accID string) (*acme.Account, error) { assert.Equals(t, accID, acc.ID) @@ -1628,9 +1548,9 @@ func TestHandler_extractOrLookupJWK(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: tc.db, linker: tc.linker} + ctx := newBaseContext(tc.ctx, tc.db, tc.linker) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() extractOrLookupJWK(tc.next)(w, req) res := w.Result() @@ -1664,7 +1584,7 @@ func TestHandler_checkPrerequisites(t *testing.T) { u := fmt.Sprintf("%s/acme/%s/account/1234", baseURL, provName) type test struct { - linker Linker + linker acme.Linker ctx context.Context prerequisitesChecker func(context.Context) (bool, error) next func(http.ResponseWriter, *http.Request) @@ -1673,10 +1593,9 @@ func TestHandler_checkPrerequisites(t *testing.T) { } var tests = map[string]func(t *testing.T) test{ "fail/error": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := acme.NewProvisionerContext(context.Background(), prov) return test{ - linker: NewLinker("dns", "acme"), + linker: acme.NewLinker("dns", "acme"), ctx: ctx, prerequisitesChecker: func(context.Context) (bool, error) { return false, errors.New("force") }, next: func(w http.ResponseWriter, r *http.Request) { @@ -1687,10 +1606,9 @@ func TestHandler_checkPrerequisites(t *testing.T) { } }, "fail/prerequisites-nok": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := acme.NewProvisionerContext(context.Background(), prov) return test{ - linker: NewLinker("dns", "acme"), + linker: acme.NewLinker("dns", "acme"), ctx: ctx, prerequisitesChecker: func(context.Context) (bool, error) { return false, nil }, next: func(w http.ResponseWriter, r *http.Request) { @@ -1701,10 +1619,9 @@ func TestHandler_checkPrerequisites(t *testing.T) { } }, "ok": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := acme.NewProvisionerContext(context.Background(), prov) return test{ - linker: NewLinker("dns", "acme"), + linker: acme.NewLinker("dns", "acme"), ctx: ctx, prerequisitesChecker: func(context.Context) (bool, error) { return true, nil }, next: func(w http.ResponseWriter, r *http.Request) { diff --git a/acme/api/order.go b/acme/api/order.go index 2b9f912e..08718977 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -72,13 +72,17 @@ func NewOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() db := acme.MustDatabaseFromContext(ctx) linker := acme.MustLinkerFromContext(ctx) - prov := acme.MustProvisionerFromContext(ctx) acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } + prov, err := provisionerFromContext(ctx) + if err != nil { + render.Error(w, err) + return + } payload, err := payloadFromContext(ctx) if err != nil { render.Error(w, err) @@ -189,13 +193,17 @@ func GetOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() db := acme.MustDatabaseFromContext(ctx) linker := acme.MustLinkerFromContext(ctx) - prov := acme.MustProvisionerFromContext(ctx) acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } + prov, err := provisionerFromContext(ctx) + if err != nil { + render.Error(w, err) + return + } o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID")) if err != nil { @@ -228,13 +236,17 @@ func FinalizeOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() db := acme.MustDatabaseFromContext(ctx) linker := acme.MustLinkerFromContext(ctx) - prov := acme.MustProvisionerFromContext(ctx) acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } + prov, err := provisionerFromContext(ctx) + if err != nil { + render.Error(w, err) + return + } payload, err := payloadFromContext(ctx) if err != nil { render.Error(w, err) diff --git a/acme/api/order_test.go b/acme/api/order_test.go index f0a2d1d4..0ab76778 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -276,15 +276,17 @@ func TestHandler_GetOrder(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-account": func(t *testing.T) test { return test{ - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + db: &acme.MockDB{}, + ctx: acme.NewProvisionerContext(context.Background(), prov), statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), } }, "fail/nil-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), @@ -294,6 +296,7 @@ func TestHandler_GetOrder(t *testing.T) { acc := &acme.Account{ID: "accountID"} ctx := context.WithValue(context.Background(), accContextKey, acc) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("provisioner does not exist"), @@ -301,9 +304,10 @@ func TestHandler_GetOrder(t *testing.T) { }, "fail/nil-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, nil) + ctx := acme.NewProvisionerContext(context.Background(), nil) ctx = context.WithValue(ctx, accContextKey, acc) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("provisioner does not exist"), @@ -311,7 +315,7 @@ func TestHandler_GetOrder(t *testing.T) { }, "fail/db.GetOrder-error": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -325,7 +329,7 @@ func TestHandler_GetOrder(t *testing.T) { }, "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -341,7 +345,7 @@ func TestHandler_GetOrder(t *testing.T) { }, "fail/provisioner-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -357,7 +361,7 @@ func TestHandler_GetOrder(t *testing.T) { }, "fail/order-update-error": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -381,10 +385,9 @@ func TestHandler_GetOrder(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ db: &acme.MockDB{ MockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) { @@ -421,9 +424,9 @@ func TestHandler_GetOrder(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} + ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme")) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() GetOrder(w, req) res := w.Result() @@ -636,8 +639,8 @@ func TestHandler_newAuthorization(t *testing.T) { for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) - // h := &Handler{db: tc.db} - if err := newAuthorization(context.Background(), tc.az); err != nil { + ctx := newBaseContext(context.Background(), tc.db) + if err := newAuthorization(ctx, tc.az); err != nil { if assert.NotNil(t, tc.err) { switch k := err.(type) { case *acme.Error: @@ -677,15 +680,17 @@ func TestHandler_NewOrder(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-account": func(t *testing.T) test { return test{ - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + db: &acme.MockDB{}, + ctx: acme.NewProvisionerContext(context.Background(), prov), statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), } }, "fail/nil-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), @@ -695,6 +700,7 @@ func TestHandler_NewOrder(t *testing.T) { acc := &acme.Account{ID: "accountID"} ctx := context.WithValue(context.Background(), accContextKey, acc) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("provisioner does not exist"), @@ -702,9 +708,10 @@ func TestHandler_NewOrder(t *testing.T) { }, "fail/nil-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, nil) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("provisioner does not exist"), @@ -713,8 +720,9 @@ func TestHandler_NewOrder(t *testing.T) { "fail/no-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := context.WithValue(context.Background(), accContextKey, acc) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload does not exist"), @@ -722,10 +730,11 @@ func TestHandler_NewOrder(t *testing.T) { }, "fail/nil-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("paylod does not exist"), @@ -733,10 +742,11 @@ func TestHandler_NewOrder(t *testing.T) { }, "fail/unmarshal-payload-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "failed to unmarshal new-order request payload: unexpected end of JSON input"), @@ -747,10 +757,11 @@ func TestHandler_NewOrder(t *testing.T) { fr := &NewOrderRequest{} b, err := json.Marshal(fr) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty"), @@ -765,7 +776,7 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(fr) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ @@ -793,7 +804,7 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(fr) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) var ( @@ -863,10 +874,9 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(nor) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) var ( ch1, ch2, ch3, ch4 **acme.Challenge az1ID, az2ID *string @@ -978,10 +988,9 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(nor) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) 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 @@ -1070,10 +1079,9 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(nor) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) 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 @@ -1161,10 +1169,9 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(nor) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) 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 @@ -1253,10 +1260,9 @@ func TestHandler_NewOrder(t *testing.T) { } b, err := json.Marshal(nor) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) 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 @@ -1334,9 +1340,9 @@ 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} + ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme")) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() NewOrder(w, req) res := w.Result() @@ -1371,6 +1377,7 @@ func TestHandler_NewOrder(t *testing.T) { } func TestHandler_FinalizeOrder(t *testing.T) { + mockMustAuthority(t, &mockCA{}) prov := newProv() escProvName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} @@ -1429,15 +1436,17 @@ func TestHandler_FinalizeOrder(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-account": func(t *testing.T) test { return test{ - ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + db: &acme.MockDB{}, + ctx: acme.NewProvisionerContext(context.Background(), prov), statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), } }, "fail/nil-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), @@ -1447,6 +1456,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { acc := &acme.Account{ID: "accountID"} ctx := context.WithValue(context.Background(), accContextKey, acc) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("provisioner does not exist"), @@ -1454,9 +1464,10 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/nil-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, nil) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("provisioner does not exist"), @@ -1465,8 +1476,9 @@ func TestHandler_FinalizeOrder(t *testing.T) { "fail/no-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := context.WithValue(context.Background(), accContextKey, acc) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload does not exist"), @@ -1474,10 +1486,11 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/nil-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("paylod does not exist"), @@ -1485,10 +1498,11 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/unmarshal-payload-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "failed to unmarshal finalize-order request payload: unexpected end of JSON input"), @@ -1499,10 +1513,11 @@ func TestHandler_FinalizeOrder(t *testing.T) { fr := &FinalizeRequest{} b, err := json.Marshal(fr) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: acme.NewError(acme.ErrorMalformedType, "unable to parse csr: asn1: syntax error: sequence truncated"), @@ -1511,7 +1526,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { "fail/db.GetOrder-error": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -1526,7 +1541,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -1543,7 +1558,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/provisioner-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -1560,7 +1575,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/order-finalize-error": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -1585,10 +1600,9 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -1624,9 +1638,9 @@ func TestHandler_FinalizeOrder(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} + ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme")) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() FinalizeOrder(w, req) res := w.Result() diff --git a/acme/api/revoke.go b/acme/api/revoke.go index 584ed27e..a8b98f3f 100644 --- a/acme/api/revoke.go +++ b/acme/api/revoke.go @@ -30,7 +30,6 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) { ctx := r.Context() db := acme.MustDatabaseFromContext(ctx) linker := acme.MustLinkerFromContext(ctx) - prov := acme.MustProvisionerFromContext(ctx) jws, err := jwsFromContext(ctx) if err != nil { @@ -38,6 +37,12 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) { return } + prov, err := provisionerFromContext(ctx) + if err != nil { + render.Error(w, err) + return + } + payload, err := payloadFromContext(ctx) if err != nil { render.Error(w, err) diff --git a/acme/api/revoke_test.go b/acme/api/revoke_test.go index 3a0ba70d..c746c11b 100644 --- a/acme/api/revoke_test.go +++ b/acme/api/revoke_test.go @@ -511,6 +511,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/no-jws": func(t *testing.T) test { ctx := context.Background() return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("jws expected in request context"), @@ -519,6 +520,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/nil-jws": func(t *testing.T) test { ctx := context.WithValue(context.Background(), jwsContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("jws expected in request context"), @@ -527,6 +529,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/no-provisioner": func(t *testing.T) test { ctx := context.WithValue(context.Background(), jwsContextKey, jws) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("provisioner does not exist"), @@ -534,8 +537,9 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/nil-provisioner": func(t *testing.T) test { ctx := context.WithValue(context.Background(), jwsContextKey, jws) - ctx = context.WithValue(ctx, provisionerContextKey, nil) + ctx = acme.NewProvisionerContext(ctx, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("provisioner does not exist"), @@ -543,8 +547,9 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/no-payload": func(t *testing.T) test { ctx := context.WithValue(context.Background(), jwsContextKey, jws) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload does not exist"), @@ -552,9 +557,10 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/nil-payload": func(t *testing.T) test { ctx := context.WithValue(context.Background(), jwsContextKey, jws) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("payload does not exist"), @@ -563,9 +569,10 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/unmarshal-payload": func(t *testing.T) test { malformedPayload := []byte(`{"payload":malformed?}`) ctx := context.WithValue(context.Background(), jwsContextKey, jws) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = acme.NewProvisionerContext(ctx, prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: malformedPayload}) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 500, err: acme.NewErrorISE("error unmarshaling payload"), @@ -577,10 +584,11 @@ func TestHandler_RevokeCert(t *testing.T) { } wronglyEncodedPayloadBytes, err := json.Marshal(wrongPayload) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: wronglyEncodedPayloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: &acme.Error{ @@ -596,10 +604,11 @@ func TestHandler_RevokeCert(t *testing.T) { } emptyPayloadBytes, err := json.Marshal(emptyPayload) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: emptyPayloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) return test{ + db: &acme.MockDB{}, ctx: ctx, statusCode: 400, err: &acme.Error{ @@ -610,7 +619,7 @@ func TestHandler_RevokeCert(t *testing.T) { } }, "fail/db.GetCertificateBySerial": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) db := &acme.MockDB{ @@ -628,7 +637,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/different-certificate-contents": func(t *testing.T) test { aDifferentCert, _, err := generateCertKeyPair() assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) db := &acme.MockDB{ @@ -647,7 +656,7 @@ func TestHandler_RevokeCert(t *testing.T) { } }, "fail/no-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) db := &acme.MockDB{ @@ -666,7 +675,7 @@ func TestHandler_RevokeCert(t *testing.T) { } }, "fail/nil-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) ctx = context.WithValue(ctx, accContextKey, nil) @@ -687,11 +696,10 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/account-not-valid": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusInvalid} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) db := &acme.MockDB{ MockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) { @@ -717,11 +725,10 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/account-not-authorized": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) db := &acme.MockDB{ MockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) { @@ -771,10 +778,9 @@ func TestHandler_RevokeCert(t *testing.T) { assert.FatalError(t, err) unauthorizedPayloadBytes, err := json.Marshal(jwsPayload) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: unauthorizedPayloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) db := &acme.MockDB{ MockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) { @@ -798,11 +804,10 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/certificate-revoked-check-fails": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) db := &acme.MockDB{ MockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) { @@ -832,7 +837,7 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/certificate-already-revoked": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) @@ -870,7 +875,7 @@ func TestHandler_RevokeCert(t *testing.T) { invalidReasonCodePayloadBytes, err := json.Marshal(invalidReasonPayload) assert.FatalError(t, err) acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: invalidReasonCodePayloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) @@ -908,7 +913,7 @@ func TestHandler_RevokeCert(t *testing.T) { }, } acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} - ctx := context.WithValue(context.Background(), provisionerContextKey, mockACMEProv) + ctx := acme.NewProvisionerContext(context.Background(), mockACMEProv) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) @@ -940,7 +945,7 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/ca.Revoke": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) @@ -972,7 +977,7 @@ func TestHandler_RevokeCert(t *testing.T) { }, "fail/ca.Revoke-already-revoked": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) @@ -1003,11 +1008,10 @@ func TestHandler_RevokeCert(t *testing.T) { }, "ok/using-account-key": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) db := &acme.MockDB{ MockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) { @@ -1031,10 +1035,9 @@ func TestHandler_RevokeCert(t *testing.T) { assert.FatalError(t, err) jws, err := jose.ParseJWS(string(jwsBytes)) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) db := &acme.MockDB{ MockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) { @@ -1057,9 +1060,10 @@ func TestHandler_RevokeCert(t *testing.T) { for name, setup := range tests { tc := setup(t) t.Run(name, func(t *testing.T) { - // h := &Handler{linker: NewLinker("dns", "acme"), db: tc.db, ca: tc.ca} + ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme")) + mockMustAuthority(t, tc.ca) req := httptest.NewRequest("POST", revokeURL, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() RevokeCert(w, req) res := w.Result() From ba499eeb2ad26f66d5bf0b45f0af12478dec4573 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 2 May 2022 17:40:10 -0700 Subject: [PATCH 176/241] Fix acme/api tests. --- acme/api/middleware_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/api/middleware_test.go b/acme/api/middleware_test.go index 39a696ae..193f5347 100644 --- a/acme/api/middleware_test.go +++ b/acme/api/middleware_test.go @@ -1634,9 +1634,9 @@ func TestHandler_checkPrerequisites(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - // h := &Handler{db: nil, linker: tc.linker, prerequisitesChecker: tc.prerequisitesChecker} + ctx := acme.NewPrerequisitesCheckerContext(tc.ctx, tc.prerequisitesChecker) req := httptest.NewRequest("GET", u, nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() checkPrerequisites(tc.next)(w, req) res := w.Result() From 2ab7dc6f9d2278fabc926404b0f9781f9284d323 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 2 May 2022 18:09:26 -0700 Subject: [PATCH 177/241] Fix acme tests. --- acme/challenge_test.go | 222 ++++++++++++++++++++++------------------- acme/linker.go | 8 +- acme/linker_test.go | 72 ++++++++----- 3 files changed, 171 insertions(+), 131 deletions(-) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index c05b25e7..e1b6816a 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -29,6 +29,18 @@ import ( "github.com/smallstep/assert" ) +type mockClient struct { + get func(url string) (*http.Response, error) + lookupTxt func(name string) ([]string, error) + tlsDial func(network, addr string, config *tls.Config) (*tls.Conn, error) +} + +func (m *mockClient) Get(url string) (*http.Response, error) { return m.get(url) } +func (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) } +func (m *mockClient) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) { + return m.tlsDial(network, addr, config) +} + func Test_storeError(t *testing.T) { type test struct { ch *Challenge @@ -229,7 +241,7 @@ func TestKeyAuthorization(t *testing.T) { func TestChallenge_Validate(t *testing.T) { type test struct { ch *Challenge - vo *ValidateChallengeOptions + vc Client jwk *jose.JSONWebKey db DB srv *httptest.Server @@ -273,8 +285,8 @@ func TestChallenge_Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return nil, errors.New("force") }, }, @@ -309,8 +321,8 @@ func TestChallenge_Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return nil, errors.New("force") }, }, @@ -344,8 +356,8 @@ func TestChallenge_Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return nil, errors.New("force") }, }, @@ -381,8 +393,8 @@ func TestChallenge_Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return nil, errors.New("force") }, }, @@ -416,8 +428,8 @@ func TestChallenge_Validate(t *testing.T) { } return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + vc: &mockClient{ + tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { return nil, errors.New("force") }, }, @@ -466,8 +478,8 @@ func TestChallenge_Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -493,7 +505,8 @@ func TestChallenge_Validate(t *testing.T) { defer tc.srv.Close() } - if err := tc.ch.Validate(context.Background(), tc.db, tc.jwk, tc.vo); err != nil { + ctx := NewClientContext(context.Background(), tc.vc) + if err := tc.ch.Validate(ctx, tc.db, tc.jwk); err != nil { if assert.NotNil(t, tc.err) { switch k := err.(type) { case *Error: @@ -524,7 +537,7 @@ func (errReader) Close() error { func TestHTTP01Validate(t *testing.T) { type test struct { - vo *ValidateChallengeOptions + vc Client ch *Challenge jwk *jose.JSONWebKey db DB @@ -541,8 +554,8 @@ func TestHTTP01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return nil, errors.New("force") }, }, @@ -575,8 +588,8 @@ func TestHTTP01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return nil, errors.New("force") }, }, @@ -608,8 +621,8 @@ func TestHTTP01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusBadRequest, Body: errReader(0), @@ -645,8 +658,8 @@ func TestHTTP01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusBadRequest, Body: errReader(0), @@ -681,8 +694,8 @@ func TestHTTP01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return &http.Response{ Body: errReader(0), }, nil @@ -704,8 +717,8 @@ func TestHTTP01Validate(t *testing.T) { jwk.Key = "foo" return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return &http.Response{ Body: io.NopCloser(bytes.NewBufferString("foo")), }, nil @@ -730,8 +743,8 @@ func TestHTTP01Validate(t *testing.T) { assert.FatalError(t, err) return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return &http.Response{ Body: io.NopCloser(bytes.NewBufferString("foo")), }, nil @@ -772,8 +785,8 @@ func TestHTTP01Validate(t *testing.T) { assert.FatalError(t, err) return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return &http.Response{ Body: io.NopCloser(bytes.NewBufferString("foo")), }, nil @@ -815,8 +828,8 @@ func TestHTTP01Validate(t *testing.T) { assert.FatalError(t, err) return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return &http.Response{ Body: io.NopCloser(bytes.NewBufferString(expKeyAuth)), }, nil @@ -857,8 +870,8 @@ func TestHTTP01Validate(t *testing.T) { assert.FatalError(t, err) return test{ ch: ch, - vo: &ValidateChallengeOptions{ - HTTPGet: func(url string) (*http.Response, error) { + vc: &mockClient{ + get: func(url string) (*http.Response, error) { return &http.Response{ Body: io.NopCloser(bytes.NewBufferString(expKeyAuth)), }, nil @@ -887,7 +900,8 @@ func TestHTTP01Validate(t *testing.T) { for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) - if err := http01Validate(context.Background(), tc.ch, tc.db, tc.jwk, tc.vo); err != nil { + ctx := NewClientContext(context.Background(), tc.vc) + if err := http01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil { if assert.NotNil(t, tc.err) { switch k := err.(type) { case *Error: @@ -911,7 +925,7 @@ func TestDNS01Validate(t *testing.T) { fulldomain := "*.zap.internal" domain := strings.TrimPrefix(fulldomain, "*.") type test struct { - vo *ValidateChallengeOptions + vc Client ch *Challenge jwk *jose.JSONWebKey db DB @@ -928,8 +942,8 @@ func TestDNS01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return nil, errors.New("force") }, }, @@ -963,8 +977,8 @@ func TestDNS01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return nil, errors.New("force") }, }, @@ -1001,8 +1015,8 @@ func TestDNS01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return []string{"foo"}, nil }, }, @@ -1026,8 +1040,8 @@ func TestDNS01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return []string{"foo", "bar"}, nil }, }, @@ -1068,8 +1082,8 @@ func TestDNS01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return []string{"foo", "bar"}, nil }, }, @@ -1111,8 +1125,8 @@ func TestDNS01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return []string{"foo", expected}, nil }, }, @@ -1156,8 +1170,8 @@ func TestDNS01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - LookupTxt: func(url string) ([]string, error) { + vc: &mockClient{ + lookupTxt: func(url string) ([]string, error) { return []string{"foo", expected}, nil }, }, @@ -1186,7 +1200,8 @@ func TestDNS01Validate(t *testing.T) { for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) - if err := dns01Validate(context.Background(), tc.ch, tc.db, tc.jwk, tc.vo); err != nil { + ctx := NewClientContext(context.Background(), tc.vc) + if err := dns01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil { if assert.NotNil(t, tc.err) { switch k := err.(type) { case *Error: @@ -1206,6 +1221,8 @@ func TestDNS01Validate(t *testing.T) { } } +type tlsDialer func(network, addr string, config *tls.Config) (conn *tls.Conn, err error) + func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tlsDialer) { srv := httptest.NewUnstartedServer(http.NewServeMux()) @@ -1309,7 +1326,7 @@ func TestTLSALPN01Validate(t *testing.T) { } } type test struct { - vo *ValidateChallengeOptions + vc Client ch *Challenge jwk *jose.JSONWebKey db DB @@ -1321,8 +1338,8 @@ func TestTLSALPN01Validate(t *testing.T) { ch := makeTLSCh() return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + vc: &mockClient{ + tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { return nil, errors.New("force") }, }, @@ -1351,8 +1368,8 @@ func TestTLSALPN01Validate(t *testing.T) { ch := makeTLSCh() return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + vc: &mockClient{ + tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { return nil, errors.New("force") }, }, @@ -1384,8 +1401,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1413,8 +1430,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + vc: &mockClient{ + tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { return tls.Client(&noopConn{}, config), nil }, }, @@ -1443,8 +1460,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + vc: &mockClient{ + tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { return tls.Client(&noopConn{}, config), nil }, }, @@ -1479,8 +1496,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + vc: &mockClient{ + tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { return tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", srv.Listener.Addr().String(), config) }, }, @@ -1516,8 +1533,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + vc: &mockClient{ + tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { return tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", srv.Listener.Addr().String(), config) }, }, @@ -1562,8 +1579,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1605,8 +1622,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1649,8 +1666,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1692,8 +1709,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1736,8 +1753,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, srv: srv, jwk: jwk, @@ -1758,8 +1775,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1797,8 +1814,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1841,8 +1858,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1884,8 +1901,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1924,8 +1941,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -1963,8 +1980,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -2008,8 +2025,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -2054,8 +2071,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -2100,8 +2117,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -2144,8 +2161,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -2189,8 +2206,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -2226,8 +2243,8 @@ func TestTLSALPN01Validate(t *testing.T) { return test{ ch: ch, - vo: &ValidateChallengeOptions{ - TLSDial: tlsDial, + vc: &mockClient{ + tlsDial: tlsDial, }, db: &MockDB{ MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { @@ -2253,7 +2270,8 @@ func TestTLSALPN01Validate(t *testing.T) { defer tc.srv.Close() } - if err := tlsalpn01Validate(context.Background(), tc.ch, tc.db, tc.jwk, tc.vo); err != nil { + ctx := NewClientContext(context.Background(), tc.vc) + if err := tlsalpn01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil { if assert.NotNil(t, tc.err) { switch k := err.(type) { case *Error: diff --git a/acme/linker.go b/acme/linker.go index 6e9110c2..bddc21f1 100644 --- a/acme/linker.go +++ b/acme/linker.go @@ -206,6 +206,11 @@ func (l *linker) Middleware(next http.Handler) http.Handler { // GetLink is a helper for GetLinkExplicit. func (l *linker) GetLink(ctx context.Context, typ LinkType, inputs ...string) string { + var name string + if p, ok := ProvisionerFromContext(ctx); ok { + name = p.GetName() + } + var u url.URL if baseURL := baseURLFromContext(ctx); baseURL != nil { u = *baseURL @@ -217,8 +222,7 @@ func (l *linker) GetLink(ctx context.Context, typ LinkType, inputs ...string) st u.Host = l.dns } - p := MustProvisionerFromContext(ctx) - u.Path = l.prefix + GetUnescapedPathSuffix(typ, p.GetName(), inputs...) + u.Path = l.prefix + GetUnescapedPathSuffix(typ, name, inputs...) return u.String() } diff --git a/acme/linker_test.go b/acme/linker_test.go index 1946dd88..b85d1a53 100644 --- a/acme/linker_test.go +++ b/acme/linker_test.go @@ -5,16 +5,34 @@ import ( "fmt" "net/url" "testing" + "time" "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/provisioner" ) -func TestLinker_GetUnescapedPathSuffix(t *testing.T) { - dns := "ca.smallstep.com" - prefix := "acme" - linker := NewLinker(dns, prefix) +func mockProvisioner(t *testing.T) Provisioner { + t.Helper() + var defaultDisableRenewal = false + + // Initialize provisioners + p := &provisioner.ACME{ + Type: "ACME", + Name: "test@acme-provisioner.com", + } + if err := p.Init(provisioner.Config{Claims: provisioner.Claims{ + MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, + MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, + DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, + DisableRenewal: &defaultDisableRenewal, + }}); err != nil { + fmt.Printf("%v", err) + } + return p +} - getPath := linker.GetUnescapedPathSuffix +func TestGetUnescapedPathSuffix(t *testing.T) { + getPath := GetUnescapedPathSuffix assert.Equals(t, getPath(NewNonceLinkType, "{provisionerID}"), "/{provisionerID}/new-nonce") assert.Equals(t, getPath(DirectoryLinkType, "{provisionerID}"), "/{provisionerID}/directory") @@ -31,9 +49,9 @@ func TestLinker_GetUnescapedPathSuffix(t *testing.T) { } func TestLinker_DNS(t *testing.T) { - prov := newProv() + prov := mockProvisioner(t) escProvName := url.PathEscape(prov.GetName()) - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx := NewProvisionerContext(context.Background(), prov) type test struct { name string dns string @@ -116,19 +134,19 @@ func TestLinker_GetLink(t *testing.T) { linker := NewLinker(dns, prefix) id := "1234" - prov := newProv() + prov := mockProvisioner(t) escProvName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx := NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, baseURLKey{}, baseURL) // No provisioner and no BaseURL from request assert.Equals(t, linker.GetLink(context.Background(), NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", "https://ca.smallstep.com", "")) // Provisioner: yes, BaseURL: no - assert.Equals(t, linker.GetLink(context.WithValue(context.Background(), provisionerContextKey, prov), NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", "https://ca.smallstep.com", escProvName)) + assert.Equals(t, linker.GetLink(context.WithValue(context.Background(), provisionerKey{}, prov), NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", "https://ca.smallstep.com", escProvName)) // Provisioner: no, BaseURL: yes - assert.Equals(t, linker.GetLink(context.WithValue(context.Background(), baseURLContextKey, baseURL), NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", "https://test.ca.smallstep.com", "")) + assert.Equals(t, linker.GetLink(context.WithValue(context.Background(), baseURLKey{}, baseURL), NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", "https://test.ca.smallstep.com", "")) assert.Equals(t, linker.GetLink(ctx, NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", baseURL, escProvName)) assert.Equals(t, linker.GetLink(ctx, NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", baseURL, escProvName)) @@ -162,10 +180,10 @@ func TestLinker_GetLink(t *testing.T) { func TestLinker_LinkOrder(t *testing.T) { baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - prov := newProv() + prov := mockProvisioner(t) provName := url.PathEscape(prov.GetName()) - ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx := NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, baseURLKey{}, baseURL) oid := "orderID" certID := "certID" @@ -227,10 +245,10 @@ func TestLinker_LinkOrder(t *testing.T) { func TestLinker_LinkAccount(t *testing.T) { baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - prov := newProv() + prov := mockProvisioner(t) provName := url.PathEscape(prov.GetName()) - ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx := NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, baseURLKey{}, baseURL) accID := "accountID" linkerPrefix := "acme" @@ -259,10 +277,10 @@ func TestLinker_LinkAccount(t *testing.T) { func TestLinker_LinkChallenge(t *testing.T) { baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - prov := newProv() + prov := mockProvisioner(t) provName := url.PathEscape(prov.GetName()) - ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx := NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, baseURLKey{}, baseURL) chID := "chID" azID := "azID" @@ -292,10 +310,10 @@ func TestLinker_LinkChallenge(t *testing.T) { func TestLinker_LinkAuthorization(t *testing.T) { baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - prov := newProv() + prov := mockProvisioner(t) provName := url.PathEscape(prov.GetName()) - ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx := NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, baseURLKey{}, baseURL) chID0 := "chID-0" chID1 := "chID-1" @@ -334,10 +352,10 @@ func TestLinker_LinkAuthorization(t *testing.T) { func TestLinker_LinkOrdersByAccountID(t *testing.T) { baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - prov := newProv() + prov := mockProvisioner(t) provName := url.PathEscape(prov.GetName()) - ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx := NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, baseURLKey{}, baseURL) linkerPrefix := "acme" l := NewLinker("dns", linkerPrefix) From a8a42619804ef67cd39e4589bc472060366e17e7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 2 May 2022 18:39:03 -0700 Subject: [PATCH 178/241] Fix authority/admin/api tests --- authority/admin/api/acme_test.go | 34 +++++++++++--------- authority/admin/api/admin_test.go | 32 ++++++------------- authority/admin/api/middleware_test.go | 13 +++----- authority/admin/api/provisioner_test.go | 42 +++++++++++-------------- 4 files changed, 52 insertions(+), 69 deletions(-) diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 6ffe1418..6b89b288 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -29,6 +29,17 @@ func readProtoJSON(r io.ReadCloser, m proto.Message) error { return protojson.Unmarshal(data, m) } +func mockMustAuthority(t *testing.T, a adminAuthority) { + t.Helper() + fn := mustAuthority + t.Cleanup(func() { + mustAuthority = fn + }) + mustAuthority = func(ctx context.Context) adminAuthority { + return a + } +} + func TestHandler_requireEABEnabled(t *testing.T) { type test struct { ctx context.Context @@ -54,6 +65,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { return test{ ctx: ctx, auth: auth, + adminDB: &admin.MockDB{}, err: err, statusCode: 500, } @@ -143,16 +155,12 @@ 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, - } - + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() - h.requireEABEnabled(tc.next)(w, req) + requireEABEnabled(tc.next)(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -194,6 +202,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { } return test{ auth: auth, + adminDB: &admin.MockDB{}, provisionerName: "provName", want: false, err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), @@ -358,12 +367,9 @@ func TestHandler_provisionerHasEABEnabled(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, - } - got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(context.Background(), tc.adminDB) + got, prov, err := provisionerHasEABEnabled(ctx, tc.provisionerName) if (err != nil) != (tc.err != nil) { t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err) return diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go index 8d223b52..2f5528e1 100644 --- a/authority/admin/api/admin_test.go +++ b/authority/admin/api/admin_test.go @@ -317,14 +317,11 @@ func TestHandler_GetAdmin(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } - + mockMustAuthority(t, tc.auth) req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetAdmin(w, req) + GetAdmin(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -456,13 +453,10 @@ func TestHandler_GetAdmins(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } - + mockMustAuthority(t, tc.auth) req := tc.req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetAdmins(w, req) + GetAdmins(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -640,13 +634,11 @@ func TestHandler_CreateAdmin(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } + mockMustAuthority(t, tc.auth) req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.CreateAdmin(w, req) + CreateAdmin(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -732,13 +724,11 @@ func TestHandler_DeleteAdmin(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } + mockMustAuthority(t, tc.auth) req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.DeleteAdmin(w, req) + DeleteAdmin(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -877,13 +867,11 @@ func TestHandler_UpdateAdmin(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } + mockMustAuthority(t, tc.auth) req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.UpdateAdmin(w, req) + UpdateAdmin(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index 7fb4671a..3445a3b5 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -64,13 +64,11 @@ func TestHandler_requireAPIEnabled(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } + mockMustAuthority(t, tc.auth) req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.requireAPIEnabled(tc.next)(w, req) + requireAPIEnabled(tc.next)(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -194,13 +192,10 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } - + mockMustAuthority(t, tc.auth) req := tc.req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.extractAuthorizeTokenAdmin(tc.next)(w, req) + extractAuthorizeTokenAdmin(tc.next)(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) diff --git a/authority/admin/api/provisioner_test.go b/authority/admin/api/provisioner_test.go index 6d5024f2..6ee26dba 100644 --- a/authority/admin/api/provisioner_test.go +++ b/authority/admin/api/provisioner_test.go @@ -47,6 +47,7 @@ func TestHandler_GetProvisioner(t *testing.T) { ctx: ctx, req: req, auth: auth, + adminDB: &admin.MockDB{}, statusCode: 500, err: &admin.Error{ Type: admin.ErrorServerInternalType.String(), @@ -71,6 +72,7 @@ func TestHandler_GetProvisioner(t *testing.T) { ctx: ctx, req: req, auth: auth, + adminDB: &admin.MockDB{}, statusCode: 500, err: &admin.Error{ Type: admin.ErrorServerInternalType.String(), @@ -153,13 +155,11 @@ func TestHandler_GetProvisioner(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, - } - req := tc.req.WithContext(tc.ctx) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + req := tc.req.WithContext(ctx) w := httptest.NewRecorder() - h.GetProvisioner(w, req) + GetProvisioner(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -277,12 +277,10 @@ func TestHandler_GetProvisioners(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } + mockMustAuthority(t, tc.auth) req := tc.req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.GetProvisioners(w, req) + GetProvisioners(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -402,13 +400,11 @@ func TestHandler_CreateProvisioner(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } + mockMustAuthority(t, tc.auth) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.CreateProvisioner(w, req) + CreateProvisioner(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -562,12 +558,10 @@ func TestHandler_DeleteProvisioner(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - h := &Handler{ - auth: tc.auth, - } + mockMustAuthority(t, tc.auth) req := tc.req.WithContext(tc.ctx) w := httptest.NewRecorder() - h.DeleteProvisioner(w, req) + DeleteProvisioner(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) @@ -616,6 +610,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { return test{ ctx: context.Background(), body: body, + adminDB: &admin.MockDB{}, statusCode: 500, err: &admin.Error{ // TODO(hs): this probably needs a better error Type: "", @@ -645,6 +640,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { return test{ ctx: ctx, body: body, + adminDB: &admin.MockDB{}, auth: auth, statusCode: 500, err: &admin.Error{ @@ -1052,14 +1048,12 @@ func TestHandler_UpdateProvisioner(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, - } + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() - h.UpdateProvisioner(w, req) + UpdateProvisioner(w, req) res := w.Result() assert.Equals(t, tc.statusCode, res.StatusCode) From 9147356d8afd01dcc4553a2574db36f236b79ba8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 2 May 2022 18:47:47 -0700 Subject: [PATCH 179/241] Fix linter errors --- acme/api/handler.go | 4 ++-- acme/api/handler_test.go | 2 +- acme/client.go | 6 ++--- authority/admin/api/handler.go | 4 ---- ca/ca.go | 5 ++-- scep/api/api.go | 42 +++++++++++++++++----------------- scep/authority.go | 14 ------------ 7 files changed, 29 insertions(+), 48 deletions(-) diff --git a/acme/api/handler.go b/acme/api/handler.go index f6d79031..d00f8275 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -40,7 +40,7 @@ type payloadInfo struct { // HandlerOptions required to create a new ACME API request handler. type HandlerOptions struct { - // DB storage backend that impements the acme.DB interface. + // DB storage backend that implements the acme.DB interface. // // Deprecated: use acme.NewContex(context.Context, acme.DB) DB acme.DB @@ -50,7 +50,7 @@ type HandlerOptions struct { // Deprecated: use authority.NewContext(context.Context, *authority.Authority) CA acme.CertificateAuthority - // Backdate is the duration that the CA will substract from the current time + // Backdate is the duration that the CA will subtract from the current time // to set the NotBefore in the certificate. Backdate provisioner.Duration diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index 2ac41228..bd88c96f 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -31,7 +31,7 @@ type mockClient struct { tlsDial func(network, addr string, config *tls.Config) (*tls.Conn, error) } -func (m *mockClient) Get(url string) (*http.Response, error) { return m.get(url) } +func (m *mockClient) Get(u string) (*http.Response, error) { return m.get(u) } func (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) } func (m *mockClient) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) { return m.tlsDial(network, addr, config) diff --git a/acme/client.go b/acme/client.go index 2b200e45..31f4c975 100644 --- a/acme/client.go +++ b/acme/client.go @@ -37,11 +37,11 @@ func ClientFromContext(ctx context.Context) (c Client, ok bool) { // MustClientFromContext returns the current client from the given context. It will // return a new instance of the client if it does not exist. func MustClientFromContext(ctx context.Context) Client { - if c, ok := ClientFromContext(ctx); !ok { + c, ok := ClientFromContext(ctx) + if !ok { return NewClient() - } else { - return c } + return c } type client struct { diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 0acd3ca9..95b9cd9c 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -40,10 +40,6 @@ func Route(r api.Router, acmeResponder acmeAdminResponderInterface) { return extractAuthorizeTokenAdmin(requireAPIEnabled(next)) } - requireEABEnabled := func(next nextHTTP) nextHTTP { - return requireEABEnabled(next) - } - // Provisioners r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner)) r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners)) diff --git a/ca/ca.go b/ca/ca.go index e910da74..d7943a6c 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -230,13 +230,12 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { if err != nil { return nil, errors.Wrap(err, "error creating SCEP authority") } - scepRouterHandler := scepAPI.New(scepAuthority) // According to the RFC (https://tools.ietf.org/html/rfc8894#section-7.10), // SCEP operations are performed using HTTP, so that's why the API is mounted // to the insecure mux. insecureMux.Route("/"+scepPrefix, func(r chi.Router) { - scepRouterHandler.Route(r) + scepAPI.Route(r) }) // The RFC also mentions usage of HTTPS, but seems to advise @@ -246,7 +245,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { // as well as HTTPS can be used to request certificates // using SCEP. mux.Route("/"+scepPrefix, func(r chi.Router) { - scepRouterHandler.Route(r) + scepAPI.Route(r) }) } diff --git a/scep/api/api.go b/scep/api/api.go index 0d62904d..e513aa43 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -38,8 +38,8 @@ type request struct { Message []byte } -// response is a SCEP server response. -type response struct { +// Response is a SCEP server Response. +type Response struct { Operation string CACertNum int Data []byte @@ -81,7 +81,7 @@ func Get(w http.ResponseWriter, r *http.Request) { } ctx := r.Context() - var res response + var res Response switch req.Operation { case opnGetCACert: @@ -110,7 +110,7 @@ func Post(w http.ResponseWriter, r *http.Request) { return } - var res response + var res Response switch req.Operation { case opnPKIOperation: res, err = PKIOperation(r.Context(), req) @@ -207,18 +207,18 @@ func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { } // GetCACert returns the CA certificates in a SCEP response -func GetCACert(ctx context.Context) (response, error) { +func GetCACert(ctx context.Context) (Response, error) { auth := scep.MustFromContext(ctx) certs, err := auth.GetCACertificates(ctx) if err != nil { - return response{}, err + return Response{}, err } if len(certs) == 0 { - return response{}, errors.New("missing CA cert") + return Response{}, errors.New("missing CA cert") } - res := response{ + res := Response{ Operation: opnGetCACert, CACertNum: len(certs), } @@ -231,7 +231,7 @@ func GetCACert(ctx context.Context) (response, error) { // not signed or encrypted data has to be returned. data, err := microscep.DegenerateCertificates(certs) if err != nil { - return response{}, err + return Response{}, err } res.Data = data } @@ -240,11 +240,11 @@ func GetCACert(ctx context.Context) (response, error) { } // GetCACaps returns the CA capabilities in a SCEP response -func GetCACaps(ctx context.Context) (response, error) { +func GetCACaps(ctx context.Context) (Response, error) { auth := scep.MustFromContext(ctx) caps := auth.GetCACaps(ctx) - res := response{ + res := Response{ Operation: opnGetCACaps, Data: formatCapabilities(caps), } @@ -253,12 +253,12 @@ func GetCACaps(ctx context.Context) (response, error) { } // PKIOperation performs PKI operations and returns a SCEP response -func PKIOperation(ctx context.Context, req request) (response, error) { +func PKIOperation(ctx context.Context, req request) (Response, error) { // parse the message using microscep implementation microMsg, err := microscep.ParsePKIMessage(req.Message) if err != nil { // return the error, because we can't use the msg for creating a CertRep - return response{}, err + return Response{}, err } // this is essentially doing the same as microscep.ParsePKIMessage, but @@ -266,7 +266,7 @@ func PKIOperation(ctx context.Context, req request) (response, error) { // wrapper for the microscep implementation. p7, err := pkcs7.Parse(microMsg.Raw) if err != nil { - return response{}, err + return Response{}, err } // copy over properties to our internal PKIMessage @@ -280,7 +280,7 @@ func PKIOperation(ctx context.Context, req request) (response, error) { auth := scep.MustFromContext(ctx) if err := auth.DecryptPKIEnvelope(ctx, msg); err != nil { - return response{}, err + return Response{}, err } // NOTE: at this point we have sufficient information for returning nicely signed CertReps @@ -315,7 +315,7 @@ func PKIOperation(ctx context.Context, req request) (response, error) { return createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err)) } - res := response{ + res := Response{ Operation: opnPKIOperation, Data: certRep.Raw, Certificate: certRep.Certificate, @@ -329,7 +329,7 @@ func formatCapabilities(caps []string) []byte { } // writeResponse writes a SCEP response back to the SCEP client. -func writeResponse(w http.ResponseWriter, res response) { +func writeResponse(w http.ResponseWriter, res Response) { if res.Error != nil { log.Error(w, res.Error) @@ -349,20 +349,20 @@ func fail(w http.ResponseWriter, err error) { http.Error(w, err.Error(), http.StatusInternalServerError) } -func createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (response, error) { +func createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (Response, error) { auth := scep.MustFromContext(ctx) certRepMsg, err := auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error()) if err != nil { - return response{}, err + return Response{}, err } - return response{ + return Response{ Operation: opnPKIOperation, Data: certRepMsg.Raw, Error: failError, }, nil } -func contentHeader(r response) string { +func contentHeader(r Response) string { switch r.Operation { default: return "text/plain" diff --git a/scep/authority.go b/scep/authority.go index 946fa948..7fe01c1d 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -63,20 +63,6 @@ type AuthorityOptions struct { Prefix string } -type optionsKey struct{} - -func newOptionsContext(ctx context.Context, o *AuthorityOptions) context.Context { - return context.WithValue(ctx, optionsKey{}, o) -} - -func optionsFromContext(ctx context.Context) *AuthorityOptions { - o, ok := ctx.Value(optionsKey{}).(*AuthorityOptions) - if !ok { - panic("scep options are not in the context") - } - return o -} - // SignAuthority is the interface for a signing authority type SignAuthority interface { Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) From 62d93a644e8d00d23d07582e403104c8096c3374 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 2 May 2022 19:39:50 -0700 Subject: [PATCH 180/241] Apply base context to test of the ca package --- ca/bootstrap_test.go | 4 ++++ ca/ca_test.go | 19 +++++++++++++------ ca/tls_test.go | 8 +++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index 9aaa5f1f..1cda8232 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -53,7 +53,11 @@ func startCABootstrapServer() *httptest.Server { if err != nil { panic(err) } + baseContext := buildContext(ca.auth, nil, nil, nil) srv.Config.Handler = ca.srv.Handler + srv.Config.BaseContext = func(net.Listener) context.Context { + return baseContext + } srv.TLS = ca.srv.TLSConfig srv.StartTLS() // Force the use of GetCertificate on IPs diff --git a/ca/ca_test.go b/ca/ca_test.go index e4c35a90..29eac575 100644 --- a/ca/ca_test.go +++ b/ca/ca_test.go @@ -2,6 +2,7 @@ package ca import ( "bytes" + "context" "crypto" "crypto/rand" "crypto/sha1" @@ -281,7 +282,8 @@ ZEp7knvU2psWRw== assert.FatalError(t, err) rr := httptest.NewRecorder() - tc.ca.srv.Handler.ServeHTTP(rr, rq) + ctx := authority.NewContext(context.Background(), tc.ca.auth) + tc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx)) if assert.Equals(t, rr.Code, tc.status) { body := &ClosingBuffer{rr.Body} @@ -360,7 +362,8 @@ func TestCAProvisioners(t *testing.T) { assert.FatalError(t, err) rr := httptest.NewRecorder() - tc.ca.srv.Handler.ServeHTTP(rr, rq) + ctx := authority.NewContext(context.Background(), tc.ca.auth) + tc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx)) if assert.Equals(t, rr.Code, tc.status) { body := &ClosingBuffer{rr.Body} @@ -426,7 +429,8 @@ func TestCAProvisionerEncryptedKey(t *testing.T) { assert.FatalError(t, err) rr := httptest.NewRecorder() - tc.ca.srv.Handler.ServeHTTP(rr, rq) + ctx := authority.NewContext(context.Background(), tc.ca.auth) + tc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx)) if assert.Equals(t, rr.Code, tc.status) { body := &ClosingBuffer{rr.Body} @@ -487,7 +491,8 @@ func TestCARoot(t *testing.T) { assert.FatalError(t, err) rr := httptest.NewRecorder() - tc.ca.srv.Handler.ServeHTTP(rr, rq) + ctx := authority.NewContext(context.Background(), tc.ca.auth) + tc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx)) if assert.Equals(t, rr.Code, tc.status) { body := &ClosingBuffer{rr.Body} @@ -534,7 +539,8 @@ func TestCAHealth(t *testing.T) { assert.FatalError(t, err) rr := httptest.NewRecorder() - tc.ca.srv.Handler.ServeHTTP(rr, rq) + ctx := authority.NewContext(context.Background(), tc.ca.auth) + tc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx)) if assert.Equals(t, rr.Code, tc.status) { body := &ClosingBuffer{rr.Body} @@ -628,7 +634,8 @@ func TestCARenew(t *testing.T) { rq.TLS = tc.tlsConnState rr := httptest.NewRecorder() - tc.ca.srv.Handler.ServeHTTP(rr, rq) + ctx := authority.NewContext(context.Background(), tc.ca.auth) + tc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx)) if assert.Equals(t, rr.Code, tc.status) { body := &ClosingBuffer{rr.Body} diff --git a/ca/tls_test.go b/ca/tls_test.go index 93dbe9b3..946a6cb5 100644 --- a/ca/tls_test.go +++ b/ca/tls_test.go @@ -10,6 +10,7 @@ import ( "encoding/hex" "io" "log" + "net" "net/http" "net/http/httptest" "reflect" @@ -77,7 +78,12 @@ func startCATestServer() *httptest.Server { panic(err) } // Use a httptest.Server instead - return startTestServer(ca.srv.TLSConfig, ca.srv.Handler) + srv := startTestServer(ca.srv.TLSConfig, ca.srv.Handler) + baseContext := buildContext(ca.auth, nil, nil, nil) + srv.Config.BaseContext = func(net.Listener) context.Context { + return baseContext + } + return srv } func sign(domain string) (*Client, *api.SignResponse, crypto.PrivateKey) { From 02c0ae81ac07af43d7009621919f4c3248750800 Mon Sep 17 00:00:00 2001 From: vijayjt <2975049+vijayjt@users.noreply.github.com> Date: Thu, 5 May 2022 00:10:59 +0100 Subject: [PATCH 181/241] Allow KMS type to be specified in the helm chart template if specified on the command line. --- pki/helm.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pki/helm.go b/pki/helm.go index 0a2f7f02..e13bb97c 100644 --- a/pki/helm.go +++ b/pki/helm.go @@ -68,6 +68,10 @@ inject: federateRoots: [] crt: {{ .Intermediate }} key: {{ .IntermediateKey }} + {{- if .Kms }} + kms: + type: {{ lower (.Kms.Type | toString) }} + {{- end }} {{- if .EnableSSH }} ssh: hostKey: {{ .Ssh.HostKey }} From 43ddcf2efe96d6bbf41a92c6f59ad9442e5c3f48 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 4 May 2022 17:35:34 -0700 Subject: [PATCH 182/241] Do not use deprecated AuthorizeSign --- api/api.go | 1 - api/api_test.go | 12 ++++-------- api/revoke_test.go | 8 ++++---- api/sign.go | 7 +++++-- api/ssh_test.go | 2 +- authority/authorize.go | 3 +-- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/api/api.go b/api/api.go index 0ca4a5ef..75d26237 100644 --- a/api/api.go +++ b/api/api.go @@ -35,7 +35,6 @@ type Authority interface { SSHAuthority // context specifies the Authorize[Sign|Revoke|etc.] method. Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) - AuthorizeSign(ott string) ([]provisioner.SignOption, error) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) GetTLSOptions() *config.TLSOptions Root(shasum string) (*x509.Certificate, error) diff --git a/api/api_test.go b/api/api_test.go index 698b629c..1f27ab8c 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -185,7 +185,7 @@ func mockMustAuthority(t *testing.T, a Authority) { type mockAuthority struct { ret1, ret2 interface{} err error - authorizeSign func(ott string) ([]provisioner.SignOption, error) + authorize func(ctx context.Context, ott string) ([]provisioner.SignOption, error) authorizeRenewToken func(ctx context.Context, ott string) (*x509.Certificate, error) getTLSOptions func() *authority.TLSOptions root func(shasum string) (*x509.Certificate, error) @@ -214,12 +214,8 @@ type mockAuthority struct { // TODO: remove once Authorize is deprecated. func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { - return m.AuthorizeSign(ott) -} - -func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) { - if m.authorizeSign != nil { - return m.authorizeSign(ott) + if m.authorize != nil { + return m.authorize(ctx, ott) } return m.ret1.([]provisioner.SignOption), m.err } @@ -908,7 +904,7 @@ func Test_Sign(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mockMustAuthority(t, &mockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.signErr, - authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return tt.certAttrOpts, tt.autherr }, getTLSOptions: func() *authority.TLSOptions { diff --git a/api/revoke_test.go b/api/revoke_test.go index fa46dd90..c3fa6ceb 100644 --- a/api/revoke_test.go +++ b/api/revoke_test.go @@ -108,7 +108,7 @@ func Test_caHandler_Revoke(t *testing.T) { input: string(input), statusCode: http.StatusOK, auth: &mockAuthority{ - authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return nil, nil }, revoke: func(ctx context.Context, opts *authority.RevokeOptions) error { @@ -152,7 +152,7 @@ func Test_caHandler_Revoke(t *testing.T) { statusCode: http.StatusOK, tls: cs, auth: &mockAuthority{ - authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return nil, nil }, revoke: func(ctx context.Context, ri *authority.RevokeOptions) error { @@ -187,7 +187,7 @@ func Test_caHandler_Revoke(t *testing.T) { input: string(input), statusCode: http.StatusInternalServerError, auth: &mockAuthority{ - authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return nil, nil }, revoke: func(ctx context.Context, opts *authority.RevokeOptions) error { @@ -209,7 +209,7 @@ func Test_caHandler_Revoke(t *testing.T) { input: string(input), statusCode: http.StatusForbidden, auth: &mockAuthority{ - authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return nil, nil }, revoke: func(ctx context.Context, opts *authority.RevokeOptions) error { diff --git a/api/sign.go b/api/sign.go index b263e2e9..f7c3cc5a 100644 --- a/api/sign.go +++ b/api/sign.go @@ -68,8 +68,11 @@ func Sign(w http.ResponseWriter, r *http.Request) { TemplateData: body.TemplateData, } - a := mustAuthority(r.Context()) - signOpts, err := a.AuthorizeSign(body.OTT) + ctx := r.Context() + a := mustAuthority(ctx) + + ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) + signOpts, err := a.Authorize(ctx, body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) return diff --git a/api/ssh_test.go b/api/ssh_test.go index c6fee2de..57dd6775 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -316,7 +316,7 @@ func Test_SSHSign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockMustAuthority(t, &mockAuthority{ - authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return []provisioner.SignOption{}, tt.authErr }, signSSH: func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { diff --git a/authority/authorize.go b/authority/authorize.go index 7f9f456c..c0722a1b 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -251,8 +251,7 @@ func (a *Authority) authorizeSign(ctx context.Context, token string) ([]provisio // AuthorizeSign authorizes a signature request by validating and authenticating // a token that must be sent w/ the request. // -// NOTE: This method is deprecated and should not be used. We make it available -// in the short term os as not to break existing clients. +// Deprecated: Use Authorize(context.Context, string) ([]provisioner.SignOption, error). func (a *Authority) AuthorizeSign(token string) ([]provisioner.SignOption, error) { ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) return a.Authorize(ctx, token) From d51c6b7d83b566182f0584ea6d6e82332057ed39 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 4 May 2022 19:20:34 -0700 Subject: [PATCH 183/241] Make step handler backward compatible --- scep/api/api.go | 34 +++++++++++++++++++++++++++------- scep/authority.go | 1 - 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/scep/api/api.go b/scep/api/api.go index e513aa43..49a5267a 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -48,28 +48,48 @@ type Response struct { } // handler is the SCEP request handler. -type handler struct{} +type handler struct { + auth *scep.Authority +} // Route traffic and implement the Router interface. // // Deprecated: use scep.Route(r api.Router) func (h *handler) Route(r api.Router) { - Route(r) + route(r, func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := scep.NewContext(r.Context(), h.auth) + next(w, r.WithContext(ctx)) + } + }) } // New returns a new SCEP API router. // // Deprecated: use scep.Route(r api.Router) func New(auth *scep.Authority) api.RouterHandler { - return &handler{} + return &handler{auth: auth} } // Route traffic and implement the Router interface. func Route(r api.Router) { - r.MethodFunc(http.MethodGet, "/{provisionerName}/*", lookupProvisioner(Get)) - r.MethodFunc(http.MethodGet, "/{provisionerName}", lookupProvisioner(Get)) - r.MethodFunc(http.MethodPost, "/{provisionerName}/*", lookupProvisioner(Post)) - r.MethodFunc(http.MethodPost, "/{provisionerName}", lookupProvisioner(Post)) + route(r, nil) +} + +func route(r api.Router, middleware func(next http.HandlerFunc) http.HandlerFunc) { + getHandler := lookupProvisioner(Get) + postHandler := lookupProvisioner(Post) + + // For backward compatibility. + if middleware != nil { + getHandler = middleware(getHandler) + postHandler = middleware(postHandler) + } + + r.MethodFunc(http.MethodGet, "/{provisionerName}/*", getHandler) + r.MethodFunc(http.MethodGet, "/{provisionerName}", getHandler) + r.MethodFunc(http.MethodPost, "/{provisionerName}/*", postHandler) + r.MethodFunc(http.MethodPost, "/{provisionerName}", postHandler) } // Get handles all SCEP GET requests diff --git a/scep/authority.go b/scep/authority.go index 7fe01c1d..7dbbb8c5 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -453,7 +453,6 @@ func (a *Authority) CreateFailureResponse(ctx context.Context, csr *x509.Certifi // MatchChallengePassword verifies a SCEP challenge password func (a *Authority) MatchChallengePassword(ctx context.Context, password string) (bool, error) { - p, err := provisionerFromContext(ctx) if err != nil { return false, err From 60d8b22d89a5eb5b68e9156e27c68b49a8511949 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 5 May 2022 11:05:57 +0200 Subject: [PATCH 184/241] 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 185/241] 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 186/241] 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 187/241] 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 188/241] 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 2ea0c703448f5dff7398535167da9b346d1fd5a9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 5 May 2022 12:25:07 -0700 Subject: [PATCH 189/241] Move acme context middleware to deprecated handler --- acme/api/handler.go | 57 +++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/acme/api/handler.go b/acme/api/handler.go index d00f8275..96e22d85 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -79,12 +79,29 @@ type handler struct { opts *HandlerOptions } -// Route traffic and implement the Router interface. +// Route traffic and implement the Router interface. For backward compatibility +// this route adds will add a new middleware that will set the ACME components +// on the context. +// +// Deprecated: use api.Route(r api.Router) func (h *handler) Route(r api.Router) { - route(r, h.opts) + client := acme.NewClient() + linker := acme.NewLinker(h.opts.DNS, h.opts.Prefix) + route(r, func(next nextHTTP) nextHTTP { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if ca, ok := h.opts.CA.(*authority.Authority); ok && ca != nil { + ctx = authority.NewContext(ctx, ca) + } + ctx = acme.NewContext(ctx, h.opts.DB, client, linker, h.opts.PrerequisitesChecker) + next(w, r.WithContext(ctx)) + } + }) } // NewHandler returns a new ACME API handler. +// +// Deprecated: use api.Route(r api.Router) func NewHandler(opts HandlerOptions) api.RouterHandler { return &handler{ opts: &opts, @@ -98,40 +115,18 @@ func Route(r api.Router) { route(r, nil) } -func route(r api.Router, opts *HandlerOptions) { - var withContext func(next nextHTTP) nextHTTP - - // For backward compatibility this block adds will add a new middleware that - // will set the ACME components to the context. - if opts != nil { - client := acme.NewClient() - linker := acme.NewLinker(opts.DNS, opts.Prefix) - - withContext = func(next nextHTTP) nextHTTP { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - if ca, ok := opts.CA.(*authority.Authority); ok && ca != nil { - ctx = authority.NewContext(ctx, ca) - } - ctx = acme.NewContext(ctx, opts.DB, client, linker, opts.PrerequisitesChecker) - next(w, r.WithContext(ctx)) - } - } - } else { - withContext = func(next nextHTTP) nextHTTP { - return func(w http.ResponseWriter, r *http.Request) { - next(w, r) - } - } - } - +func route(r api.Router, middleware func(next nextHTTP) nextHTTP) { commonMiddleware := func(next nextHTTP) nextHTTP { - return withContext(func(w http.ResponseWriter, r *http.Request) { + handler := func(w http.ResponseWriter, r *http.Request) { // Linker middleware gets the provisioner and current url from the // request and sets them in the context. linker := acme.MustLinkerFromContext(r.Context()) linker.Middleware(http.HandlerFunc(checkPrerequisites(next))).ServeHTTP(w, r) - }) + } + if middleware != nil { + handler = middleware(handler) + } + return handler } validatingMiddleware := func(next nextHTTP) nextHTTP { return commonMiddleware(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next)))))) From 7104299119974b2105bb1c26a984f22feba1ae75 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 6 May 2022 13:12:13 +0200 Subject: [PATCH 190/241] 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 191/241] 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 192/241] 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 } From f639bfc53b7d53f089e5edc22ead70f91957e532 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 6 May 2022 14:05:08 -0700 Subject: [PATCH 193/241] Use contexts on the new PolicyAdminResponder --- authority/admin/api/handler.go | 6 +- authority/admin/api/policy.go | 165 ++++++++++------------- authority/admin/api/policy_test.go | 202 +++++++++++++++++++---------- ca/ca.go | 2 +- 4 files changed, 209 insertions(+), 166 deletions(-) diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index bb871c2a..0ab417e6 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -13,7 +13,7 @@ import ( // Handler is the Admin API request handler. type Handler struct { acmeResponder acmeAdminResponderInterface - policyResponder policyAdminResponderInterface + policyResponder PolicyAdminResponder } // Route traffic and implement the Router interface. @@ -24,7 +24,7 @@ func (h *Handler) Route(r api.Router) { } // NewHandler returns a new Authority Config Handler. -func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface, policyResponder policyAdminResponderInterface) api.RouterHandler { +func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface, policyResponder PolicyAdminResponder) api.RouterHandler { return &Handler{ acmeResponder: acmeResponder, policyResponder: policyResponder, @@ -36,7 +36,7 @@ var mustAuthority = func(ctx context.Context) adminAuthority { } // Route traffic and implement the Router interface. -func Route(r api.Router, acmeResponder acmeAdminResponderInterface, policyResponder policyAdminResponderInterface) { +func Route(r api.Router, acmeResponder acmeAdminResponderInterface, policyResponder PolicyAdminResponder) { authnz := func(next http.HandlerFunc) http.HandlerFunc { return extractAuthorizeTokenAdmin(requireAPIEnabled(next)) } diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 6af1104a..9f338c0b 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -1,6 +1,7 @@ package api import ( + "context" "errors" "net/http" @@ -14,7 +15,9 @@ import ( "github.com/smallstep/certificates/authority/policy" ) -type policyAdminResponderInterface interface { +// PolicyAdminResponder is the interface responsible for writing ACME admin +// responses. +type PolicyAdminResponder interface { GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) @@ -29,39 +32,24 @@ type policyAdminResponderInterface interface { DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) } -// PolicyAdminResponder is responsible for writing ACME admin responses -type PolicyAdminResponder struct { - 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) *PolicyAdminResponder { - - var isLinkedCA bool - if a, ok := adminDB.(interface{ IsLinkedCA() bool }); ok { - isLinkedCA = a.IsLinkedCA() - } +// policyAdminResponder is responsible for writing ACME admin responses. +type policyAdminResponder struct{} - return &PolicyAdminResponder{ - auth: auth, - adminDB: adminDB, - acmeDB: acmeDB, - isLinkedCA: isLinkedCA, - } +// NewACMEAdminResponder returns a new PolicyAdminResponder. +func NewPolicyAdminResponder() PolicyAdminResponder { + return &policyAdminResponder{} } // GetAuthorityPolicy handles the GET /admin/authority/policy request -func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) { - - if err := par.blockLinkedCA(); err != nil { +func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - authorityPolicy, err := par.auth.GetAuthorityPolicy(r.Context()) + auth := mustAuthority(ctx) + authorityPolicy, err := 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 @@ -76,15 +64,15 @@ 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 { +func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() - authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx) + auth := mustAuthority(ctx) + authorityPolicy, err := auth.GetAuthorityPolicy(ctx) if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) { render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy")) @@ -113,7 +101,7 @@ func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r adm := linkedca.MustAdminFromContext(ctx) var createdPolicy *linkedca.Policy - if createdPolicy, err = par.auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil { + if createdPolicy, err = auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil { if isBadRequest(err) { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy")) return @@ -127,15 +115,15 @@ 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 { +func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() - authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx) + auth := mustAuthority(ctx) + authorityPolicy, err := auth.GetAuthorityPolicy(ctx) if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) { render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy")) @@ -163,7 +151,7 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r adm := linkedca.MustAdminFromContext(ctx) var updatedPolicy *linkedca.Policy - if updatedPolicy, err = par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil { + if updatedPolicy, err = auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil { if isBadRequest(err) { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy")) return @@ -177,15 +165,15 @@ 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 { +func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() - authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx) + auth := mustAuthority(ctx) + authorityPolicy, err := auth.GetAuthorityPolicy(ctx) if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) { render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy")) @@ -197,7 +185,7 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r return } - if err := par.auth.RemoveAuthorityPolicy(ctx); err != nil { + if err := auth.RemoveAuthorityPolicy(ctx); err != nil { render.Error(w, admin.WrapErrorISE(err, "error deleting authority policy")) return } @@ -206,15 +194,14 @@ 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 { +func (par *policyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - prov := linkedca.MustProvisionerFromContext(r.Context()) - + prov := linkedca.MustProvisionerFromContext(ctx) provisionerPolicy := prov.GetPolicy() if provisionerPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) @@ -225,16 +212,14 @@ 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 { +func (par *policyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() prov := linkedca.MustProvisionerFromContext(ctx) - provisionerPolicy := prov.GetPolicy() if provisionerPolicy != nil { adminErr := admin.NewError(admin.ErrorConflictType, "provisioner %s already has a policy", prov.Name) @@ -256,8 +241,8 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, } prov.Policy = newPolicy - - if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { + auth := mustAuthority(ctx) + if err := auth.UpdateProvisioner(ctx, prov); err != nil { if isBadRequest(err) { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy")) return @@ -271,16 +256,14 @@ 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 { +func (par *policyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() prov := linkedca.MustProvisionerFromContext(ctx) - provisionerPolicy := prov.GetPolicy() if provisionerPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) @@ -301,7 +284,8 @@ func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, } prov.Policy = newPolicy - if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { + auth := mustAuthority(ctx) + if err := auth.UpdateProvisioner(ctx, prov); err != nil { if isBadRequest(err) { render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy")) return @@ -315,16 +299,14 @@ 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 { +func (par *policyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() prov := linkedca.MustProvisionerFromContext(ctx) - if prov.Policy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return @@ -333,7 +315,8 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, // remove the policy prov.Policy = nil - if err := par.auth.UpdateProvisioner(ctx, prov); err != nil { + auth := mustAuthority(ctx) + if err := auth.UpdateProvisioner(ctx, prov); err != nil { render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner policy")) return } @@ -341,16 +324,14 @@ func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) } -func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - - if err := par.blockLinkedCA(); err != nil { +func (par *policyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() eak := linkedca.MustExternalAccountKeyFromContext(ctx) - eakPolicy := eak.GetPolicy() if eakPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) @@ -360,17 +341,15 @@ func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r * render.ProtoJSONStatus(w, eakPolicy, http.StatusOK) } -func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - - if err := par.blockLinkedCA(); err != nil { +func (par *policyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() prov := linkedca.MustProvisionerFromContext(ctx) eak := linkedca.MustExternalAccountKeyFromContext(ctx) - eakPolicy := eak.GetPolicy() if eakPolicy != nil { adminErr := admin.NewError(admin.ErrorConflictType, "ACME EAK %s already has a policy", eak.Id) @@ -394,7 +373,8 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, eak.Policy = newPolicy acmeEAK := linkedEAKToCertificates(eak) - if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { + acmeDB := acme.MustDatabaseFromContext(ctx) + if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { render.Error(w, admin.WrapErrorISE(err, "error creating ACME EAK policy")) return } @@ -402,17 +382,15 @@ func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, render.ProtoJSONStatus(w, newPolicy, http.StatusCreated) } -func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - - if err := par.blockLinkedCA(); err != nil { +func (par *policyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() prov := linkedca.MustProvisionerFromContext(ctx) eak := linkedca.MustExternalAccountKeyFromContext(ctx) - eakPolicy := eak.GetPolicy() if eakPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) @@ -434,7 +412,8 @@ func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, eak.Policy = newPolicy acmeEAK := linkedEAKToCertificates(eak) - if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { + acmeDB := acme.MustDatabaseFromContext(ctx) + if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { render.Error(w, admin.WrapErrorISE(err, "error updating ACME EAK policy")) return } @@ -442,17 +421,15 @@ func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, render.ProtoJSONStatus(w, newPolicy, http.StatusOK) } -func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - - if err := par.blockLinkedCA(); err != nil { +func (par *policyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := blockLinkedCA(ctx); err != nil { render.Error(w, err) return } - ctx := r.Context() prov := linkedca.MustProvisionerFromContext(ctx) eak := linkedca.MustExternalAccountKeyFromContext(ctx) - eakPolicy := eak.GetPolicy() if eakPolicy == nil { render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist")) @@ -463,7 +440,8 @@ func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, eak.Policy = nil acmeEAK := linkedEAKToCertificates(eak) - if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { + acmeDB := acme.MustDatabaseFromContext(ctx) + if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil { render.Error(w, admin.WrapErrorISE(err, "error deleting ACME EAK policy")) return } @@ -472,9 +450,10 @@ func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, } // blockLinkedCA blocks all API operations on linked deployments -func (par *PolicyAdminResponder) blockLinkedCA() error { +func blockLinkedCA(ctx context.Context) error { // temporary blocking linked deployments - if par.isLinkedCA { + adminDB := admin.MustFromContext(ctx) + if a, ok := adminDB.(interface{ IsLinkedCA() bool }); ok && a.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 1e70db52..1ec88fb6 100644 --- a/authority/admin/api/policy_test.go +++ b/authority/admin/api/policy_test.go @@ -109,7 +109,8 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") err.Message = "error retrieving authority policy: force" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorServerInternalType, "force") @@ -124,7 +125,8 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { err := admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist") err.Message = "authority policy does not exist" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorNotFoundType, "not found") @@ -179,7 +181,8 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { }, } return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return policy, nil @@ -234,11 +237,12 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(tc.auth, tc.adminDB, nil) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("GET", "/foo", nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.GetAuthorityPolicy(w, req) @@ -301,7 +305,8 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") err.Message = "error retrieving authority policy: force" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorServerInternalType, "force") @@ -316,7 +321,8 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { err := admin.NewError(admin.ErrorConflictType, "authority already has a policy") err.Message = "authority already has a policy" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return &linkedca.Policy{}, nil @@ -332,7 +338,8 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" body := []byte("{?}") return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorNotFoundType, "not found") @@ -358,7 +365,8 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { } }`) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorNotFoundType, "not found") @@ -509,11 +517,13 @@ func TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.CreateAuthorityPolicy(w, req) @@ -586,7 +596,8 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") err.Message = "error retrieving authority policy: force" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorServerInternalType, "force") @@ -602,7 +613,8 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { err.Message = "authority policy does not exist" err.Status = http.StatusNotFound return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, nil @@ -625,7 +637,8 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" body := []byte("{?}") return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return policy, nil @@ -658,7 +671,8 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { } }`) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return policy, nil @@ -809,11 +823,13 @@ func TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.UpdateAuthorityPolicy(w, req) @@ -886,7 +902,8 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { err := admin.WrapErrorISE(errors.New("force"), "error retrieving authority policy") err.Message = "error retrieving authority policy: force" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorServerInternalType, "force") @@ -902,7 +919,8 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { err.Message = "authority policy does not exist" err.Status = http.StatusNotFound return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, nil @@ -924,7 +942,8 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { err := admin.NewErrorISE("error deleting authority policy: force") err.Message = "error deleting authority policy: force" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return policy, nil @@ -947,7 +966,8 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { } ctx := context.Background() return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return policy, nil @@ -963,11 +983,13 @@ func TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.DeleteAuthorityPolicy(w, req) @@ -1033,6 +1055,7 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { err.Message = "provisioner policy does not exist" return test{ ctx: ctx, + adminDB: &admin.MockDB{}, err: err, statusCode: 404, } @@ -1085,7 +1108,8 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { } ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, response: &testPolicyResponse{ X509: &testX509Policy{ Allow: &testX509Names{ @@ -1135,11 +1159,13 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("GET", "/foo", nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.GetProvisionerPolicy(w, req) @@ -1214,6 +1240,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { err.Message = "provisioner provName already has a policy" return test{ ctx: ctx, + adminDB: &admin.MockDB{}, err: err, statusCode: 409, } @@ -1228,6 +1255,7 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { body := []byte("{?}") return test{ ctx: ctx, + adminDB: &admin.MockDB{}, body: body, err: adminErr, statusCode: 400, @@ -1251,7 +1279,8 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { } }`) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorNotFoundType, "not found") @@ -1283,7 +1312,8 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { return &authority.PolicyError{ @@ -1318,7 +1348,8 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { return &authority.PolicyError{ @@ -1351,7 +1382,8 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { return nil @@ -1372,11 +1404,12 @@ func TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(tc.auth, tc.adminDB, nil) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.CreateProvisionerPolicy(w, req) @@ -1452,6 +1485,7 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { err.Message = "provisioner policy does not exist" return test{ ctx: ctx, + adminDB: &admin.MockDB{}, err: err, statusCode: 404, } @@ -1474,6 +1508,7 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { body := []byte("{?}") return test{ ctx: ctx, + adminDB: &admin.MockDB{}, body: body, err: adminErr, statusCode: 400, @@ -1505,7 +1540,8 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { } }`) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) { return nil, admin.NewError(admin.ErrorNotFoundType, "not found") @@ -1538,7 +1574,8 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { return &authority.PolicyError{ @@ -1574,7 +1611,8 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { return &authority.PolicyError{ @@ -1608,7 +1646,8 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { return nil @@ -1629,11 +1668,12 @@ func TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(tc.auth, tc.adminDB, nil) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.UpdateProvisionerPolicy(w, req) @@ -1710,6 +1750,7 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { err.Message = "provisioner policy does not exist" return test{ ctx: ctx, + adminDB: &admin.MockDB{}, err: err, statusCode: 404, } @@ -1723,7 +1764,8 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { err := admin.NewErrorISE("error deleting provisioner policy: force") err.Message = "error deleting provisioner policy: force" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { return errors.New("force") @@ -1740,7 +1782,8 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { } ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, auth: &mockAdminAuthority{ MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { return nil @@ -1753,11 +1796,13 @@ func TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(tc.auth, tc.adminDB, tc.acmeDB) + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.DeleteProvisionerPolicy(w, req) @@ -1828,6 +1873,7 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { err.Message = "ACME EAK policy does not exist" return test{ ctx: ctx, + adminDB: &admin.MockDB{}, err: err, statusCode: 404, } @@ -1885,7 +1931,8 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, response: &testPolicyResponse{ X509: &testX509Policy{ Allow: &testX509Names{ @@ -1935,11 +1982,12 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(nil, tc.adminDB, tc.acmeDB) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("GET", "/foo", nil) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.GetACMEAccountPolicy(w, req) @@ -2018,6 +2066,7 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { err.Message = "ACME EAK eakID already has a policy" return test{ ctx: ctx, + adminDB: &admin.MockDB{}, err: err, statusCode: 409, } @@ -2036,6 +2085,7 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { body := []byte("{?}") return test{ ctx: ctx, + adminDB: &admin.MockDB{}, body: body, err: adminErr, statusCode: 400, @@ -2064,6 +2114,7 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { }`) return test{ ctx: ctx, + adminDB: &admin.MockDB{}, body: body, err: adminErr, statusCode: 400, @@ -2091,7 +2142,8 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { assert.Equal(t, "provID", provisionerID) @@ -2124,7 +2176,8 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { assert.Equal(t, "provID", provisionerID) @@ -2147,11 +2200,12 @@ func TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(nil, tc.adminDB, tc.acmeDB) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.CreateACMEAccountPolicy(w, req) @@ -2231,6 +2285,7 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { err.Message = "ACME EAK policy does not exist" return test{ ctx: ctx, + adminDB: &admin.MockDB{}, err: err, statusCode: 404, } @@ -2257,6 +2312,7 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { body := []byte("{?}") return test{ ctx: ctx, + adminDB: &admin.MockDB{}, body: body, err: adminErr, statusCode: 400, @@ -2293,6 +2349,7 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { }`) return test{ ctx: ctx, + adminDB: &admin.MockDB{}, body: body, err: adminErr, statusCode: 400, @@ -2321,7 +2378,8 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { assert.Equal(t, "provID", provisionerID) @@ -2355,7 +2413,8 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { body, err := protojson.Marshal(policy) assert.NoError(t, err) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { assert.Equal(t, "provID", provisionerID) @@ -2378,11 +2437,12 @@ func TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(nil, tc.adminDB, tc.acmeDB) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.UpdateACMEAccountPolicy(w, req) @@ -2462,6 +2522,7 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { err.Message = "ACME EAK policy does not exist" return test{ ctx: ctx, + adminDB: &admin.MockDB{}, err: err, statusCode: 404, } @@ -2487,7 +2548,8 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { err := admin.NewErrorISE("error deleting ACME EAK policy: force") err.Message = "error deleting ACME EAK policy: force" return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { assert.Equal(t, "provID", provisionerID) @@ -2518,7 +2580,8 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) ctx = linkedca.NewContextWithExternalAccountKey(ctx, eak) return test{ - ctx: ctx, + ctx: ctx, + adminDB: &admin.MockDB{}, acmeDB: &acme.MockDB{ MockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { assert.Equal(t, "provID", provisionerID) @@ -2533,11 +2596,12 @@ func TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) { for name, prep := range tests { tc := prep(t) t.Run(name, func(t *testing.T) { - - par := NewPolicyAdminResponder(nil, tc.adminDB, tc.acmeDB) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + ctx = acme.NewDatabaseContext(ctx, tc.acmeDB) + par := NewPolicyAdminResponder() req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) - req = req.WithContext(tc.ctx) + req = req.WithContext(ctx) w := httptest.NewRecorder() par.DeleteACMEAccountPolicy(w, req) diff --git a/ca/ca.go b/ca/ca.go index 16a5c600..9252fff7 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -213,7 +213,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() mux.Route("/admin", func(r chi.Router) { adminAPI.Route(r, acmeAdminResponder, policyAdminResponder) }) From 1e03bbb1afb8f84f99ccd4d2b7a62f0051b4f1c8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 6 May 2022 14:11:10 -0700 Subject: [PATCH 194/241] Change types in the ACMEAdminResponder --- authority/admin/api/acme.go | 17 +++++---- authority/admin/api/handler.go | 69 +++++++++++++++++++--------------- authority/admin/api/policy.go | 2 +- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 814ca226..db393e9a 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -53,32 +53,33 @@ func requireEABEnabled(next http.HandlerFunc) http.HandlerFunc { } } -type acmeAdminResponderInterface interface { +// ACMEAdminResponder is responsible for writing ACME admin responses +type ACMEAdminResponder interface { GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) } -// ACMEAdminResponder is responsible for writing ACME admin responses -type ACMEAdminResponder struct{} +// acmeAdminResponder implements ACMEAdminResponder. +type acmeAdminResponder struct{} // NewACMEAdminResponder returns a new ACMEAdminResponder -func NewACMEAdminResponder() *ACMEAdminResponder { - return &ACMEAdminResponder{} +func NewACMEAdminResponder() ACMEAdminResponder { + return &acmeAdminResponder{} } // GetExternalAccountKeys writes the response for the EAB keys GET endpoint -func (h *ACMEAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { +func (h *acmeAdminResponder) GetExternalAccountKeys(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")) } // CreateExternalAccountKey writes the response for the EAB key POST endpoint -func (h *ACMEAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { +func (h *acmeAdminResponder) CreateExternalAccountKey(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")) } // DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint -func (h *ACMEAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { +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")) } diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 0ab417e6..1e5919ce 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -12,19 +12,21 @@ import ( // Handler is the Admin API request handler. type Handler struct { - acmeResponder acmeAdminResponderInterface + acmeResponder ACMEAdminResponder policyResponder PolicyAdminResponder } // Route traffic and implement the Router interface. // -// Deprecated: use Route(r api.Router, acmeResponder acmeAdminResponderInterface) +// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) func (h *Handler) Route(r api.Router) { Route(r, h.acmeResponder, h.policyResponder) } // NewHandler returns a new Authority Config Handler. -func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface, policyResponder PolicyAdminResponder) api.RouterHandler { +// +// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) +func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) api.RouterHandler { return &Handler{ acmeResponder: acmeResponder, policyResponder: policyResponder, @@ -36,7 +38,7 @@ var mustAuthority = func(ctx context.Context) adminAuthority { } // Route traffic and implement the Router interface. -func Route(r api.Router, acmeResponder acmeAdminResponderInterface, policyResponder PolicyAdminResponder) { +func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) { authnz := func(next http.HandlerFunc) http.HandlerFunc { return extractAuthorizeTokenAdmin(requireAPIEnabled(next)) } @@ -79,32 +81,37 @@ func Route(r api.Router, acmeResponder acmeAdminResponderInterface, policyRespon r.MethodFunc("PATCH", "/admins/{id}", authnz(UpdateAdmin)) r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin)) - // ACME External Account Binding Keys - r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys)) - r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys)) - r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey)) - r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey)) - - // Policy - Authority - r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy)) - r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy)) - r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy)) - r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy)) - - // Policy - Provisioner - r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy)) - r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy)) - r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy)) - r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy)) - - // Policy - ACME Account - r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy)) - r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy)) - r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy)) - r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy)) - r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy)) - r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy)) - r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy)) - r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy)) + // ACME responder + if acmeResponder != nil { + // ACME External Account Binding Keys + r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys)) + r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys)) + r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey)) + r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey)) + } + // Policy responder + if policyResponder != nil { + // Policy - Authority + r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy)) + r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy)) + r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy)) + r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy)) + + // Policy - Provisioner + r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy)) + r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy)) + r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy)) + r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy)) + + // Policy - ACME Account + r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy)) + r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy)) + r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy)) + r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy)) + r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy)) + r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy)) + r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy)) + r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy)) + } } diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 9f338c0b..a478c83c 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -32,7 +32,7 @@ type PolicyAdminResponder interface { DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) } -// policyAdminResponder is responsible for writing ACME admin responses. +// policyAdminResponder implements PolicyAdminResponder. type policyAdminResponder struct{} // NewACMEAdminResponder returns a new PolicyAdminResponder. From 688ae837a4beaa8ec1c147a8c33aa2f4c22ef561 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 7 May 2022 00:26:18 +0200 Subject: [PATCH 195/241] Add some tests for SCEP request decoding --- scep/api/api_test.go | 113 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 scep/api/api_test.go diff --git a/scep/api/api_test.go b/scep/api/api_test.go new file mode 100644 index 00000000..bdb51594 --- /dev/null +++ b/scep/api/api_test.go @@ -0,0 +1,113 @@ +// Package api implements a SCEP HTTP server. +package api + +import ( + "bytes" + "errors" + "net/http" + "net/http/httptest" + "reflect" + "testing" + "testing/iotest" +) + +func Test_decodeRequest(t *testing.T) { + type args struct { + r *http.Request + } + tests := []struct { + name string + args args + want request + wantErr bool + }{ + { + name: "fail/unsupported-method", + args: args{ + r: httptest.NewRequest(http.MethodPatch, "http://scep:8080/?operation=AnUnsupportOperation", nil), + }, + want: request{}, + wantErr: true, + }, + { + name: "fail/get-unsupported-operation", + args: args{ + r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=AnUnsupportOperation", nil), + }, + want: request{}, + wantErr: true, + }, + { + name: "fail/get-PKIOperation", + args: args{ + r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=PKIOperation&message='somewronginput'", nil), + }, + want: request{}, + wantErr: true, + }, + { + name: "fail/post-PKIOperation", + args: args{ + r: httptest.NewRequest(http.MethodPost, "http://scep:8080/?operation=PKIOperation", iotest.ErrReader(errors.New("a read error"))), + }, + want: request{}, + wantErr: true, + }, + { + name: "ok/get-GetCACert", + args: args{ + r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=GetCACert", nil), + }, + want: request{ + Operation: "GetCACert", + Message: []byte{}, + }, + wantErr: false, + }, + { + name: "ok/get-GetCACaps", + args: args{ + r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=GetCACaps", nil), + }, + want: request{ + Operation: "GetCACaps", + Message: []byte{}, + }, + wantErr: false, + }, + { + name: "ok/get-PKIOperation", + args: args{ + r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=PKIOperation&message=MTIzNA==", nil), + }, + want: request{ + Operation: "PKIOperation", + Message: []byte("1234"), + }, + wantErr: false, + }, + { + name: "ok/post-PKIOperation", + args: args{ + r: httptest.NewRequest(http.MethodPost, "http://scep:8080/?operation=PKIOperation", bytes.NewBufferString("1234")), + }, + want: request{ + Operation: "PKIOperation", + Message: []byte("1234"), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeRequest(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeRequest() = %v, want %v", got, tt.want) + } + }) + } +} From 894242297350750f33d134545213cb1e2a8c9a76 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 May 2022 16:51:09 -0700 Subject: [PATCH 196/241] Add GetID() and add authority to initial context --- authority/authority.go | 28 +++++++++++++++++++--------- authority/authority_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index cdf2c8bf..c184c6e9 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -250,6 +250,7 @@ func (a *Authority) init() error { } var err error + ctx := NewContext(context.Background(), a) // Set password if they are not set. var configPassword []byte @@ -285,7 +286,7 @@ func (a *Authority) init() error { if a.config.KMS != nil { options = *a.config.KMS } - a.keyManager, err = kms.New(context.Background(), options) + a.keyManager, err = kms.New(ctx, options) if err != nil { return err } @@ -315,7 +316,7 @@ func (a *Authority) init() error { // Configure linked RA if linkedcaClient != nil && options.CertificateAuthority == "" { - conf, err := linkedcaClient.GetConfiguration(context.Background()) + conf, err := linkedcaClient.GetConfiguration(ctx) if err != nil { return err } @@ -349,7 +350,7 @@ func (a *Authority) init() error { } } - a.x509CAService, err = cas.New(context.Background(), options) + a.x509CAService, err = cas.New(ctx, options) if err != nil { return err } @@ -536,7 +537,7 @@ func (a *Authority) init() error { } } - a.scepService, err = scep.NewService(context.Background(), options) + a.scepService, err = scep.NewService(ctx, options) if err != nil { return err } @@ -558,19 +559,19 @@ func (a *Authority) init() error { } } - provs, err := a.adminDB.GetProvisioners(context.Background()) + provs, err := a.adminDB.GetProvisioners(ctx) if err != nil { return admin.WrapErrorISE(err, "error loading provisioners to initialize authority") } if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") { // Create First Provisioner - prov, err := CreateFirstProvisioner(context.Background(), a.adminDB, string(a.password)) + prov, err := CreateFirstProvisioner(ctx, a.adminDB, string(a.password)) if err != nil { return admin.WrapErrorISE(err, "error creating first provisioner") } // Create first admin - if err := a.adminDB.CreateAdmin(context.Background(), &linkedca.Admin{ + if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{ ProvisionerId: prov.Id, Subject: "step", Type: linkedca.Admin_SUPER_ADMIN, @@ -581,12 +582,12 @@ func (a *Authority) init() error { } // Load Provisioners and Admins - if err := a.reloadAdminResources(context.Background()); err != nil { + if err := a.reloadAdminResources(ctx); err != nil { return err } // Load x509 and SSH Policy Engines - if err := a.reloadPolicyEngines(context.Background()); err != nil { + if err := a.reloadPolicyEngines(ctx); err != nil { return err } @@ -611,6 +612,15 @@ func (a *Authority) init() error { return nil } +// GetID returns the define authority id or a zero uuid. +func (a *Authority) GetID() string { + const zeroUUID = "00000000-0000-0000-0000-000000000000" + if id := a.config.AuthorityConfig.AuthorityID; id != "" { + return id + } + return zeroUUID +} + // GetDatabase returns the authority database. If the configuration does not // define a database, GetDatabase will return a db.SimpleDB instance. func (a *Authority) GetDatabase() db.AuthDB { diff --git a/authority/authority_test.go b/authority/authority_test.go index 1f63333d..9f35f23e 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -14,6 +14,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "go.step.sm/crypto/jose" @@ -421,3 +422,31 @@ func TestAuthority_GetSCEPService(t *testing.T) { }) } } + +func TestAuthority_GetID(t *testing.T) { + type fields struct { + authorityID string + } + tests := []struct { + name string + fields fields + want string + }{ + {"ok", fields{""}, "00000000-0000-0000-0000-000000000000"}, + {"ok with id", fields{"10b9a431-ed3b-4a5f-abee-ec35119b65e7"}, "10b9a431-ed3b-4a5f-abee-ec35119b65e7"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + config: &config.Config{ + AuthorityConfig: &config.AuthConfig{ + AuthorityID: tt.fields.authorityID, + }, + }, + } + if got := a.GetID(); got != tt.want { + t.Errorf("Authority.GetID() = %v, want %v", got, tt.want) + } + }) + } +} From 7030dbb7a1c5badb204d4af614b890e2a590809e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 11 May 2022 21:18:47 +0200 Subject: [PATCH 197/241] Use github.com/smallstep/pkcs7 fork with patches applied --- go.mod | 3 +++ go.sum | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 36e39b4d..b55b7266 100644 --- a/go.mod +++ b/go.mod @@ -64,3 +64,6 @@ require ( // replace go.step.sm/crypto => ../crypto // replace go.step.sm/cli-utils => ../cli-utils // replace go.step.sm/linkedca => ../linkedca + +// use github.com/smallstep/pkcs7 fork with patches applied +replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6 diff --git a/go.sum b/go.sum index 55528b4f..0758ad69 100644 --- a/go.sum +++ b/go.sum @@ -735,6 +735,8 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/nosql v0.4.0 h1:Go3WYwttUuvwqMtFiiU4g7kBIlY+hR0bIZAqVdakQ3M= github.com/smallstep/nosql v0.4.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo= +github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6 h1:8Rjy6IZbSM/jcYgBWCoLIGjug7QcoLtF9sUuhDrHD2U= +github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -796,9 +798,6 @@ 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= -go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -815,8 +814,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.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= From 25b8d196d84542c97a946a963782a4d20c3c9ca9 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 11 May 2022 17:04:43 -0700 Subject: [PATCH 198/241] Couple changes in response to PR - add skipInit option to skip authority initialization - check admin API status when removing provisioners - no need to check admins when not using Admin API --- authority/authority.go | 32 +++++++++++++------------------- authority/options.go | 9 +++++++++ authority/provisioners.go | 30 ++++++++++++++++-------------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 63375351..5b08ec40 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -78,8 +78,12 @@ type Authority struct { authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc adminMutex sync.RWMutex + + // Do Not initialize the authority + skipInit bool } +// Info contains information about the authority. type Info struct { StartTime time.Time RootX509Certs []*x509.Certificate @@ -107,25 +111,13 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) { } } - // Initialize authority from options or configuration. - if err := a.init(); err != nil { - return nil, err - } - - return a, nil -} - -// FromOptions creates an Authority exclusively using the passed in options -// and does not initialize the Authority. -func FromOptions(opts ...Option) (*Authority, error) { - var a = new(Authority) - - // Apply options. - for _, fn := range opts { - if err := fn(a); err != nil { + if !a.skipInit { + // Initialize authority from options or configuration. + if err := a.init(); err != nil { return nil, err } } + return a, nil } @@ -159,9 +151,11 @@ func NewEmbedded(opts ...Option) (*Authority, error) { // Initialize config required fields. a.config.Init() - // Initialize authority from options or configuration. - if err := a.init(); err != nil { - return nil, err + if !a.skipInit { + // Initialize authority from options or configuration. + if err := a.init(); err != nil { + return nil, err + } } return a, nil diff --git a/authority/options.go b/authority/options.go index 1c154577..b583bb89 100644 --- a/authority/options.go +++ b/authority/options.go @@ -284,6 +284,15 @@ func WithX509Enforcers(ces ...provisioner.CertificateEnforcer) Option { } } +// WithSkipInit is an option that allows the constructor to skip initializtion +// of the authority. +func WithSkipInit() Option { + return func(a *Authority) error { + a.skipInit = true + return nil + } +} + func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) { var block *pem.Block var certs []*x509.Certificate diff --git a/authority/provisioners.go b/authority/provisioners.go index 5944f007..642bb5b1 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -243,27 +243,29 @@ func (a *Authority) RemoveProvisioner(ctx context.Context, id string) error { } provName, provID := p.GetName(), p.GetID() - // Validate - // - Check that there will be SUPER_ADMINs that remain after we - // remove this provisioner. - if a.admins.SuperCount() == a.admins.SuperCountByProvisioner(provName) { - return admin.NewError(admin.ErrorBadRequestType, - "cannot remove provisioner %s because no super admins will remain", provName) - } + if a.IsAdminAPIEnabled() { + // Validate + // - Check that there will be SUPER_ADMINs that remain after we + // remove this provisioner. + if a.IsAdminAPIEnabled() && a.admins.SuperCount() == a.admins.SuperCountByProvisioner(provName) { + return admin.NewError(admin.ErrorBadRequestType, + "cannot remove provisioner %s because no super admins will remain", provName) + } - // Delete all admins associated with the provisioner. - admins, ok := a.admins.LoadByProvisioner(provName) - if ok { - for _, adm := range admins { - if err := a.removeAdmin(ctx, adm.Id); err != nil { - return admin.WrapErrorISE(err, "error deleting admin %s, as part of provisioner %s deletion", adm.Subject, provName) + // Delete all admins associated with the provisioner. + admins, ok := a.admins.LoadByProvisioner(provName) + if ok { + for _, adm := range admins { + if err := a.removeAdmin(ctx, adm.Id); err != nil { + return admin.WrapErrorISE(err, "error deleting admin %s, as part of provisioner %s deletion", adm.Subject, provName) + } } } } // Remove provisioner from authority caches. if err := a.provisioners.Remove(provID); err != nil { - return admin.WrapErrorISE(err, "error removing admin from authority cache") + return admin.WrapErrorISE(err, "error removing provisioner from authority cache") } // Remove provisioner from database. if err := a.adminDB.DeleteProvisioner(ctx, provID); err != nil { From c695b23e24655eb838bc775aa674ee928fd7bb60 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 12 May 2022 16:33:32 +0200 Subject: [PATCH 199/241] Fix check for admin not belonging to policy --- authority/policy.go | 21 +++++--- authority/policy_test.go | 100 +++++++++++++++++++++++++++++++++----- authority/provisioners.go | 8 +-- 3 files changed, 104 insertions(+), 25 deletions(-) diff --git a/authority/policy.go b/authority/policy.go index 6348c690..258873af 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -137,7 +137,7 @@ func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *link return a.checkPolicy(ctx, currentAdmin, allAdmins, p) } -func (a *Authority) checkProvisionerPolicy(ctx context.Context, currentAdmin *linkedca.Admin, provName string, p *linkedca.Policy) error { +func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string, p *linkedca.Policy) error { // no policy and thus nothing to evaluate; return early if p == nil { @@ -147,7 +147,11 @@ func (a *Authority) checkProvisionerPolicy(ctx context.Context, currentAdmin *li // 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) + // check the policy; pass in nil as the current admin, as all admins for the + // provisioner will be checked by looping through allProvisionerAdmins. Also, + // the current admin may be a super admin not belonging to the provisioner, so + // can't be blocked, but is not required to be in the policy, either. + return a.checkPolicy(ctx, nil, allProvisionerAdmins, p) } // checkPolicy checks if a new or updated policy configuration results in the user @@ -178,10 +182,13 @@ func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admi // 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. - sans := []string{currentAdmin.GetSubject()} - if err := isAllowed(engine, sans); err != nil { - return err + // would be applied. This case is skipped when current admin is nil, which + // is the case when a provisioner policy is checked. + if currentAdmin != nil { + sans := []string{currentAdmin.GetSubject()} + if err := isAllowed(engine, sans); err != nil { + return err + } } // loop through admins to verify that none of them would be @@ -189,7 +196,7 @@ func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admi // an error with a message that includes the admin subject that // would be locked out. for _, adm := range otherAdmins { - sans = []string{adm.GetSubject()} + sans := []string{adm.GetSubject()} if err := isAllowed(engine, sans); err != nil { return err } diff --git a/authority/policy_test.go b/authority/policy_test.go index efeb743b..1dccf0d1 100644 --- a/authority/policy_test.go +++ b/authority/policy_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "gopkg.in/square/go-jose.v2" "go.step.sm/linkedca" @@ -917,13 +918,58 @@ func TestAuthority_checkAuthorityPolicy(t *testing.T) { }, wantErr: true, }, + { + name: "fail/policy", + fields: fields{ + admins: administrator.NewCollection(nil), + adminDB: &admin.MockDB{ + MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) { + return []*linkedca.Admin{ + { + Id: "adminID1", + Subject: "anotherAdmin", + }, + { + Id: "adminID2", + Subject: "step", + }, + { + Id: "adminID3", + Subject: "otherAdmin", + }, + }, nil + }, + }, + }, + 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 + return []*linkedca.Admin{ + { + Id: "adminID2", + Subject: "step", + }, + { + Id: "adminID3", + Subject: "otherAdmin", + }, + }, nil }, }, }, @@ -957,6 +1003,20 @@ func TestAuthority_checkAuthorityPolicy(t *testing.T) { } func TestAuthority_checkProvisionerPolicy(t *testing.T) { + jwkProvisioner := &provisioner.JWK{ + ID: "jwkID", + Type: "JWK", + Name: "jwkProv", + Key: &jose.JSONWebKey{KeyID: "jwkKeyID"}, + } + provisioners := provisioner.NewCollection(testAudiences) + provisioners.Store(jwkProvisioner) + admins := administrator.NewCollection(provisioners) + admins.Store(&linkedca.Admin{ + Id: "adminID", + Subject: "step", + ProvisionerId: "jwkID", + }, jwkProvisioner) type fields struct { provisioners *provisioner.Collection admins *administrator.Collection @@ -964,10 +1024,9 @@ func TestAuthority_checkProvisionerPolicy(t *testing.T) { adminDB admin.DB } type args struct { - ctx context.Context - currentAdmin *linkedca.Admin - provName string - p *linkedca.Policy + ctx context.Context + provName string + p *linkedca.Policy } tests := []struct { name string @@ -979,20 +1038,37 @@ func TestAuthority_checkProvisionerPolicy(t *testing.T) { name: "no policy", fields: fields{}, args: args{ - currentAdmin: nil, - provName: "prov", - p: nil, + provName: "prov", + p: nil, }, wantErr: false, }, + { + name: "fail/policy", + fields: fields{ + provisioners: provisioners, + admins: admins, + }, + args: args{ + provName: "jwkProv", + p: &linkedca.Policy{ + X509: &linkedca.X509Policy{ + Allow: &linkedca.X509Names{ + Dns: []string{"otherAdmin"}, // step not in policy + }, + }, + }, + }, + wantErr: true, + }, { name: "ok", fields: fields{ - admins: administrator.NewCollection(nil), + provisioners: provisioners, + admins: admins, }, args: args{ - currentAdmin: &linkedca.Admin{Subject: "step"}, - provName: "prov", + provName: "jwkProv", p: &linkedca.Policy{ X509: &linkedca.X509Policy{ Allow: &linkedca.X509Names{ @@ -1012,7 +1088,7 @@ func TestAuthority_checkProvisionerPolicy(t *testing.T) { 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 { + if err := a.checkProvisionerPolicy(tt.args.ctx, tt.args.provName, tt.args.p); (err != nil) != tt.wantErr { t.Errorf("Authority.checkProvisionerPolicy() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/authority/provisioners.go b/authority/provisioners.go index cde4a6e9..76c1a129 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -173,9 +173,7 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi return admin.WrapErrorISE(err, "error generating provisioner config") } - adm := linkedca.MustAdminFromContext(ctx) - - if err := a.checkProvisionerPolicy(ctx, adm, prov.Name, prov.Policy); err != nil { + if err := a.checkProvisionerPolicy(ctx, prov.Name, prov.Policy); err != nil { return err } @@ -224,9 +222,7 @@ func (a *Authority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisio return admin.WrapErrorISE(err, "error generating provisioner config") } - adm := linkedca.MustAdminFromContext(ctx) - - if err := a.checkProvisionerPolicy(ctx, adm, nu.Name, nu.Policy); err != nil { + if err := a.checkProvisionerPolicy(ctx, nu.Name, nu.Policy); err != nil { return err } From 400b1ece0bc19b217262712ea292cdeea44b0135 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 May 2022 17:39:36 -0700 Subject: [PATCH 200/241] Remove scep handler after merge. --- scep/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scep/api/api.go b/scep/api/api.go index f063df21..b738a933 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -109,7 +109,7 @@ func Get(w http.ResponseWriter, r *http.Request) { case opnGetCACaps: res, err = GetCACaps(ctx) case opnPKIOperation: - res, err = h.PKIOperation(ctx, req) + res, err = PKIOperation(ctx, req) default: err = fmt.Errorf("unknown operation: %s", req.Operation) } From b75ce3acbdfb61a74ce85adfad5db2f3dcc355b5 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 17 May 2022 23:39:01 +0200 Subject: [PATCH 201/241] Update to go.step.sm/crypto v0.16.2 This patch release of go.step.sm/crypto fixes an issue with not all `Subject` names being available for usage in a template as `ExtraNames`. --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 36e39b4d..8d0f935d 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( 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/crypto v0.16.2 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 diff --git a/go.sum b/go.sum index 55528b4f..cad29555 100644 --- a/go.sum +++ b/go.sum @@ -813,10 +813,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds= go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= 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.step.sm/crypto v0.16.2 h1:Pr9aazTwWBBZNogUsOqhOrPSdwAa9pPs+lMB602lnDA= +go.step.sm/crypto v0.16.2/go.mod h1:1WkTOTY+fOX/RY4TnZREp6trQAsBHRQ7nu6QJBiNQF8= 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= From bfb406bf703d716d98dc86c169acb92d49cb4cf4 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 18 May 2022 09:43:32 -0700 Subject: [PATCH 202/241] Fixes for PR review --- authority/admin/db.go | 46 ------------------------------------------- authority/options.go | 8 ++++++++ ca/adminClient.go | 8 ++++---- ca/client.go | 7 ++++--- 4 files changed, 16 insertions(+), 53 deletions(-) diff --git a/authority/admin/db.go b/authority/admin/db.go index 6e4e7c49..bf34a3c2 100644 --- a/authority/admin/db.go +++ b/authority/admin/db.go @@ -71,52 +71,6 @@ type DB interface { DeleteAdmin(ctx context.Context, id string) error } -type NoDB struct{} - -func NewNoDB() *NoDB { - return &NoDB{} -} - -func (n *NoDB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { - return nil -} - -func (n *NoDB) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) { - return nil, nil -} - -func (n *NoDB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) { - return nil, nil -} - -func (n *NoDB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { - return nil -} - -func (n *NoDB) DeleteProvisioner(ctx context.Context, id string) error { - return nil -} - -func (n *NoDB) CreateAdmin(ctx context.Context, admin *linkedca.Admin) error { - return nil -} - -func (n *NoDB) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) { - return nil, nil -} - -func (n *NoDB) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) { - return nil, nil -} - -func (n *NoDB) UpdateAdmin(ctx context.Context, prov *linkedca.Admin) error { - return nil -} - -func (n *NoDB) DeleteAdmin(ctx context.Context, id string) error { - return nil -} - // MockDB is an implementation of the DB interface that should only be used as // a mock in tests. type MockDB struct { diff --git a/authority/options.go b/authority/options.go index b583bb89..755e0fbc 100644 --- a/authority/options.go +++ b/authority/options.go @@ -266,6 +266,14 @@ func WithAdminDB(d admin.DB) Option { } } +// WithProvisioners is an option to set the provisioner collection. +func WithProvisioners(ps *provisioner.Collection) Option { + return func(a *Authority) error { + a.provisioners = ps + return nil + } +} + // WithLinkedCAToken is an option to set the authentication token used to enable // linked ca. func WithLinkedCAToken(token string) Option { diff --git a/ca/adminClient.go b/ca/adminClient.go index e898a898..90b0ab1d 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -369,12 +369,12 @@ func (c *AdminClient) GetProvisioner(opts ...ProvisionerOption) (*linkedca.Provi } var u *url.URL switch { - case len(o.ID) > 0: + case o.ID != "": u = c.endpoint.ResolveReference(&url.URL{ Path: "/admin/provisioners/id", RawQuery: o.rawQuery(), }) - case len(o.Name) > 0: + case o.Name != "": u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.Name)}) default: return nil, errors.New("must set either name or id in method options") @@ -478,12 +478,12 @@ func (c *AdminClient) RemoveProvisioner(opts ...ProvisionerOption) error { } switch { - case len(o.ID) > 0: + case o.ID != "": u = c.endpoint.ResolveReference(&url.URL{ Path: path.Join(adminURLPrefix, "provisioners/id"), RawQuery: o.rawQuery(), }) - case len(o.Name) > 0: + case o.Name != "": u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.Name)}) default: return errors.New("must set either name or id in method options") diff --git a/ca/client.go b/ca/client.go index 3871c749..44961357 100644 --- a/ca/client.go +++ b/ca/client.go @@ -435,6 +435,7 @@ type ProvisionerOptions struct { Name string } +// Apply caches provisioner options on a struct for later use. func (o *ProvisionerOptions) Apply(opts []ProvisionerOption) (err error) { for _, fn := range opts { if err = fn(o); err != nil { @@ -446,16 +447,16 @@ func (o *ProvisionerOptions) Apply(opts []ProvisionerOption) (err error) { func (o *ProvisionerOptions) rawQuery() string { v := url.Values{} - if len(o.Cursor) > 0 { + if o.Cursor != "" { v.Set("cursor", o.Cursor) } if o.Limit > 0 { v.Set("limit", strconv.Itoa(o.Limit)) } - if len(o.ID) > 0 { + if o.ID != "" { v.Set("id", o.ID) } - if len(o.Name) > 0 { + if o.Name != "" { v.Set("name", o.Name) } return v.Encode() From fff00aca785958596dec61691bcc06317218f33c Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 18 May 2022 15:56:40 -0700 Subject: [PATCH 203/241] Updates to issue templates --- .github/ISSUE_TEMPLATE/bug-report.yml | 56 +++++++++++++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 27 --------- .../ISSUE_TEMPLATE/documentation-request.md | 12 +++- .github/ISSUE_TEMPLATE/enhancement.md | 17 +++++- 4 files changed, 80 insertions(+), 32 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..64b4d103 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,56 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug", "needs triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Tell us how to reproduce this issue. + placeholder: These are the steps! + validations: + required: true + - type: textarea + id: your-env + attributes: + label: Your Environment + value: |- + * OS - + * `step-ca` Version - + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + - type: textarea + id: actual-behavior + attributes: + label: Actual Behavior + description: What happens instead? + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any other context about the problem here. + validations: + required: false + - type: textarea + id: contributing + attributes: + label: Contributing + value: | + Vote on this issue by adding a 👍 reaction. + To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already). + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 066c4951..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug, needs triage -assignees: '' - ---- - -### Subject of the issue -Describe your issue here. - -### Your environment -* OS - -* Version - - -### Steps to reproduce -Tell us how to reproduce this issue. Please provide a working demo, you can use [this template](https://plnkr.co/edit/XorWgI?p=preview) as a base. - -### Expected behaviour -Tell us what should happen - -### Actual behaviour -Tell us what happens instead - -### Additional context -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/documentation-request.md b/.github/ISSUE_TEMPLATE/documentation-request.md index 86d15328..cf0250ae 100644 --- a/.github/ISSUE_TEMPLATE/documentation-request.md +++ b/.github/ISSUE_TEMPLATE/documentation-request.md @@ -1,12 +1,20 @@ --- name: Documentation Request about: Request documentation for a feature -title: '' -labels: documentation, needs triage +title: '[Docs]:' +labels: docs, needs triage assignees: '' --- +## Hello! + + +- Vote on this issue by adding a 👍 reaction +- If you want to document this feature, comment to let us know (we'll work with you on design, scheduling, etc.) + +## Affected area/feature + +- Vote on this issue by adding a 👍 reaction +- If you want to implement this feature, comment to let us know (we'll work with you on design, scheduling, etc.) -### Why this is needed +## Issue details + + + +## Why is this needed? + + From 479eda73397a98f729c87b0253b7684921c251ba Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 19 May 2022 01:25:30 +0200 Subject: [PATCH 204/241] Improve error message when client renews with expired certificate When a client provides an expired certificate and `AllowAfterExpiry` is not enabled, the client would get a rather generic error with instructions to view the CA logs. Viewing the CA logs can be done when running `step-ca`, but they can't be accessed easily in the hosted solution. This commit returns a slightly more informational message to the client in this specific situation. --- authority/provisioner/controller.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/authority/provisioner/controller.go b/authority/provisioner/controller.go index 0ca40267..063ab50c 100644 --- a/authority/provisioner/controller.go +++ b/authority/provisioner/controller.go @@ -3,6 +3,7 @@ package provisioner import ( "context" "crypto/x509" + "net/http" "regexp" "strings" "time" @@ -131,7 +132,9 @@ func DefaultAuthorizeRenew(ctx context.Context, p *Controller, cert *x509.Certif return errs.Unauthorized("certificate is not yet valid" + " " + now.UTC().Format(time.RFC3339Nano) + " vs " + cert.NotBefore.Format(time.RFC3339Nano)) } if now.After(cert.NotAfter) && !p.Claimer.AllowRenewalAfterExpiry() { - return errs.Unauthorized("certificate has expired") + // return a custom 401 Unauthorized error with a clearer message for the client + // TODO(hs): these errors likely need to be refactored as a whole; HTTP status codes shouldn't be in this layer. + return errs.New(http.StatusUnauthorized, "The request lacked necessary authorization to be completed: certificate expired on %s", cert.NotAfter) } return nil From 20b2c6a2017ac0f370a7916b7fd2104575427347 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 May 2022 18:27:37 -0700 Subject: [PATCH 205/241] Extract cert storer methods from AuthDB To be able to extend the AuthDB with methods that also extend the provisioner we need to either create a new method or to split the interface. This change splits the interface so we can have a cleaner implementation. --- db/db.go | 9 +++++++-- db/simple.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/db/db.go b/db/db.go index eccaf801..8cd1db0f 100644 --- a/db/db.go +++ b/db/db.go @@ -50,14 +50,19 @@ type AuthDB interface { Revoke(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error GetCertificate(serialNumber string) (*x509.Certificate, error) - StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) IsSSHHost(name string) (bool, error) - StoreSSHCertificate(crt *ssh.Certificate) error GetSSHHostPrincipals() ([]string, error) Shutdown() error } +// CertificateStorer is an extension of AuthDB that allows to store +// certificates. +type CertificateStorer interface { + StoreCertificate(crt *x509.Certificate) error + StoreSSHCertificate(crt *ssh.Certificate) error +} + // DB is a wrapper over the nosql.DB interface. type DB struct { nosql.DB diff --git a/db/simple.go b/db/simple.go index 0e5426ec..a7e38de9 100644 --- a/db/simple.go +++ b/db/simple.go @@ -20,7 +20,7 @@ type SimpleDB struct { usedTokens *sync.Map } -func newSimpleDB(c *Config) (AuthDB, error) { +func newSimpleDB(c *Config) (*SimpleDB, error) { db := &SimpleDB{} db.usedTokens = new(sync.Map) return db, nil From de99c3cac00385ee20b92c1e74eaf7185ba69ce5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 May 2022 18:30:53 -0700 Subject: [PATCH 206/241] Report provisioner and parent on linkedca --- authority/linkedca.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/authority/linkedca.go b/authority/linkedca.go index 0552f2d1..9daba353 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -292,11 +292,22 @@ func (c *linkedCaClient) StoreRenewedCertificate(parent *x509.Certificate, fullc return errors.Wrap(err, "error posting certificate") } -func (c *linkedCaClient) StoreSSHCertificate(crt *ssh.Certificate) error { +func (c *linkedCaClient) StoreSSHCertificate(prov provisioner.Interface, crt *ssh.Certificate) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{ Certificate: string(ssh.MarshalAuthorizedKey(crt)), + Provisioner: createProvisionerIdentity(prov), + }) + return errors.Wrap(err, "error posting ssh certificate") +} + +func (c *linkedCaClient) StoreRenewedSSHCertificate(parent, crt *ssh.Certificate) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + _, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{ + Certificate: string(ssh.MarshalAuthorizedKey(crt)), + ParentCertificate: string(ssh.MarshalAuthorizedKey(parent)), }) return errors.Wrap(err, "error posting ssh certificate") } From c8d7ad7ab92d2f3963ef6e9559d89d7c1562f7fc Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 May 2022 18:33:22 -0700 Subject: [PATCH 207/241] Fix store certificates methods with new interface --- authority/tls.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index d23b0da7..fd21ae98 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -365,28 +365,31 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 // `StoreCertificate(...*x509.Certificate) error` instead of just // `StoreCertificate(*x509.Certificate) error`. func (a *Authority) storeCertificate(prov provisioner.Interface, fullchain []*x509.Certificate) error { - type linkedChainStorer interface { + type certificateChainStorer interface { StoreCertificateChain(provisioner.Interface, ...*x509.Certificate) error } - type certificateChainStorer interface { + type certificateChainSimpleStorer interface { StoreCertificateChain(...*x509.Certificate) error } + // Store certificate in linkedca switch s := a.adminDB.(type) { - case linkedChainStorer: - return s.StoreCertificateChain(prov, fullchain...) case certificateChainStorer: + return s.StoreCertificateChain(prov, fullchain...) + case certificateChainSimpleStorer: return s.StoreCertificateChain(fullchain...) } // Store certificate in local db switch s := a.db.(type) { - case linkedChainStorer: - return s.StoreCertificateChain(prov, fullchain...) case certificateChainStorer: + return s.StoreCertificateChain(prov, fullchain...) + case certificateChainSimpleStorer: return s.StoreCertificateChain(fullchain...) + case db.CertificateStorer: + return s.StoreCertificate(fullchain[0]) default: - return a.db.StoreCertificate(fullchain[0]) + return nil } } @@ -398,15 +401,21 @@ func (a *Authority) storeRenewedCertificate(oldCert *x509.Certificate, fullchain type renewedCertificateChainStorer interface { StoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error } + // Store certificate in linkedca if s, ok := a.adminDB.(renewedCertificateChainStorer); ok { return s.StoreRenewedCertificate(oldCert, fullchain...) } + // Store certificate in local db - if s, ok := a.db.(renewedCertificateChainStorer); ok { + switch s := a.db.(type) { + case renewedCertificateChainStorer: return s.StoreRenewedCertificate(oldCert, fullchain...) + case db.CertificateStorer: + return s.StoreCertificate(fullchain[0]) + default: + return nil } - return a.db.StoreCertificate(fullchain[0]) } // RevokeOptions are the options for the Revoke API. From 293586079af39d21b10510ee6ebc472c33ff4028 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 May 2022 18:33:53 -0700 Subject: [PATCH 208/241] Store provisioner with SignSSH This change also allows to store the old certificate on renewal on linkedca or if the db interface supports it. --- authority/ssh.go | 59 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/authority/ssh.go b/authority/ssh.go index 0521ab58..bb69f9db 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -161,8 +161,13 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi // Set backdate with the configured value opts.Backdate = a.config.AuthorityConfig.Backdate.Duration + var prov provisioner.Interface for _, op := range signOpts { switch o := op.(type) { + // Capture current provisioner + case provisioner.Interface: + prov = o + // add options to NewCertificate case provisioner.SSHCertificateOptions: certOptions = append(certOptions, o.Options(opts)...) @@ -276,7 +281,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi } } - if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeSSHCertificate(prov, cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db") } @@ -340,7 +345,7 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate") } - if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeRenewedSSHCertificate(oldCert, cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db") } @@ -419,21 +424,59 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub } } - if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeRenewedSSHCertificate(oldCert, cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db") } return cert, nil } -func (a *Authority) storeSSHCertificate(cert *ssh.Certificate) error { +func (a *Authority) storeSSHCertificate(prov provisioner.Interface, cert *ssh.Certificate) error { type sshCertificateStorer interface { - StoreSSHCertificate(crt *ssh.Certificate) error + StoreSSHCertificate(provisioner.Interface, *ssh.Certificate) error + } + + // Store certificate in admindb or linkedca + switch s := a.adminDB.(type) { + case sshCertificateStorer: + return s.StoreSSHCertificate(prov, cert) + case db.CertificateStorer: + return s.StoreSSHCertificate(cert) + } + + // Store certificate in localdb + switch s := a.db.(type) { + case sshCertificateStorer: + return s.StoreSSHCertificate(prov, cert) + case db.CertificateStorer: + return s.StoreSSHCertificate(cert) + default: + return nil } - if s, ok := a.adminDB.(sshCertificateStorer); ok { +} + +func (a *Authority) storeRenewedSSHCertificate(parent, cert *ssh.Certificate) error { + type sshRenewerCertificateStorer interface { + StoreRenewedSSHCertificate(parent, cert *ssh.Certificate) error + } + + // Store certificate in admindb or linkedca + switch s := a.adminDB.(type) { + case sshRenewerCertificateStorer: + return s.StoreRenewedSSHCertificate(parent, cert) + case db.CertificateStorer: return s.StoreSSHCertificate(cert) } - return a.db.StoreSSHCertificate(cert) + + // Store certificate in localdb + switch s := a.db.(type) { + case sshRenewerCertificateStorer: + return s.StoreRenewedSSHCertificate(parent, cert) + case db.CertificateStorer: + return s.StoreSSHCertificate(cert) + default: + return nil + } } // IsValidForAddUser checks if a user provisioner certificate can be issued to @@ -511,7 +554,7 @@ func (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subje } cert.Signature = sig - if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeRenewedSSHCertificate(subject, cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error storing certificate in db") } From e7d7eb1a947bd60cc70fc67abe775c9f6060a29e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 May 2022 18:42:42 -0700 Subject: [PATCH 209/241] Add provisioner as a signOption for SSH --- authority/provisioner/aws.go | 1 + authority/provisioner/azure.go | 1 + authority/provisioner/gcp.go | 1 + authority/provisioner/jwk.go | 1 + authority/provisioner/k8sSA.go | 1 + authority/provisioner/nebula.go | 1 + authority/provisioner/noop.go | 2 +- authority/provisioner/oidc.go | 1 + authority/provisioner/x5c.go | 1 + 9 files changed, 9 insertions(+), 1 deletion(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 8433fde5..afc61dd7 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -747,6 +747,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, signOptions = append(signOptions, templateOptions) return append(signOptions, + p, // Validate user SignSSHOptions. sshCertOptionsValidator(defaults), // Set the validity bounds if not set. diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 438ab5b3..b6f7ec91 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -418,6 +418,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio signOptions = append(signOptions, templateOptions) return append(signOptions, + p, // Validate user SignSSHOptions. sshCertOptionsValidator(defaults), // Set the validity bounds if not set. diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 94c19e17..a116312d 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -425,6 +425,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, signOptions = append(signOptions, templateOptions) return append(signOptions, + p, // Validate user SignSSHOptions. sshCertOptionsValidator(defaults), // Set the validity bounds if not set. diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 336736db..de592941 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -257,6 +257,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, } return append(signOptions, + p, // Set the validity bounds if not set. &sshDefaultDuration{p.ctl.Claimer}, // Validate public key diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index e2dbf840..28be0d5c 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -275,6 +275,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio signOptions := []SignOption{templateOptions} return append(signOptions, + p, // Require type, key-id and principals in the SignSSHOptions. &sshCertOptionsRequireValidator{CertType: true, KeyID: true, Principals: true}, // Set the validity bounds if not set. diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index 38a2409f..cde5857c 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -250,6 +250,7 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti } return append(signOptions, + p, templateOptions, // Checks the validity bounds, and set the validity if has not been set. &sshLimitDuration{p.ctl.Claimer, crt.Details.NotAfter}, diff --git a/authority/provisioner/noop.go b/authority/provisioner/noop.go index 39661e54..9ccd0c8c 100644 --- a/authority/provisioner/noop.go +++ b/authority/provisioner/noop.go @@ -50,7 +50,7 @@ func (p *noop) AuthorizeRevoke(ctx context.Context, token string) error { } func (p *noop) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { - return []SignOption{}, nil + return []SignOption{p}, nil } func (p *noop) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 9f389b29..e64d98d9 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -434,6 +434,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption } return append(signOptions, + o, // Set the validity bounds if not set. &sshDefaultDuration{o.ctl.Claimer}, // Validate public key diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 69576da5..b9ae24c5 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -312,6 +312,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, } return append(signOptions, + p, // Checks the validity bounds, and set the validity if has not been set. &sshLimitDuration{p.ctl.Claimer, claims.chains[0][0].NotAfter}, // Validate public key. From a627f2144039e9b2e56020e18ced2f7b6b80c2cf Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 May 2022 18:51:36 -0700 Subject: [PATCH 210/241] Fix AuthorizeSSHSign tests with extra SignOption --- authority/authorize_test.go | 2 +- authority/provisioner/aws_test.go | 1 - authority/provisioner/k8sSA_test.go | 3 ++- authority/provisioner/ssh_test.go | 1 + authority/provisioner/x5c_test.go | 5 +++-- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/authority/authorize_test.go b/authority/authorize_test.go index 087318be..b221d0de 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -1034,7 +1034,7 @@ func TestAuthority_authorizeSSHSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Len(t, 8, got) // number of provisioner.SignOptions returned + assert.Len(t, 9, got) // number of provisioner.SignOptions returned } } }) diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 0d9b5d4d..d12d0626 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -813,7 +813,6 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) { } else if assert.NotNil(t, got) { cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) if (err != nil) != tt.wantSignErr { - t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr) } else { if tt.wantSignErr { diff --git a/authority/provisioner/k8sSA_test.go b/authority/provisioner/k8sSA_test.go index 1eff379d..2458babb 100644 --- a/authority/provisioner/k8sSA_test.go +++ b/authority/provisioner/k8sSA_test.go @@ -368,9 +368,10 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) { } else { if assert.Nil(t, tc.err) { if assert.NotNil(t, opts) { - assert.Len(t, 7, opts) + assert.Len(t, 8, opts) for _, o := range opts { switch v := o.(type) { + case Interface: case sshCertificateOptionsFunc: case *sshCertOptionsRequireValidator: assert.Equals(t, v, &sshCertOptionsRequireValidator{CertType: true, KeyID: true, Principals: true}) diff --git a/authority/provisioner/ssh_test.go b/authority/provisioner/ssh_test.go index c530cd3c..90271443 100644 --- a/authority/provisioner/ssh_test.go +++ b/authority/provisioner/ssh_test.go @@ -53,6 +53,7 @@ func signSSHCertificate(key crypto.PublicKey, opts SignSSHOptions, signOpts []Si for _, op := range signOpts { switch o := op.(type) { + case Interface: // add options to NewCertificate case SSHCertificateOptions: certOptions = append(certOptions, o.Options(opts)...) diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index f28fcc7c..3bcf30d1 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -769,6 +769,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { nw := now() for _, o := range opts { switch v := o.(type) { + case Interface: case sshCertOptionsValidator: tc.claims.Step.SSH.ValidAfter.t = time.Time{} tc.claims.Step.SSH.ValidBefore.t = time.Time{} @@ -799,9 +800,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { tot++ } if len(tc.claims.Step.SSH.CertType) > 0 { - assert.Equals(t, tot, 10) + assert.Equals(t, tot, 11) } else { - assert.Equals(t, tot, 8) + assert.Equals(t, tot, 9) } } } From dd985ce154a4abd7733e303679df2698b365c07b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 19 May 2022 18:41:13 -0700 Subject: [PATCH 211/241] Clarify errors when sending renewed certificates --- authority/linkedca.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authority/linkedca.go b/authority/linkedca.go index 9daba353..fd5c0a81 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -289,7 +289,7 @@ func (c *linkedCaClient) StoreRenewedCertificate(parent *x509.Certificate, fullc PemCertificateChain: serializeCertificateChain(fullchain[1:]...), PemParentCertificate: serializeCertificateChain(parent), }) - return errors.Wrap(err, "error posting certificate") + return errors.Wrap(err, "error posting renewed certificate") } func (c *linkedCaClient) StoreSSHCertificate(prov provisioner.Interface, crt *ssh.Certificate) error { @@ -309,7 +309,7 @@ func (c *linkedCaClient) StoreRenewedSSHCertificate(parent, crt *ssh.Certificate Certificate: string(ssh.MarshalAuthorizedKey(crt)), ParentCertificate: string(ssh.MarshalAuthorizedKey(parent)), }) - return errors.Wrap(err, "error posting ssh certificate") + return errors.Wrap(err, "error posting renewed ssh certificate") } func (c *linkedCaClient) Revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error { From 1ad75a3bdb10c77de7d8dc671b5000d2c27ac563 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 19 May 2022 18:51:51 -0700 Subject: [PATCH 212/241] Skip failing test for now This test fails randomly on VMs, there's an issue to fix this so skipping it for now --- ca/bootstrap_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index 9aaa5f1f..9eb3b2ea 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -370,6 +370,7 @@ func TestBootstrapClient(t *testing.T) { } func TestBootstrapClientServerRotation(t *testing.T) { + t.Skipf("skip until we fix https://github.com/smallstep/certificates/issues/873") reset := setMinCertDuration(1 * time.Second) defer reset() From 586e4fd3b5b9285ed0629ade4a121bf5ee65457c Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 19 May 2022 22:26:20 -0700 Subject: [PATCH 213/241] Update authority/options.go Co-authored-by: Mariano Cano --- authority/options.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/authority/options.go b/authority/options.go index 755e0fbc..429ccb91 100644 --- a/authority/options.go +++ b/authority/options.go @@ -267,6 +267,8 @@ func WithAdminDB(d admin.DB) Option { } // WithProvisioners is an option to set the provisioner collection. +// +// Deprecated: provisioner collections will likely change func WithProvisioners(ps *provisioner.Collection) Option { return func(a *Authority) error { a.provisioners = ps From 8ca9442fe9f2f5dd1e8f48d7bf7270cff3a18c62 Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 19 May 2022 22:40:12 -0700 Subject: [PATCH 214/241] Add -s to make fmt and bump golangci-lint to 1.45.2 --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- Makefile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c90d949a..807cfdd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: uses: golangci/golangci-lint-action@v2 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: 'v1.45.0' + version: 'v1.45.2' # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b24426a0..046589af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: uses: golangci/golangci-lint-action@v2 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: 'v1.45.0' + version: 'v1.45.2' # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/Makefile b/Makefile index 09e342df..906569f1 100644 --- a/Makefile +++ b/Makefile @@ -151,7 +151,7 @@ integration: bin/$(BINNAME) ######################################### fmt: - $Q gofmt -l -w $(SRC) + $Q gofmt -l -s -w $(SRC) lint: $Q golangci-lint run --timeout=30m From 5443aa073a40c64bb91b0b6535abec6cb1d0f735 Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 19 May 2022 22:46:25 -0700 Subject: [PATCH 215/241] gofmt -s --- authority/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/options.go b/authority/options.go index 429ccb91..6e1949f5 100644 --- a/authority/options.go +++ b/authority/options.go @@ -267,7 +267,7 @@ func WithAdminDB(d admin.DB) Option { } // WithProvisioners is an option to set the provisioner collection. -// +// // Deprecated: provisioner collections will likely change func WithProvisioners(ps *provisioner.Collection) Option { return func(a *Authority) error { From eebbd65dd534091bdbaa610988f278eb56a27c5a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 20 May 2022 12:03:36 -0700 Subject: [PATCH 216/241] Fix linter error --- ca/bootstrap_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index 9eb3b2ea..10e22519 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "net/http/httptest" + "os" "reflect" "strings" "sync" @@ -370,7 +371,9 @@ func TestBootstrapClient(t *testing.T) { } func TestBootstrapClientServerRotation(t *testing.T) { - t.Skipf("skip until we fix https://github.com/smallstep/certificates/issues/873") + if os.Getenv("CI") == "true" { + t.Skipf("skip until we fix https://github.com/smallstep/certificates/issues/873") + } reset := setMinCertDuration(1 * time.Second) defer reset() From 6b3a8f22f3ba282e425b20b9e17681eb74940226 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 20 May 2022 14:41:44 -0700 Subject: [PATCH 217/241] Add provisioner to SSH renewals This commit allows to report the provisioner to the linkedca when a SSH certificate is renewed. --- api/ssh.go | 1 + api/sshRekey.go | 1 + api/sshRenew.go | 1 + authority/authorize.go | 45 +++++++++++++++++----------- authority/authorize_test.go | 42 +++++++++++++------------- authority/linkedca.go | 21 ++++++------- authority/provisioner/method.go | 13 ++++++++ authority/provisioner/sshpop.go | 1 + authority/provisioner/sshpop_test.go | 3 +- authority/ssh.go | 30 ++++++++++++++----- 10 files changed, 101 insertions(+), 57 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 3b0de7c1..df96396f 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -288,6 +288,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { } ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHSignMethod) + ctx = provisioner.NewContextWithToken(ctx, body.OTT) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) diff --git a/api/sshRekey.go b/api/sshRekey.go index 92278950..1819428a 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -59,6 +59,7 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { } ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRekeyMethod) + ctx = provisioner.NewContextWithToken(ctx, body.OTT) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) diff --git a/api/sshRenew.go b/api/sshRenew.go index 78d16fa6..58f2e525 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -51,6 +51,7 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { } ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRenewMethod) + ctx = provisioner.NewContextWithToken(ctx, body.OTT) _, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { render.Error(w, errs.UnauthorizedErr(err)) diff --git a/authority/authorize.go b/authority/authorize.go index 7f9f456c..21e02069 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "fmt" "net/http" "net/url" "strconv" @@ -41,14 +42,12 @@ func SkipTokenReuseFromContext(ctx context.Context) bool { return m } -// authorizeToken parses the token and returns the provisioner used to generate -// the token. This method enforces the One-Time use policy (tokens can only be -// used once). -func (a *Authority) authorizeToken(ctx context.Context, token string) (provisioner.Interface, error) { - // Validate payload +// getProvisionerFromToken extracts a provisioner from the given token without +// doing any token validation. +func (a *Authority) getProvisionerFromToken(token string) (provisioner.Interface, *Claims, error) { tok, err := jose.ParseSigned(token) if err != nil { - return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeToken: error parsing token") + return nil, nil, fmt.Errorf("error parsing token: %w", err) } // Get claims w/out verification. We need to look up the provisioner @@ -56,7 +55,25 @@ func (a *Authority) authorizeToken(ctx context.Context, token string) (provision // before we can look up the provisioner. var claims Claims if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil { - return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeToken") + return nil, nil, fmt.Errorf("error unmarshaling token: %w", err) + } + + // This method will also validate the audiences for JWK provisioners. + p, ok := a.provisioners.LoadByToken(tok, &claims.Claims) + if !ok { + return nil, nil, fmt.Errorf("provisioner not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")) + } + + return p, &claims, nil +} + +// authorizeToken parses the token and returns the provisioner used to generate +// the token. This method enforces the One-Time use policy (tokens can only be +// used once). +func (a *Authority) authorizeToken(ctx context.Context, token string) (provisioner.Interface, error) { + p, claims, err := a.getProvisionerFromToken(token) + if err != nil { + return nil, errs.UnauthorizedErr(err) } // TODO: use new persistence layer abstraction. @@ -64,17 +81,10 @@ func (a *Authority) authorizeToken(ctx context.Context, token string) (provision // This check is meant as a stopgap solution to the current lack of a persistence layer. if a.config.AuthorityConfig != nil && !a.config.AuthorityConfig.DisableIssuedAtCheck { if claims.IssuedAt != nil && claims.IssuedAt.Time().Before(a.startTime) { - return nil, errs.Unauthorized("authority.authorizeToken: token issued before the bootstrap of certificate authority") + return nil, errs.Unauthorized("token issued before the bootstrap of certificate authority") } } - // This method will also validate the audiences for JWK provisioners. - p, ok := a.provisioners.LoadByToken(tok, &claims.Claims) - if !ok { - return nil, errs.Unauthorized("authority.authorizeToken: provisioner "+ - "not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")) - } - // Store the token to protect against reuse unless it's skipped. // If we cannot get a token id from the provisioner, just hash the token. if !SkipTokenReuseFromContext(ctx) { @@ -188,11 +198,10 @@ func (a *Authority) UseToken(token string, prov provisioner.Interface) error { } ok, err := a.db.UseToken(reuseKey, token) if err != nil { - return errs.Wrap(http.StatusInternalServerError, err, - "authority.authorizeToken: failed when attempting to store token") + return errs.Wrap(http.StatusInternalServerError, err, "failed when attempting to store token") } if !ok { - return errs.Unauthorized("authority.authorizeToken: token already used") + return errs.Unauthorized("token already used") } } return nil diff --git a/authority/authorize_test.go b/authority/authorize_test.go index b221d0de..af80d3d3 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -114,7 +114,7 @@ func TestAuthority_authorizeToken(t *testing.T) { return &authorizeTest{ auth: a, token: "foo", - err: errors.New("authority.authorizeToken: error parsing token"), + err: errors.New("error parsing token"), code: http.StatusUnauthorized, } }, @@ -133,7 +133,7 @@ func TestAuthority_authorizeToken(t *testing.T) { return &authorizeTest{ auth: a, token: raw, - err: errors.New("authority.authorizeToken: token issued before the bootstrap of certificate authority"), + err: errors.New("token issued before the bootstrap of certificate authority"), code: http.StatusUnauthorized, } }, @@ -155,7 +155,7 @@ func TestAuthority_authorizeToken(t *testing.T) { return &authorizeTest{ auth: a, token: raw, - err: errors.New("authority.authorizeToken: provisioner not found or invalid audience (https://example.com/revoke)"), + err: errors.New("provisioner not found or invalid audience (https://example.com/revoke)"), code: http.StatusUnauthorized, } }, @@ -192,7 +192,7 @@ func TestAuthority_authorizeToken(t *testing.T) { return &authorizeTest{ auth: _a, token: raw, - err: errors.New("authority.authorizeToken: token already used"), + err: errors.New("token already used"), code: http.StatusUnauthorized, } }, @@ -227,7 +227,7 @@ func TestAuthority_authorizeToken(t *testing.T) { return &authorizeTest{ auth: _a, token: raw, - err: errors.New("authority.authorizeToken: token already used"), + err: errors.New("token already used"), code: http.StatusUnauthorized, } }, @@ -275,7 +275,7 @@ func TestAuthority_authorizeToken(t *testing.T) { return &authorizeTest{ auth: _a, token: raw, - err: errors.New("authority.authorizeToken: failed when attempting to store token: force"), + err: errors.New("failed when attempting to store token: force"), code: http.StatusInternalServerError, } }, @@ -300,7 +300,7 @@ func TestAuthority_authorizeToken(t *testing.T) { return &authorizeTest{ auth: _a, token: raw, - err: errors.New("authority.authorizeToken: token already used"), + err: errors.New("token already used"), code: http.StatusUnauthorized, } }, @@ -353,7 +353,7 @@ func TestAuthority_authorizeRevoke(t *testing.T) { return &authorizeTest{ auth: a, token: "foo", - err: errors.New("authority.authorizeRevoke: authority.authorizeToken: error parsing token"), + err: errors.New("authority.authorizeRevoke: error parsing token"), code: http.StatusUnauthorized, } }, @@ -437,7 +437,7 @@ func TestAuthority_authorizeSign(t *testing.T) { return &authorizeTest{ auth: a, token: "foo", - err: errors.New("authority.authorizeSign: authority.authorizeToken: error parsing token"), + err: errors.New("authority.authorizeSign: error parsing token"), code: http.StatusUnauthorized, } }, @@ -524,7 +524,7 @@ func TestAuthority_Authorize(t *testing.T) { auth: a, token: "foo", ctx: context.Background(), - err: errors.New("authority.Authorize: authority.authorizeSign: authority.authorizeToken: error parsing token"), + err: errors.New("authority.Authorize: authority.authorizeSign: error parsing token"), code: http.StatusUnauthorized, } }, @@ -533,7 +533,7 @@ func TestAuthority_Authorize(t *testing.T) { auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod), - err: errors.New("authority.Authorize: authority.authorizeSign: authority.authorizeToken: error parsing token"), + err: errors.New("authority.Authorize: authority.authorizeSign: error parsing token"), code: http.StatusUnauthorized, } }, @@ -559,7 +559,7 @@ func TestAuthority_Authorize(t *testing.T) { auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod), - err: errors.New("authority.Authorize: authority.authorizeRevoke: authority.authorizeToken: error parsing token"), + err: errors.New("authority.Authorize: authority.authorizeRevoke: error parsing token"), code: http.StatusUnauthorized, } }, @@ -585,7 +585,7 @@ func TestAuthority_Authorize(t *testing.T) { auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod), - err: errors.New("authority.Authorize: authority.authorizeSSHSign: authority.authorizeToken: error parsing token"), + err: errors.New("authority.Authorize: authority.authorizeSSHSign: error parsing token"), code: http.StatusUnauthorized, } }, @@ -615,7 +615,7 @@ func TestAuthority_Authorize(t *testing.T) { auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod), - err: errors.New("authority.Authorize: authority.authorizeSSHRenew: authority.authorizeToken: error parsing token"), + err: errors.New("authority.Authorize: authority.authorizeSSHRenew: error parsing token"), code: http.StatusUnauthorized, } }, @@ -659,7 +659,7 @@ func TestAuthority_Authorize(t *testing.T) { auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod), - err: errors.New("authority.Authorize: authority.authorizeSSHRevoke: authority.authorizeToken: error parsing token"), + err: errors.New("authority.Authorize: authority.authorizeSSHRevoke: error parsing token"), code: http.StatusUnauthorized, } }, @@ -685,7 +685,7 @@ func TestAuthority_Authorize(t *testing.T) { auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod), - err: errors.New("authority.Authorize: authority.authorizeSSHRekey: authority.authorizeToken: error parsing token"), + err: errors.New("authority.Authorize: authority.authorizeSSHRekey: error parsing token"), code: http.StatusUnauthorized, } }, @@ -988,7 +988,7 @@ func TestAuthority_authorizeSSHSign(t *testing.T) { return &authorizeTest{ auth: a, token: "foo", - err: errors.New("authority.authorizeSSHSign: authority.authorizeToken: error parsing token"), + err: errors.New("authority.authorizeSSHSign: error parsing token"), code: http.StatusUnauthorized, } }, @@ -1082,7 +1082,7 @@ func TestAuthority_authorizeSSHRenew(t *testing.T) { return &authorizeTest{ auth: a, token: "foo", - err: errors.New("authority.authorizeSSHRenew: authority.authorizeToken: error parsing token"), + err: errors.New("authority.authorizeSSHRenew: error parsing token"), code: http.StatusUnauthorized, } }, @@ -1190,7 +1190,7 @@ func TestAuthority_authorizeSSHRevoke(t *testing.T) { return &authorizeTest{ auth: a, token: "foo", - err: errors.New("authority.authorizeSSHRevoke: authority.authorizeToken: error parsing token"), + err: errors.New("authority.authorizeSSHRevoke: error parsing token"), code: http.StatusUnauthorized, } }, @@ -1282,7 +1282,7 @@ func TestAuthority_authorizeSSHRekey(t *testing.T) { return &authorizeTest{ auth: a, token: "foo", - err: errors.New("authority.authorizeSSHRekey: authority.authorizeToken: error parsing token"), + err: errors.New("authority.authorizeSSHRekey: error parsing token"), code: http.StatusUnauthorized, } }, @@ -1345,7 +1345,7 @@ func TestAuthority_authorizeSSHRekey(t *testing.T) { } else { if assert.Nil(t, tc.err) { assert.Equals(t, tc.cert.Serial, cert.Serial) - assert.Len(t, 3, signOpts) + assert.Len(t, 4, signOpts) } } }) diff --git a/authority/linkedca.go b/authority/linkedca.go index fd5c0a81..0b98f877 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -270,13 +270,13 @@ func (c *linkedCaClient) GetCertificateData(serial string) (*db.CertificateData, }, nil } -func (c *linkedCaClient) StoreCertificateChain(prov provisioner.Interface, fullchain ...*x509.Certificate) error { +func (c *linkedCaClient) StoreCertificateChain(p provisioner.Interface, fullchain ...*x509.Certificate) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _, err := c.client.PostCertificate(ctx, &linkedca.CertificateRequest{ PemCertificate: serializeCertificateChain(fullchain[0]), PemCertificateChain: serializeCertificateChain(fullchain[1:]...), - Provisioner: createProvisionerIdentity(prov), + Provisioner: createProvisionerIdentity(p), }) return errors.Wrap(err, "error posting certificate") } @@ -292,22 +292,23 @@ func (c *linkedCaClient) StoreRenewedCertificate(parent *x509.Certificate, fullc return errors.Wrap(err, "error posting renewed certificate") } -func (c *linkedCaClient) StoreSSHCertificate(prov provisioner.Interface, crt *ssh.Certificate) error { +func (c *linkedCaClient) StoreSSHCertificate(p provisioner.Interface, crt *ssh.Certificate) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{ Certificate: string(ssh.MarshalAuthorizedKey(crt)), - Provisioner: createProvisionerIdentity(prov), + Provisioner: createProvisionerIdentity(p), }) return errors.Wrap(err, "error posting ssh certificate") } -func (c *linkedCaClient) StoreRenewedSSHCertificate(parent, crt *ssh.Certificate) error { +func (c *linkedCaClient) StoreRenewedSSHCertificate(p provisioner.Interface, parent, crt *ssh.Certificate) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{ Certificate: string(ssh.MarshalAuthorizedKey(crt)), ParentCertificate: string(ssh.MarshalAuthorizedKey(parent)), + Provisioner: createProvisionerIdentity(p), }) return errors.Wrap(err, "error posting renewed ssh certificate") } @@ -380,14 +381,14 @@ func (c *linkedCaClient) DeleteAuthorityPolicy(ctx context.Context) error { return errors.New("not implemented yet") } -func createProvisionerIdentity(prov provisioner.Interface) *linkedca.ProvisionerIdentity { - if prov == nil { +func createProvisionerIdentity(p provisioner.Interface) *linkedca.ProvisionerIdentity { + if p == nil { return nil } return &linkedca.ProvisionerIdentity{ - Id: prov.GetID(), - Type: linkedca.Provisioner_Type(prov.GetType()), - Name: prov.GetName(), + Id: p.GetID(), + Type: linkedca.Provisioner_Type(p.GetType()), + Name: p.GetName(), } } diff --git a/authority/provisioner/method.go b/authority/provisioner/method.go index f5cd5221..01dda2ed 100644 --- a/authority/provisioner/method.go +++ b/authority/provisioner/method.go @@ -61,3 +61,16 @@ func MethodFromContext(ctx context.Context) Method { m, _ := ctx.Value(methodKey{}).(Method) return m } + +type tokenKey struct{} + +// NewContextWithToken creates a new context with the given token. +func NewContextWithToken(ctx context.Context, token string) context.Context { + return context.WithValue(ctx, tokenKey{}, token) +} + +// TokenFromContext returns the token stored in the given context. +func TokenFromContext(ctx context.Context) (string, bool) { + token, ok := ctx.Value(tokenKey{}).(string) + return token, ok +} diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index c3a1a639..c0246729 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -222,6 +222,7 @@ func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Cert return nil, nil, errs.BadRequest("sshpop certificate must be a host ssh certificate") } return claims.sshCert, []SignOption{ + p, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. diff --git a/authority/provisioner/sshpop_test.go b/authority/provisioner/sshpop_test.go index 13294866..1e026883 100644 --- a/authority/provisioner/sshpop_test.go +++ b/authority/provisioner/sshpop_test.go @@ -459,9 +459,10 @@ func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Len(t, 3, opts) + assert.Len(t, 4, opts) for _, o := range opts { switch v := o.(type) { + case Interface: case *sshDefaultPublicKeyValidator: case *sshCertDefaultValidator: case *sshCertValidityValidator: diff --git a/authority/ssh.go b/authority/ssh.go index bb69f9db..1fd7f2e8 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -303,6 +303,12 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss return nil, err } + // Attempt to extract the provisioner from the token. + var prov provisioner.Interface + if token, ok := provisioner.TokenFromContext(ctx); ok { + prov, _, _ = a.getProvisionerFromToken(token) + } + backdate := a.config.AuthorityConfig.Backdate.Duration duration := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second now := time.Now() @@ -345,7 +351,7 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate") } - if err = a.storeRenewedSSHCertificate(oldCert, cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db") } @@ -356,8 +362,12 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { var validators []provisioner.SSHCertValidator + var prov provisioner.Interface for _, op := range signOpts { switch o := op.(type) { + // Capture current provisioner + case provisioner.Interface: + prov = o // validate the ssh.Certificate case provisioner.SSHCertValidator: validators = append(validators, o) @@ -424,7 +434,7 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub } } - if err = a.storeRenewedSSHCertificate(oldCert, cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db") } @@ -455,15 +465,15 @@ func (a *Authority) storeSSHCertificate(prov provisioner.Interface, cert *ssh.Ce } } -func (a *Authority) storeRenewedSSHCertificate(parent, cert *ssh.Certificate) error { +func (a *Authority) storeRenewedSSHCertificate(prov provisioner.Interface, parent, cert *ssh.Certificate) error { type sshRenewerCertificateStorer interface { - StoreRenewedSSHCertificate(parent, cert *ssh.Certificate) error + StoreRenewedSSHCertificate(p provisioner.Interface, parent, cert *ssh.Certificate) error } // Store certificate in admindb or linkedca switch s := a.adminDB.(type) { case sshRenewerCertificateStorer: - return s.StoreRenewedSSHCertificate(parent, cert) + return s.StoreRenewedSSHCertificate(prov, parent, cert) case db.CertificateStorer: return s.StoreSSHCertificate(cert) } @@ -471,7 +481,7 @@ func (a *Authority) storeRenewedSSHCertificate(parent, cert *ssh.Certificate) er // Store certificate in localdb switch s := a.db.(type) { case sshRenewerCertificateStorer: - return s.StoreRenewedSSHCertificate(parent, cert) + return s.StoreRenewedSSHCertificate(prov, parent, cert) case db.CertificateStorer: return s.StoreSSHCertificate(cert) default: @@ -522,6 +532,12 @@ func (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subje return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error reading random number") } + // Attempt to extract the provisioner from the token. + var prov provisioner.Interface + if token, ok := provisioner.TokenFromContext(ctx); ok { + prov, _, _ = a.getProvisionerFromToken(token) + } + signer := a.sshCAUserCertSignKey principal := subject.ValidPrincipals[0] addUserPrincipal := a.getAddUserPrincipal() @@ -554,7 +570,7 @@ func (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subje } cert.Signature = sig - if err = a.storeRenewedSSHCertificate(subject, cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeRenewedSSHCertificate(prov, subject, cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error storing certificate in db") } From dec1067addc5bd8f43fafeaca7cd4f6264738abf Mon Sep 17 00:00:00 2001 From: Erik De Lamarter Date: Mon, 25 Apr 2022 22:45:22 +0200 Subject: [PATCH 218/241] vault kubernetes auth --- cas/vaultcas/vaultcas.go | 70 ++++++++++++++++++++++++++-------------- go.mod | 1 + go.sum | 2 ++ 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index c29ef691..8a09a850 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -18,6 +18,7 @@ import ( vault "github.com/hashicorp/vault/api" auth "github.com/hashicorp/vault/api/auth/approle" + kubeauth "github.com/hashicorp/vault/api/auth/kubernetes" ) func init() { @@ -34,6 +35,7 @@ type VaultOptions struct { PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"` PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"` + KubernetesRole string `json:"kubernetesRole,omitempty"` RoleID string `json:"roleID,omitempty"` SecretID auth.SecretID `json:"secretID,omitempty"` AppRole string `json:"appRole,omitempty"` @@ -77,31 +79,49 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { return nil, fmt.Errorf("unable to initialize vault client: %w", err) } - var appRoleAuth *auth.AppRoleAuth - if vc.IsWrappingToken { - appRoleAuth, err = auth.NewAppRoleAuth( - vc.RoleID, - &vc.SecretID, - auth.WithWrappingToken(), - auth.WithMountPath(vc.AppRole), + if vc.KubernetesRole != "" { + var kubernetesAuth *kubeauth.KubernetesAuth + kubernetesAuth, err = kubeauth.NewKubernetesAuth( + vc.KubernetesRole, ) + if err != nil { + return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err) + } + + authInfo, err := client.Auth().Login(ctx, kubernetesAuth) + if err != nil { + return nil, fmt.Errorf("unable to login to Kubernetes auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } } else { - appRoleAuth, err = auth.NewAppRoleAuth( - vc.RoleID, - &vc.SecretID, - auth.WithMountPath(vc.AppRole), - ) - } - if err != nil { - return nil, fmt.Errorf("unable to initialize AppRole auth method: %w", err) - } + var appRoleAuth *auth.AppRoleAuth + if vc.IsWrappingToken { + appRoleAuth, err = auth.NewAppRoleAuth( + vc.RoleID, + &vc.SecretID, + auth.WithWrappingToken(), + auth.WithMountPath(vc.AppRole), + ) + } else { + appRoleAuth, err = auth.NewAppRoleAuth( + vc.RoleID, + &vc.SecretID, + auth.WithMountPath(vc.AppRole), + ) + } + if err != nil { + return nil, fmt.Errorf("unable to initialize AppRole auth method: %w", err) + } - authInfo, err := client.Auth().Login(ctx, appRoleAuth) - if err != nil { - return nil, fmt.Errorf("unable to login to AppRole auth method: %w", err) - } - if authInfo == nil { - return nil, errors.New("no auth info was returned after login") + authInfo, err := client.Auth().Login(ctx, appRoleAuth) + if err != nil { + return nil, fmt.Errorf("unable to login to AppRole auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } } return &VaultCAS{ @@ -272,11 +292,11 @@ func loadOptions(config json.RawMessage) (*VaultOptions, error) { vc.PKIRoleEd25519 = vc.PKIRoleDefault } - if vc.RoleID == "" { - return nil, errors.New("vaultCAS config options must define `roleID`") + if vc.RoleID == "" && vc.KubernetesRole == "" { + return nil, errors.New("vaultCAS config options must define `roleID` or `kubernetesRole`") } - if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" { + if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" && vc.RoleID != "" { return nil, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") } diff --git a/go.mod b/go.mod index 8b66f470..0b772018 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( 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/hashicorp/vault/api/auth/kubernetes v0.1.0 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 diff --git a/go.sum b/go.sum index 4780111e..d76648c2 100644 --- a/go.sum +++ b/go.sum @@ -449,6 +449,8 @@ github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= github.com/hashicorp/vault/api/auth/approle v0.1.1 h1:R5yA+xcNvw1ix6bDuWOaLOq2L4L77zDCVsethNw97xQ= github.com/hashicorp/vault/api/auth/approle v0.1.1/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag= +github.com/hashicorp/vault/api/auth/kubernetes v0.1.0 h1:6BtyahbF4aQp8gg3ww0A/oIoqzbhpNP1spXU3nHE0n0= +github.com/hashicorp/vault/api/auth/kubernetes v0.1.0/go.mod h1:Pdgk78uIs0mgDOLvc3a+h/vYIT9rznw2sz+ucuH9024= github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY= github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= From 6c44291d8df63e16e662a9cc03ffa8783fa364ce Mon Sep 17 00:00:00 2001 From: Erik De Lamarter Date: Mon, 9 May 2022 13:27:37 +0200 Subject: [PATCH 219/241] refactor vault auth --- cas/vaultcas/auth/approle/approle.go | 46 ++++ cas/vaultcas/auth/approle/approle_test.go | 16 ++ cas/vaultcas/auth/kubernetes/kubernetes.go | 43 +++ .../auth/kubernetes/kubernetes_test.go | 21 ++ cas/vaultcas/auth/kubernetes/token | 1 + cas/vaultcas/vaultcas.go | 120 +++----- cas/vaultcas/vaultcas_test.go | 256 ++++-------------- 7 files changed, 220 insertions(+), 283 deletions(-) create mode 100644 cas/vaultcas/auth/approle/approle.go create mode 100644 cas/vaultcas/auth/approle/approle_test.go create mode 100644 cas/vaultcas/auth/kubernetes/kubernetes.go create mode 100644 cas/vaultcas/auth/kubernetes/kubernetes_test.go create mode 100644 cas/vaultcas/auth/kubernetes/token diff --git a/cas/vaultcas/auth/approle/approle.go b/cas/vaultcas/auth/approle/approle.go new file mode 100644 index 00000000..38d3c51c --- /dev/null +++ b/cas/vaultcas/auth/approle/approle.go @@ -0,0 +1,46 @@ +package approle + +import ( + "encoding/json" + "fmt" + + "github.com/hashicorp/vault/api/auth/approle" +) + +// AuthOptions defines the configuration options added using the +// VaultOptions.AuthOptions field when AuthType is approle +type AuthOptions struct { + RoleID string `json:"roleID,omitempty"` + SecretID string `json:"secretID,omitempty"` + IsWrappingToken bool `json:"isWrappingToken,omitempty"` +} + +func NewApproleAuthMethod(mountPath string, options json.RawMessage) (*approle.AppRoleAuth, error) { + var opts *AuthOptions + + err := json.Unmarshal(options, &opts) + if err != nil { + return nil, fmt.Errorf("error decoding AppRole auth options: %w", err) + } + + var approleAuth *approle.AppRoleAuth + + var loginOptions []approle.LoginOption + if mountPath != "" { + loginOptions = append(loginOptions, approle.WithMountPath(mountPath)) + } + if opts.IsWrappingToken { + loginOptions = append(loginOptions, approle.WithWrappingToken()) + } + + sid := approle.SecretID{ + FromString: opts.SecretID, + } + + approleAuth, err = approle.NewAppRoleAuth(opts.RoleID, &sid, loginOptions...) + if err != nil { + return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err) + } + + return approleAuth, nil +} diff --git a/cas/vaultcas/auth/approle/approle_test.go b/cas/vaultcas/auth/approle/approle_test.go new file mode 100644 index 00000000..ab7e6a97 --- /dev/null +++ b/cas/vaultcas/auth/approle/approle_test.go @@ -0,0 +1,16 @@ +package approle + +import ( + "encoding/json" + "testing" +) + +func TestKubernetes_NewKubernetesAuthMethod(t *testing.T) { + mountPath := "approle" + raw := `{"roleID": "roleID", "secretID": "secretIDwrapped", "isWrappedToken": true}` + + _, err := NewApproleAuthMethod(mountPath, json.RawMessage(raw)) + if err != nil { + t.Fatal(err) + } +} diff --git a/cas/vaultcas/auth/kubernetes/kubernetes.go b/cas/vaultcas/auth/kubernetes/kubernetes.go new file mode 100644 index 00000000..0c4db62f --- /dev/null +++ b/cas/vaultcas/auth/kubernetes/kubernetes.go @@ -0,0 +1,43 @@ +package kubernetes + +import ( + "encoding/json" + "fmt" + + "github.com/hashicorp/vault/api/auth/kubernetes" +) + +// AuthOptions defines the configuration options added using the +// VaultOptions.AuthOptions field when AuthType is kubernetes +type AuthOptions struct { + Role string `json:"role,omitempty"` + TokenPath string `json:"tokenPath,omitempty"` +} + +func NewKubernetesAuthMethod(mountPath string, options json.RawMessage) (*kubernetes.KubernetesAuth, error) { + var opts *AuthOptions + + err := json.Unmarshal(options, &opts) + if err != nil { + return nil, fmt.Errorf("error decoding Kubernetes auth options: %w", err) + } + + var kubernetesAuth *kubernetes.KubernetesAuth + + var loginOptions []kubernetes.LoginOption + if mountPath != "" { + loginOptions = append(loginOptions, kubernetes.WithMountPath(mountPath)) + } + if opts.TokenPath != "" { + loginOptions = append(loginOptions, kubernetes.WithServiceAccountTokenPath(opts.TokenPath)) + } + kubernetesAuth, err = kubernetes.NewKubernetesAuth( + opts.Role, + loginOptions..., + ) + if err != nil { + return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err) + } + + return kubernetesAuth, nil +} diff --git a/cas/vaultcas/auth/kubernetes/kubernetes_test.go b/cas/vaultcas/auth/kubernetes/kubernetes_test.go new file mode 100644 index 00000000..604f1898 --- /dev/null +++ b/cas/vaultcas/auth/kubernetes/kubernetes_test.go @@ -0,0 +1,21 @@ +package kubernetes + +import ( + "encoding/json" + "path" + "path/filepath" + "runtime" + "testing" +) + +func TestKubernetes_NewKubernetesAuthMethod(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + tokenPath := filepath.Join(path.Dir(filename), "token") + mountPath := "kubernetes" + raw := `{"role": "SomeRoleName", "tokenPath": "` + tokenPath + `"}` + + _, err := NewKubernetesAuthMethod(mountPath, json.RawMessage(raw)) + if err != nil { + t.Fatal(err) + } +} diff --git a/cas/vaultcas/auth/kubernetes/token b/cas/vaultcas/auth/kubernetes/token new file mode 100644 index 00000000..6745be67 --- /dev/null +++ b/cas/vaultcas/auth/kubernetes/token @@ -0,0 +1 @@ +token \ No newline at end of file diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 8a09a850..02c814b7 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -15,10 +15,10 @@ import ( "time" "github.com/smallstep/certificates/cas/apiv1" + "github.com/smallstep/certificates/cas/vaultcas/auth/approle" + "github.com/smallstep/certificates/cas/vaultcas/auth/kubernetes" vault "github.com/hashicorp/vault/api" - auth "github.com/hashicorp/vault/api/auth/approle" - kubeauth "github.com/hashicorp/vault/api/auth/kubernetes" ) func init() { @@ -30,16 +30,14 @@ func init() { // VaultOptions defines the configuration options added using the // apiv1.Options.Config field. type VaultOptions struct { - PKI string `json:"pki,omitempty"` - PKIRoleDefault string `json:"pkiRoleDefault,omitempty"` - PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` - PKIRoleEC string `json:"pkiRoleEC,omitempty"` - PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"` - KubernetesRole string `json:"kubernetesRole,omitempty"` - RoleID string `json:"roleID,omitempty"` - SecretID auth.SecretID `json:"secretID,omitempty"` - AppRole string `json:"appRole,omitempty"` - IsWrappingToken bool `json:"isWrappingToken,omitempty"` + PKIMountPath string `json:"pkiMountPath,omitempty"` + PKIRoleDefault string `json:"pkiRoleDefault,omitempty"` + PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` + PKIRoleEC string `json:"pkiRoleEC,omitempty"` + PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"` + AuthType string `json:"authType,omitempty"` + AuthMountPath string `json:"authMountPath,omitempty"` + AuthOptions json.RawMessage `json:"authOptions,omitempty"` } // VaultCAS implements a Certificate Authority Service using Hashicorp Vault. @@ -79,49 +77,25 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { return nil, fmt.Errorf("unable to initialize vault client: %w", err) } - if vc.KubernetesRole != "" { - var kubernetesAuth *kubeauth.KubernetesAuth - kubernetesAuth, err = kubeauth.NewKubernetesAuth( - vc.KubernetesRole, - ) - if err != nil { - return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err) - } - - authInfo, err := client.Auth().Login(ctx, kubernetesAuth) - if err != nil { - return nil, fmt.Errorf("unable to login to Kubernetes auth method: %w", err) - } - if authInfo == nil { - return nil, errors.New("no auth info was returned after login") - } - } else { - var appRoleAuth *auth.AppRoleAuth - if vc.IsWrappingToken { - appRoleAuth, err = auth.NewAppRoleAuth( - vc.RoleID, - &vc.SecretID, - auth.WithWrappingToken(), - auth.WithMountPath(vc.AppRole), - ) - } else { - appRoleAuth, err = auth.NewAppRoleAuth( - vc.RoleID, - &vc.SecretID, - auth.WithMountPath(vc.AppRole), - ) - } - if err != nil { - return nil, fmt.Errorf("unable to initialize AppRole auth method: %w", err) - } + var method vault.AuthMethod + switch vc.AuthType { + case "kubernetes": + method, err = kubernetes.NewKubernetesAuthMethod(vc.AuthMountPath, vc.AuthOptions) + case "approle": + method, err = approle.NewApproleAuthMethod(vc.AuthMountPath, vc.AuthOptions) + default: + return nil, fmt.Errorf("unknown auth type: %v", vc.AuthType) + } + if err != nil { + return nil, fmt.Errorf("unable to configure auth method: %w", err) + } - authInfo, err := client.Auth().Login(ctx, appRoleAuth) - if err != nil { - return nil, fmt.Errorf("unable to login to AppRole auth method: %w", err) - } - if authInfo == nil { - return nil, errors.New("no auth info was returned after login") - } + authInfo, err := client.Auth().Login(ctx, method) + if err != nil { + return nil, fmt.Errorf("unable to login to Kubernetes auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") } return &VaultCAS{ @@ -154,7 +128,7 @@ func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv // GetCertificateAuthority returns the root certificate of the certificate // authority using the configured fingerprint. func (v *VaultCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { - secret, err := v.client.Logical().Read(v.config.PKI + "/cert/ca_chain") + secret, err := v.client.Logical().Read(v.config.PKIMountPath + "/cert/ca_chain") if err != nil { return nil, fmt.Errorf("error reading ca chain: %w", err) } @@ -210,7 +184,7 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv vaultReq := map[string]interface{}{ "serial_number": formatSerialNumber(sn), } - _, err := v.client.Logical().Write(v.config.PKI+"/revoke/", vaultReq) + _, err := v.client.Logical().Write(v.config.PKIMountPath+"/revoke/", vaultReq) if err != nil { return nil, fmt.Errorf("error revoking certificate: %w", err) } @@ -244,7 +218,7 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time. "ttl": lifetime.Seconds(), } - secret, err := v.client.Logical().Write(v.config.PKI+"/sign/"+vaultPKIRole, vaultReq) + secret, err := v.client.Logical().Write(v.config.PKIMountPath+"/sign/"+vaultPKIRole, vaultReq) if err != nil { return nil, nil, fmt.Errorf("error signing certificate: %w", err) } @@ -267,21 +241,17 @@ func (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time. } func loadOptions(config json.RawMessage) (*VaultOptions, error) { - var vc *VaultOptions + // setup default values + vc := VaultOptions{ + PKIMountPath: "pki", + PKIRoleDefault: "default", + } err := json.Unmarshal(config, &vc) if err != nil { return nil, fmt.Errorf("error decoding vaultCAS config: %w", err) } - if vc.PKI == "" { - vc.PKI = "pki" // use default pki vault name - } - - if vc.PKIRoleDefault == "" { - vc.PKIRoleDefault = "default" // use default pki role name - } - if vc.PKIRoleRSA == "" { vc.PKIRoleRSA = vc.PKIRoleDefault } @@ -292,23 +262,7 @@ func loadOptions(config json.RawMessage) (*VaultOptions, error) { vc.PKIRoleEd25519 = vc.PKIRoleDefault } - if vc.RoleID == "" && vc.KubernetesRole == "" { - return nil, errors.New("vaultCAS config options must define `roleID` or `kubernetesRole`") - } - - if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" && vc.RoleID != "" { - return nil, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") - } - - if vc.PKI == "" { - vc.PKI = "pki" // use default pki vault name - } - - if vc.AppRole == "" { - vc.AppRole = "auth/approle" - } - - return vc, nil + return &vc, nil } func parseCertificates(pemCert string) []*x509.Certificate { diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 9f73a1ee..3c1f09a3 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -14,7 +14,6 @@ import ( "time" vault "github.com/hashicorp/vault/api" - auth "github.com/hashicorp/vault/api/auth/approle" "github.com/smallstep/certificates/cas/apiv1" "go.step.sm/crypto/pemutil" ) @@ -99,7 +98,7 @@ func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.RequestURI == "/v1/auth/auth/approle/login": + case r.RequestURI == "/v1/auth/approle/login": w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{ "auth": { @@ -183,11 +182,10 @@ func TestNew_register(t *testing.T) { CertificateAuthority: caURL.String(), CertificateAuthorityFingerprint: testRootFingerprint, Config: json.RawMessage(`{ - "PKI": "pki", + "PKIMountPath": "pki", "PKIRoleDefault": "pki-role", - "RoleID": "roleID", - "SecretID": {"FromString": "secretID"}, - "IsWrappingToken": false + "AuthType": "approle", + "AuthOptions": {"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false} }`), }) @@ -201,15 +199,13 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { _, client := testCAHelper(t) options := VaultOptions{ - PKI: "pki", - PKIRoleDefault: "role", - PKIRoleRSA: "rsa", - PKIRoleEC: "ec", - PKIRoleEd25519: "ed25519", - RoleID: "roleID", - SecretID: auth.SecretID{FromString: "secretID"}, - AppRole: "approle", - IsWrappingToken: false, + PKIMountPath: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + AuthType: "approle", + AuthOptions: json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`), } type fields struct { @@ -291,7 +287,7 @@ func TestVaultCAS_GetCertificateAuthority(t *testing.T) { } options := VaultOptions{ - PKI: "pki", + PKIMountPath: "pki", } rootCert := parseCertificates(testRootCertificate)[0] @@ -335,15 +331,13 @@ func TestVaultCAS_RevokeCertificate(t *testing.T) { _, client := testCAHelper(t) options := VaultOptions{ - PKI: "pki", - PKIRoleDefault: "role", - PKIRoleRSA: "rsa", - PKIRoleEC: "ec", - PKIRoleEd25519: "ed25519", - RoleID: "roleID", - SecretID: auth.SecretID{FromString: "secretID"}, - AppRole: "approle", - IsWrappingToken: false, + PKIMountPath: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + AuthType: "approle", + AuthOptions: json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`), } type fields struct { @@ -407,15 +401,13 @@ func TestVaultCAS_RenewCertificate(t *testing.T) { _, client := testCAHelper(t) options := VaultOptions{ - PKI: "pki", - PKIRoleDefault: "role", - PKIRoleRSA: "rsa", - PKIRoleEC: "ec", - PKIRoleEd25519: "ed25519", - RoleID: "roleID", - SecretID: auth.SecretID{FromString: "secretID"}, - AppRole: "approle", - IsWrappingToken: false, + PKIMountPath: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + AuthType: "approle", + AuthOptions: json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`), } type fields struct { @@ -464,202 +456,66 @@ func TestVaultCAS_loadOptions(t *testing.T) { want *VaultOptions wantErr bool }{ - { - "ok mandatory with SecretID FromString", - `{"RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, - &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "default", - PKIRoleRSA: "default", - PKIRoleEC: "default", - PKIRoleEd25519: "default", - RoleID: "roleID", - SecretID: auth.SecretID{FromString: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: false, - }, - false, - }, - { - "ok mandatory with SecretID FromFile", - `{"RoleID": "roleID", "SecretID": {"FromFile": "secretID"}}`, - &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "default", - PKIRoleRSA: "default", - PKIRoleEC: "default", - PKIRoleEd25519: "default", - RoleID: "roleID", - SecretID: auth.SecretID{FromFile: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: false, - }, - false, - }, - { - "ok mandatory with SecretID FromEnv", - `{"RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, - &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "default", - PKIRoleRSA: "default", - PKIRoleEC: "default", - PKIRoleEd25519: "default", - RoleID: "roleID", - SecretID: auth.SecretID{FromEnv: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: false, - }, - false, - }, { "ok mandatory PKIRole PKIRoleEd25519", - `{"PKIRoleDefault": "role", "PKIRoleEd25519": "ed25519" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + `{"PKIRoleDefault": "role", "PKIRoleEd25519": "ed25519"}`, &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "role", - PKIRoleRSA: "role", - PKIRoleEC: "role", - PKIRoleEd25519: "ed25519", - RoleID: "roleID", - SecretID: auth.SecretID{FromEnv: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: false, + PKIMountPath: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "role", + PKIRoleEC: "role", + PKIRoleEd25519: "ed25519", }, false, }, { "ok mandatory PKIRole PKIRoleEC", - `{"PKIRoleDefault": "role", "PKIRoleEC": "ec" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + `{"PKIRoleDefault": "role", "PKIRoleEC": "ec"}`, &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "role", - PKIRoleRSA: "role", - PKIRoleEC: "ec", - PKIRoleEd25519: "role", - RoleID: "roleID", - SecretID: auth.SecretID{FromEnv: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: false, + PKIMountPath: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "role", + PKIRoleEC: "ec", + PKIRoleEd25519: "role", }, false, }, { "ok mandatory PKIRole PKIRoleRSA", - `{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + `{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa"}`, &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "role", - PKIRoleRSA: "rsa", - PKIRoleEC: "role", - PKIRoleEd25519: "role", - RoleID: "roleID", - SecretID: auth.SecretID{FromEnv: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: false, + PKIMountPath: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "role", + PKIRoleEd25519: "role", }, false, }, { "ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519", - `{"PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519", "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + `{"PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519"}`, &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "default", - PKIRoleRSA: "rsa", - PKIRoleEC: "ec", - PKIRoleEd25519: "ed25519", - RoleID: "roleID", - SecretID: auth.SecretID{FromEnv: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: false, + PKIMountPath: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", }, false, }, { "ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519 with useless PKIRoleDefault", - `{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519", "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + `{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519"}`, &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "role", - PKIRoleRSA: "rsa", - PKIRoleEC: "ec", - PKIRoleEd25519: "ed25519", - RoleID: "roleID", - SecretID: auth.SecretID{FromEnv: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: false, + PKIMountPath: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", }, false, }, - { - "ok mandatory with AppRole", - `{"AppRole": "test", "RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, - &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "default", - PKIRoleRSA: "default", - PKIRoleEC: "default", - PKIRoleEd25519: "default", - RoleID: "roleID", - SecretID: auth.SecretID{FromString: "secretID"}, - AppRole: "test", - IsWrappingToken: false, - }, - false, - }, - { - "ok mandatory with IsWrappingToken", - `{"IsWrappingToken": true, "RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, - &VaultOptions{ - PKI: "pki", - PKIRoleDefault: "default", - PKIRoleRSA: "default", - PKIRoleEC: "default", - PKIRoleEd25519: "default", - RoleID: "roleID", - SecretID: auth.SecretID{FromString: "secretID"}, - AppRole: "auth/approle", - IsWrappingToken: true, - }, - false, - }, - { - "fail with SecretID FromFail", - `{"RoleID": "roleID", "SecretID": {"FromFail": "secretID"}}`, - nil, - true, - }, - { - "fail with SecretID empty FromEnv", - `{"RoleID": "roleID", "SecretID": {"FromEnv": ""}}`, - nil, - true, - }, - { - "fail with SecretID empty FromFile", - `{"RoleID": "roleID", "SecretID": {"FromFile": ""}}`, - nil, - true, - }, - { - "fail with SecretID empty FromString", - `{"RoleID": "roleID", "SecretID": {"FromString": ""}}`, - nil, - true, - }, - { - "fail mandatory with SecretID FromFail", - `{"RoleID": "roleID", "SecretID": {"FromFail": "secretID"}}`, - nil, - true, - }, - { - "fail missing RoleID", - `{"SecretID": {"FromString": "secretID"}}`, - nil, - true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 6989c7f146c534df3ebb9fbac5c92e9735fde882 Mon Sep 17 00:00:00 2001 From: Erik De Lamarter Date: Sun, 15 May 2022 17:42:08 +0200 Subject: [PATCH 220/241] vault auth unit tests --- cas/vaultcas/auth/approle/approle.go | 24 ++- cas/vaultcas/auth/approle/approle_test.go | 163 +++++++++++++++++- cas/vaultcas/auth/kubernetes/kubernetes.go | 6 + .../auth/kubernetes/kubernetes_test.go | 140 ++++++++++++++- cas/vaultcas/vaultcas_test.go | 8 - 5 files changed, 321 insertions(+), 20 deletions(-) diff --git a/cas/vaultcas/auth/approle/approle.go b/cas/vaultcas/auth/approle/approle.go index 38d3c51c..d842bae0 100644 --- a/cas/vaultcas/auth/approle/approle.go +++ b/cas/vaultcas/auth/approle/approle.go @@ -2,6 +2,7 @@ package approle import ( "encoding/json" + "errors" "fmt" "github.com/hashicorp/vault/api/auth/approle" @@ -12,6 +13,8 @@ import ( type AuthOptions struct { RoleID string `json:"roleID,omitempty"` SecretID string `json:"secretID,omitempty"` + SecretIDFile string `json:"secretIDFile,omitempty"` + SecretIDEnv string `json:"secretIDEnv,omitempty"` IsWrappingToken bool `json:"isWrappingToken,omitempty"` } @@ -33,8 +36,25 @@ func NewApproleAuthMethod(mountPath string, options json.RawMessage) (*approle.A loginOptions = append(loginOptions, approle.WithWrappingToken()) } - sid := approle.SecretID{ - FromString: opts.SecretID, + if opts.RoleID == "" { + return nil, errors.New("you must set roleID") + } + + var sid approle.SecretID + if opts.SecretID != "" { + sid = approle.SecretID{ + FromString: opts.SecretID, + } + } else if opts.SecretIDFile != "" { + sid = approle.SecretID{ + FromFile: opts.SecretIDFile, + } + } else if opts.SecretIDEnv != "" { + sid = approle.SecretID{ + FromEnv: opts.SecretIDEnv, + } + } else { + return nil, errors.New("you must set one of secretID, secretIDFile or secretIDEnv") } approleAuth, err = approle.NewAppRoleAuth(opts.RoleID, &sid, loginOptions...) diff --git a/cas/vaultcas/auth/approle/approle_test.go b/cas/vaultcas/auth/approle/approle_test.go index ab7e6a97..ec4d523f 100644 --- a/cas/vaultcas/auth/approle/approle_test.go +++ b/cas/vaultcas/auth/approle/approle_test.go @@ -1,16 +1,171 @@ package approle import ( + "context" "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" "testing" + + vault "github.com/hashicorp/vault/api" ) -func TestKubernetes_NewKubernetesAuthMethod(t *testing.T) { - mountPath := "approle" - raw := `{"roleID": "roleID", "secretID": "secretIDwrapped", "isWrappedToken": true}` +func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { + t.Helper() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.RequestURI == "/v1/auth/approle/login": + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "auth": { + "client_token": "hvs.0000" + } + }`) + case r.RequestURI == "/v1/auth/custom-approle/login": + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "auth": { + "client_token": "hvs.9999" + } + }`) + default: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, `{"error":"not found"}`) + } + })) + t.Cleanup(func() { + srv.Close() + }) + u, err := url.Parse(srv.URL) + if err != nil { + srv.Close() + t.Fatal(err) + } + + config := vault.DefaultConfig() + config.Address = srv.URL - _, err := NewApproleAuthMethod(mountPath, json.RawMessage(raw)) + client, err := vault.NewClient(config) if err != nil { + srv.Close() t.Fatal(err) } + + return u, client +} + +func TestApprole_LoginMountPaths(t *testing.T) { + caURL, _ := testCAHelper(t) + + config := vault.DefaultConfig() + config.Address = caURL.String() + client, _ := vault.NewClient(config) + + tests := []struct { + name string + mountPath string + token string + }{ + { + name: "ok default mount path", + mountPath: "", + token: "hvs.0000", + }, + { + name: "ok explicit mount path", + mountPath: "approle", + token: "hvs.0000", + }, + { + name: "ok custom mount path", + mountPath: "custom-approle", + token: "hvs.9999", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + method, err := NewApproleAuthMethod(tt.mountPath, json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`)) + if err != nil { + t.Errorf("NewApproleAuthMethod() error = %v", err) + return + } + + secret, err := client.Auth().Login(context.Background(), method) + if err != nil { + t.Errorf("Login() error = %v", err) + return + } + + token, _ := secret.TokenID() + if token != tt.token { + t.Errorf("Token error got %v, expected %v", token, tt.token) + return + } + }) + } +} + +func TestApprole_NewApproleAuthMethod(t *testing.T) { + tests := []struct { + name string + mountPath string + raw string + wantErr bool + }{ + { + "ok secret-id string", + "", + `{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000"}`, + false, + }, + { + "ok secret-id string and wrapped", + "", + `{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "isWrappedToken": true}`, + false, + }, + { + "ok secret-id string and wrapped with custom mountPath", + "approle2", + `{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "isWrappedToken": true}`, + false, + }, + { + "ok secret-id file", + "", + `{"RoleID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id"}`, + false, + }, + { + "ok secret-id env", + "", + `{"RoleID": "0000-0000-0000-0000", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`, + false, + }, + { + "fail mandatory role-id", + "", + `{}`, + true, + }, + { + "fail mandatory secret-id any", + "", + `{"RoleID": "0000-0000-0000-0000"}`, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewApproleAuthMethod(tt.mountPath, json.RawMessage(tt.raw)) + if (err != nil) != tt.wantErr { + t.Errorf("Approle.NewApproleAuthMethod() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } } diff --git a/cas/vaultcas/auth/kubernetes/kubernetes.go b/cas/vaultcas/auth/kubernetes/kubernetes.go index 0c4db62f..267bcdca 100644 --- a/cas/vaultcas/auth/kubernetes/kubernetes.go +++ b/cas/vaultcas/auth/kubernetes/kubernetes.go @@ -2,6 +2,7 @@ package kubernetes import ( "encoding/json" + "errors" "fmt" "github.com/hashicorp/vault/api/auth/kubernetes" @@ -31,6 +32,11 @@ func NewKubernetesAuthMethod(mountPath string, options json.RawMessage) (*kubern if opts.TokenPath != "" { loginOptions = append(loginOptions, kubernetes.WithServiceAccountTokenPath(opts.TokenPath)) } + + if opts.Role == "" { + return nil, errors.New("you must set role") + } + kubernetesAuth, err = kubernetes.NewKubernetesAuth( opts.Role, loginOptions..., diff --git a/cas/vaultcas/auth/kubernetes/kubernetes_test.go b/cas/vaultcas/auth/kubernetes/kubernetes_test.go index 604f1898..55be904d 100644 --- a/cas/vaultcas/auth/kubernetes/kubernetes_test.go +++ b/cas/vaultcas/auth/kubernetes/kubernetes_test.go @@ -1,21 +1,149 @@ package kubernetes import ( + "context" "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" "path" "path/filepath" "runtime" "testing" + + vault "github.com/hashicorp/vault/api" ) -func TestKubernetes_NewKubernetesAuthMethod(t *testing.T) { - _, filename, _, _ := runtime.Caller(0) - tokenPath := filepath.Join(path.Dir(filename), "token") - mountPath := "kubernetes" - raw := `{"role": "SomeRoleName", "tokenPath": "` + tokenPath + `"}` +func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { + t.Helper() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.RequestURI == "/v1/auth/kubernetes/login": + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "auth": { + "client_token": "hvs.0000" + } + }`) + case r.RequestURI == "/v1/auth/custom-kubernetes/login": + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "auth": { + "client_token": "hvs.9999" + } + }`) + default: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, `{"error":"not found"}`) + } + })) + t.Cleanup(func() { + srv.Close() + }) + u, err := url.Parse(srv.URL) + if err != nil { + srv.Close() + t.Fatal(err) + } - _, err := NewKubernetesAuthMethod(mountPath, json.RawMessage(raw)) + config := vault.DefaultConfig() + config.Address = srv.URL + + client, err := vault.NewClient(config) if err != nil { + srv.Close() t.Fatal(err) } + + return u, client +} + +func TestApprole_LoginMountPaths(t *testing.T) { + caURL, _ := testCAHelper(t) + _, filename, _, _ := runtime.Caller(0) + tokenPath := filepath.Join(path.Dir(filename), "token") + + config := vault.DefaultConfig() + config.Address = caURL.String() + client, _ := vault.NewClient(config) + + tests := []struct { + name string + mountPath string + token string + }{ + { + name: "ok default mount path", + mountPath: "", + token: "hvs.0000", + }, + { + name: "ok explicit mount path", + mountPath: "kubernetes", + token: "hvs.0000", + }, + { + name: "ok custom mount path", + mountPath: "custom-kubernetes", + token: "hvs.9999", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + method, err := NewKubernetesAuthMethod(tt.mountPath, json.RawMessage(`{"role": "SomeRoleName", "tokenPath": "`+tokenPath+`"}`)) + if err != nil { + t.Errorf("NewApproleAuthMethod() error = %v", err) + return + } + + secret, err := client.Auth().Login(context.Background(), method) + if err != nil { + t.Errorf("Login() error = %v", err) + return + } + + token, _ := secret.TokenID() + if token != tt.token { + t.Errorf("Token error got %v, expected %v", token, tt.token) + return + } + }) + } +} + +func TestApprole_NewApproleAuthMethod(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + tokenPath := filepath.Join(path.Dir(filename), "token") + + tests := []struct { + name string + mountPath string + raw string + wantErr bool + }{ + { + "ok secret-id string", + "", + `{"role": "SomeRoleName", "tokenPath": "` + tokenPath + `"}`, + false, + }, + { + "fail mandatory role", + "", + `{}`, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewKubernetesAuthMethod(tt.mountPath, json.RawMessage(tt.raw)) + if (err != nil) != tt.wantErr { + t.Errorf("Kubernetes.NewKubernetesAuthMethod() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } } diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 3c1f09a3..0ea0c4b1 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -182,8 +182,6 @@ func TestNew_register(t *testing.T) { CertificateAuthority: caURL.String(), CertificateAuthorityFingerprint: testRootFingerprint, Config: json.RawMessage(`{ - "PKIMountPath": "pki", - "PKIRoleDefault": "pki-role", "AuthType": "approle", "AuthOptions": {"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false} }`), @@ -204,8 +202,6 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { PKIRoleRSA: "rsa", PKIRoleEC: "ec", PKIRoleEd25519: "ed25519", - AuthType: "approle", - AuthOptions: json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`), } type fields struct { @@ -336,8 +332,6 @@ func TestVaultCAS_RevokeCertificate(t *testing.T) { PKIRoleRSA: "rsa", PKIRoleEC: "ec", PKIRoleEd25519: "ed25519", - AuthType: "approle", - AuthOptions: json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`), } type fields struct { @@ -406,8 +400,6 @@ func TestVaultCAS_RenewCertificate(t *testing.T) { PKIRoleRSA: "rsa", PKIRoleEC: "ec", PKIRoleEd25519: "ed25519", - AuthType: "approle", - AuthOptions: json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`), } type fields struct { From 9ec154aab02f25fad6ee47a545a8250ed76e3345 Mon Sep 17 00:00:00 2001 From: Erik De Lamarter Date: Tue, 17 May 2022 22:13:11 +0200 Subject: [PATCH 221/241] rewrite and improve secret-id config --- cas/vaultcas/auth/approle/approle.go | 9 +++++---- cas/vaultcas/auth/approle/approle_test.go | 24 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/cas/vaultcas/auth/approle/approle.go b/cas/vaultcas/auth/approle/approle.go index d842bae0..118afb10 100644 --- a/cas/vaultcas/auth/approle/approle.go +++ b/cas/vaultcas/auth/approle/approle.go @@ -41,19 +41,20 @@ func NewApproleAuthMethod(mountPath string, options json.RawMessage) (*approle.A } var sid approle.SecretID - if opts.SecretID != "" { + switch { + case opts.SecretID != "" && opts.SecretIDFile == "" && opts.SecretIDEnv == "": sid = approle.SecretID{ FromString: opts.SecretID, } - } else if opts.SecretIDFile != "" { + case opts.SecretIDFile != "" && opts.SecretID == "" && opts.SecretIDEnv == "": sid = approle.SecretID{ FromFile: opts.SecretIDFile, } - } else if opts.SecretIDEnv != "" { + case opts.SecretIDEnv != "" && opts.SecretIDFile == "" && opts.SecretID == "": sid = approle.SecretID{ FromEnv: opts.SecretIDEnv, } - } else { + default: return nil, errors.New("you must set one of secretID, secretIDFile or secretIDEnv") } diff --git a/cas/vaultcas/auth/approle/approle_test.go b/cas/vaultcas/auth/approle/approle_test.go index ec4d523f..28b7b7f7 100644 --- a/cas/vaultcas/auth/approle/approle_test.go +++ b/cas/vaultcas/auth/approle/approle_test.go @@ -158,6 +158,30 @@ func TestApprole_NewApproleAuthMethod(t *testing.T) { `{"RoleID": "0000-0000-0000-0000"}`, true, }, + { + "fail multiple secret-id types id and env", + "", + `{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`, + true, + }, + { + "fail multiple secret-id types id and file", + "", + `{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id"}`, + true, + }, + { + "fail multiple secret-id types env and file", + "", + `{"RoleID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`, + true, + }, + { + "fail multiple secret-id types all", + "", + `{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`, + true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 07984a968fba1eedfa514d80e088a41e1f59651f Mon Sep 17 00:00:00 2001 From: Erik DeLamarter Date: Sat, 21 May 2022 21:00:50 +0200 Subject: [PATCH 222/241] better error messages Co-authored-by: Mariano Cano --- cas/vaultcas/vaultcas.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 02c814b7..a5658620 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -84,15 +84,15 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { case "approle": method, err = approle.NewApproleAuthMethod(vc.AuthMountPath, vc.AuthOptions) default: - return nil, fmt.Errorf("unknown auth type: %v", vc.AuthType) + return nil, fmt.Errorf("unknown auth type: %s, only 'kubernetes' and 'approle' currently supported", vc.AuthType) } if err != nil { - return nil, fmt.Errorf("unable to configure auth method: %w", err) + return nil, fmt.Errorf("unable to configure %s auth method: %w", vc.AuthType, err) } authInfo, err := client.Auth().Login(ctx, method) if err != nil { - return nil, fmt.Errorf("unable to login to Kubernetes auth method: %w", err) + return nil, fmt.Errorf("unable to login to %s auth method: %w", vc.AuthType, err) } if authInfo == nil { return nil, errors.New("no auth info was returned after login") From e7f4eaf6c42b9334866ae182ca4befccb36f72a8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 23 May 2022 14:04:31 -0700 Subject: [PATCH 223/241] Remove explicit deprecation notice This will avoid linter errors on other projects for now. --- acme/api/handler.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/acme/api/handler.go b/acme/api/handler.go index 96e22d85..2e3931b1 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -83,7 +83,9 @@ type handler struct { // this route adds will add a new middleware that will set the ACME components // on the context. // -// Deprecated: use api.Route(r api.Router) +// Note: this method is deprecated in step-ca, other applications can still use +// this to support ACME, but the recommendation is to use use +// api.Route(api.Router) and acme.NewContext() instead. func (h *handler) Route(r api.Router) { client := acme.NewClient() linker := acme.NewLinker(h.opts.DNS, h.opts.Prefix) @@ -101,7 +103,9 @@ func (h *handler) Route(r api.Router) { // NewHandler returns a new ACME API handler. // -// Deprecated: use api.Route(r api.Router) +// Note: this method is deprecated in step-ca, other applications can still use +// this to support ACME, but the recommendation is to use use +// api.Route(api.Router) and acme.NewContext() instead. func NewHandler(opts HandlerOptions) api.RouterHandler { return &handler{ opts: &opts, From 94f5b92513439ed5262c10a336dad8e104a11ec4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 23 May 2022 15:31:43 -0700 Subject: [PATCH 224/241] Use proper context in authority package --- authority/authority.go | 2 +- authority/authorize.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 5fa4b0fc..933ceb14 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -590,7 +590,7 @@ func (a *Authority) init() error { } // Load Provisioners and Admins - if err := a.ReloadAdminResources(context.Background()); err != nil { + if err := a.ReloadAdminResources(ctx); err != nil { return err } diff --git a/authority/authorize.go b/authority/authorize.go index c0722a1b..56b53658 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -253,7 +253,8 @@ func (a *Authority) authorizeSign(ctx context.Context, token string) ([]provisio // // Deprecated: Use Authorize(context.Context, string) ([]provisioner.SignOption, error). func (a *Authority) AuthorizeSign(token string) ([]provisioner.SignOption, error) { - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) + ctx := NewContext(context.Background(), a) + ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) return a.Authorize(ctx, token) } From a7dd3a986fbc3acd654a29ee02fcabf9abe66bc9 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 25 May 2022 16:51:26 +0200 Subject: [PATCH 225/241] Set nil dial context for js/wasm runtime --- ca/tls.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ca/tls.go b/ca/tls.go index 7954cbdf..57440bad 100644 --- a/ca/tls.go +++ b/ca/tls.go @@ -12,6 +12,7 @@ import ( "net" "net/http" "os" + "runtime" "time" "github.com/pkg/errors" @@ -288,10 +289,20 @@ func getDefaultDialer() *net.Dialer { // transport for HTTP/2. func getDefaultTransport(tlsConfig *tls.Config) *http.Transport { var dialContext func(ctx context.Context, network string, addr string) (net.Conn, error) - if mTLSDialContext == nil { + switch { + case runtime.GOOS == "js" && runtime.GOARCH == "wasm": + // when running in js/wasm and using the default dialer context all requests + // performed by the CA client resulted in a "protocol not supported" error. + // By setting the dial context to nil requests will be handled by the browser + // fetch API instead. Currently this will always set the dial context to nil, + // but we could implement some additional logic similar to what's found in + // https://github.com/golang/go/pull/46923/files to support a different dial + // context if it is available, required and expected to work. + dialContext = nil + case mTLSDialContext == nil: d := getDefaultDialer() dialContext = d.DialContext - } else { + default: dialContext = mTLSDialContext() } return &http.Transport{ From fd546287acf41504075dcb851df013f3ab0dad82 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 25 May 2022 22:46:26 +0200 Subject: [PATCH 226/241] Strip base64-url padding from ACME CSR This commit strips the padding from a base64-url encoded CSR submitted by a client that doesn't use raw base64-url encoding. --- acme/api/order.go | 8 +++++++- acme/api/order_test.go | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/acme/api/order.go b/acme/api/order.go index c37285d2..5ca1e014 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -54,7 +54,13 @@ type FinalizeRequest struct { // Validate validates a finalize request body. func (f *FinalizeRequest) Validate() error { var err error - csrBytes, err := base64.RawURLEncoding.DecodeString(f.CSR) + // RFC 8555 isn't 100% conclusive about using raw base64-url encoding for the + // CSR specifically, instead of "normal" base64-url encoding (incl. padding). + // By trimming the padding from CSRs submitted by ACME clients that use + // base64-url encoding instead of raw base64-url encoding, these are also + // supported. This was reported in https://github.com/smallstep/certificates/issues/939 + // to be the case for a Synology DSM NAS system. + csrBytes, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(f.CSR, "=")) if err != nil { return acme.WrapError(acme.ErrorMalformedType, err, "error base64url decoding csr") } diff --git a/acme/api/order_test.go b/acme/api/order_test.go index 35abab65..088ebf6e 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -210,6 +210,13 @@ func TestFinalizeRequestValidate(t *testing.T) { }, } }, + "ok/padding": func(t *testing.T) test { + return test{ + fr: &FinalizeRequest{ + CSR: base64.RawURLEncoding.EncodeToString(csr.Raw) + "==", // add intentional padding + }, + } + }, } for name, run := range tests { tc := run(t) From ce9a23a0f7d6f4586b498e4a18c666577e712d81 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 25 May 2022 16:55:22 -0700 Subject: [PATCH 227/241] Fix SSH certificate revocation --- CHANGELOG.md | 1 + authority/tls.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd504e2..cab6e7e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Deprecated ### Removed ### Fixed +- Fixed SSH revocation. ### Security ## [0.19.0] - 2022-04-19 diff --git a/authority/tls.go b/authority/tls.go index fd21ae98..4c29ca15 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -560,7 +560,7 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn }); ok { return lca.RevokeSSH(crt, rci) } - return a.db.Revoke(rci) + return a.db.RevokeSSH(rci) } // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. From 9c049eec5a4641215b0d35bdb28ca789b2944b4e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 25 May 2022 17:10:07 -0700 Subject: [PATCH 228/241] Add revoke ssh unit test --- authority/tls_test.go | 45 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/authority/tls_test.go b/authority/tls_test.go index 9330f0a3..23d2f8fa 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -1301,8 +1301,11 @@ func TestAuthority_Revoke(t *testing.T) { a := testAuthority(t) + tlsRevokeCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod) + type test struct { auth *Authority + ctx context.Context opts *RevokeOptions err error code int @@ -1312,6 +1315,7 @@ func TestAuthority_Revoke(t *testing.T) { "fail/token/authorizeRevoke error": func() test { return test{ auth: a, + ctx: tlsRevokeCtx, opts: &RevokeOptions{ OTT: "foo", Serial: "sn", @@ -1336,6 +1340,7 @@ func TestAuthority_Revoke(t *testing.T) { return test{ auth: a, + ctx: tlsRevokeCtx, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, @@ -1375,6 +1380,7 @@ func TestAuthority_Revoke(t *testing.T) { return test{ auth: _a, + ctx: tlsRevokeCtx, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, @@ -1414,6 +1420,7 @@ func TestAuthority_Revoke(t *testing.T) { return test{ auth: _a, + ctx: tlsRevokeCtx, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, @@ -1451,6 +1458,7 @@ func TestAuthority_Revoke(t *testing.T) { assert.FatalError(t, err) return test{ auth: _a, + ctx: tlsRevokeCtx, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, @@ -1467,6 +1475,7 @@ func TestAuthority_Revoke(t *testing.T) { return test{ auth: _a, + ctx: tlsRevokeCtx, opts: &RevokeOptions{ Crt: crt, Serial: "102012593071130646873265215610956555026", @@ -1491,6 +1500,7 @@ func TestAuthority_Revoke(t *testing.T) { return test{ auth: _a, + ctx: tlsRevokeCtx, opts: &RevokeOptions{ Crt: crt, Serial: "102012593071130646873265215610956555026", @@ -1508,6 +1518,7 @@ func TestAuthority_Revoke(t *testing.T) { return test{ auth: _a, + ctx: tlsRevokeCtx, opts: &RevokeOptions{ Crt: crt, Serial: "102012593071130646873265215610956555026", @@ -1517,12 +1528,42 @@ func TestAuthority_Revoke(t *testing.T) { }, } }, + "ok/ssh": func() test { + a := testAuthority(t, WithDatabase(&db.MockAuthDB{ + MRevoke: func(rci *db.RevokedCertificateInfo) error { + return errors.New("Revoke was called") + }, + MRevokeSSH: func(rci *db.RevokedCertificateInfo) error { + return nil + }, + })) + + cl := jwt.Claims{ + Subject: "sn", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: validAudience, + ID: "44", + } + raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return test{ + auth: a, + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod), + opts: &RevokeOptions{ + Serial: "sn", + ReasonCode: reasonCode, + Reason: reason, + OTT: raw, + }, + } + }, } for name, f := range tests { tc := f() t.Run(name, func(t *testing.T) { - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod) - if err := tc.auth.Revoke(ctx, tc.opts); err != nil { + if err := tc.auth.Revoke(tc.ctx, tc.opts); err != nil { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") From 2adf8caac7f986d93d2fc1daaaa1d9dcf14d36a9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 25 May 2022 17:11:45 -0700 Subject: [PATCH 229/241] Fix Dependabot warning on an indirect dependency --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b772018..546ec53d 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( 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 + gopkg.in/yaml.v3 v3.0.0 // indirect ) // replace github.com/smallstep/nosql => ../nosql diff --git a/go.sum b/go.sum index d76648c2..32a27e27 100644 --- a/go.sum +++ b/go.sum @@ -1347,8 +1347,8 @@ 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/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= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/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 6d580a69e83ff630c7f116ca0ba460e5511e0a3a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 25 May 2022 12:52:32 -0700 Subject: [PATCH 230/241] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cab6e7e3..ccadb8f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased - 0.19.1] - DATE ### Added +- Added Kubernetes auth method for Vault RAs. +- Added support for reporting provisioners to linkedca. ### Changed +- Context usage in HTTP APIs. +- Changed authentication for Vault RAs. ### Deprecated +- HTTP API handler types. ### Removed ### Fixed - Fixed SSH revocation. From b4b9893fcd003fb652a52b90fb426c077cd60eb9 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 25 May 2022 23:28:37 +0200 Subject: [PATCH 231/241] Update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccadb8f7..36897778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Added Kubernetes auth method for Vault RAs. - Added support for reporting provisioners to linkedca. +- Added support for certificate policies on authority level. +- Added a Dockerfile with a step-ca build with HSM support. ### Changed - Context usage in HTTP APIs. - Changed authentication for Vault RAs. +- Error message returned to client when authenticating with expired certificate. +- Strip padding from ACME CSRs. ### Deprecated - HTTP API handler types. ### Removed ### Fixed - Fixed SSH revocation. +- CA client dial context for js/wasm target. +- Incomplete `extraNames` support in templates. +- SCEP GET request support. +- Large SCEP request handling. ### Security ## [0.19.0] - 2022-04-19 @@ -33,6 +41,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. When a context has been configured and no configuration file is provided on startup, the configuration for the current context is used. - Added startup info logging and option to skip it (`--quiet`). +- Added support for renaming the CA (Common Name). ### Changed - Made SCEP CA URL paths dynamic. - Support two latest versions of Go (1.17, 1.18). From 5e56a7b4ec08eac9f9d18410302fc0d93402ca04 Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 26 May 2022 10:55:24 -0700 Subject: [PATCH 232/241] Changelog update for 0.20.0 - added line for new WithOptions on authority Init --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36897778..5fb517a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased - 0.19.1] - DATE +## [0.20.0] - 2022-05-26 ### Added - Added Kubernetes auth method for Vault RAs. - Added support for reporting provisioners to linkedca. - Added support for certificate policies on authority level. - Added a Dockerfile with a step-ca build with HSM support. +- A few new WithXX methods for instantiating authorities ### Changed - Context usage in HTTP APIs. - Changed authentication for Vault RAs. @@ -17,14 +18,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Strip padding from ACME CSRs. ### Deprecated - HTTP API handler types. -### Removed ### Fixed - Fixed SSH revocation. - CA client dial context for js/wasm target. - Incomplete `extraNames` support in templates. - SCEP GET request support. - Large SCEP request handling. -### Security ## [0.19.0] - 2022-04-19 ### Added From fed09047f9f6c847a63a06f4ec587fdbfdabac15 Mon Sep 17 00:00:00 2001 From: Gary Belvin Date: Thu, 9 Jun 2022 13:51:14 -0400 Subject: [PATCH 233/241] pinfile --- cmd/step-pkcs11-init/main.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd/step-pkcs11-init/main.go b/cmd/step-pkcs11-init/main.go index 4dc15799..7a23c664 100644 --- a/cmd/step-pkcs11-init/main.go +++ b/cmd/step-pkcs11-init/main.go @@ -47,6 +47,7 @@ type Config struct { RootFile string KeyFile string Pin string + PinFile string NoCerts bool EnableSSH bool Force bool @@ -74,6 +75,8 @@ func (c *Config) Validate() error { return errors.New("flag `--root-gen` requires flag `--root-key-obj`") case c.RootFile == "" && c.GenerateRoot && c.RootPath == "": return errors.New("flag `--root-gen` requires `--root-cert-path`") + case c.Pin != "" && c.PinFile != "": + return errors.New("Only set one of pin and pin-file") default: if c.RootFile != "" { c.GenerateRoot = false @@ -108,6 +111,7 @@ func main() { var c Config flag.StringVar(&c.KMS, "kms", kmsuri, "PKCS #11 URI with the module-path and token to connect to the module.") flag.StringVar(&c.Pin, "pin", "", "PKCS #11 PIN") + flag.StringVar(&c.PinFile, "pin-file", "", "PKCS #11 PIN File") // Option 1: Generate new root flag.BoolVar(&c.GenerateRoot, "root-gen", true, "Enable the generation of a root key.") flag.StringVar(&c.RootSubject, "root-name", "PKCS #11 Smallstep Root", "Subject and Issuer of the root certificate.") @@ -147,7 +151,18 @@ func main() { // Initialize windows terminal ui.Init() - if u.Get("pin-value") == "" && u.Get("pin-source") == "" && c.Pin == "" { + switch { + case u.Get("pin-value") != "": + case u.Get("pin-source") != "": + case c.Pin != "": + case c.PinFile != "": + content, err := os.ReadFile(c.PinFile) + if err != nil { + fatal(err) + } + c.Pin = string(content) + + default: pin, err := ui.PromptPassword("What is the PKCS#11 PIN?") if err != nil { fatal(err) From fe04f93d7fbe881ca5ad7df7e9b5b676c4daa8a9 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Thu, 16 Jun 2022 01:28:59 +0700 Subject: [PATCH 234/241] all: reformat all go files with the next gofmt (Go 1.19) There are some changes that manually edited, for example using '-' as default list and grouping imports. --- acme/api/eab.go | 9 ++- acme/api/middleware.go | 14 ++-- authority/provisioner/aws.go | 21 +++-- ca/bootstrap.go | 81 ++++++++++--------- ca/ca.go | 2 +- cas/cloudcas/cloudcas.go | 4 +- cas/cloudcas/mock_client_test.go | 5 +- cas/cloudcas/mock_operation_server_test.go | 3 +- commands/onboard.go | 3 +- .../internal/mock/key_vault_client.go | 3 +- kms/cloudkms/cloudkms.go | 3 +- kms/pkcs11/opensc_test.go | 15 ++-- kms/pkcs11/softhsm2_test.go | 10 ++- kms/pkcs11/yubihsm2_test.go | 3 +- logging/clf.go | 4 +- 15 files changed, 102 insertions(+), 78 deletions(-) diff --git a/acme/api/eab.go b/acme/api/eab.go index cf4f1993..4c4fff04 100644 --- a/acme/api/eab.go +++ b/acme/api/eab.go @@ -107,10 +107,11 @@ func keysAreEqual(x, y *jose.JSONWebKey) bool { // validateEABJWS verifies the contents of the External Account Binding JWS. // The protected header of the JWS MUST meet the following criteria: -// o The "alg" field MUST indicate a MAC-based algorithm -// o The "kid" field MUST contain the key identifier provided by the CA -// o The "nonce" field MUST NOT be present -// o The "url" field MUST be set to the same value as the outer JWS +// +// - The "alg" field MUST indicate a MAC-based algorithm +// - The "kid" field MUST contain the key identifier provided by the CA +// - The "nonce" field MUST NOT be present +// - The "url" field MUST be set to the same value as the outer JWS func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) { if jws == nil { return "", acme.NewErrorISE("no JWS provided") diff --git a/acme/api/middleware.go b/acme/api/middleware.go index a254a83b..5dcb93e3 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -119,13 +119,13 @@ func parseJWS(next nextHTTP) nextHTTP { // The JWS Unprotected Header [RFC7515] MUST NOT be used // The JWS Payload MUST NOT be detached // The JWS Protected Header MUST include the following fields: -// * “alg” (Algorithm) -// * This field MUST NOT contain “none” or a Message Authentication Code -// (MAC) algorithm (e.g. one in which the algorithm registry description -// mentions MAC/HMAC). -// * “nonce” (defined in Section 6.5) -// * “url” (defined in Section 6.4) -// * Either “jwk” (JSON Web Key) or “kid” (Key ID) as specified below +// - “alg” (Algorithm). +// This field MUST NOT contain “none” or a Message Authentication Code +// (MAC) algorithm (e.g. one in which the algorithm registry description +// mentions MAC/HMAC). +// - “nonce” (defined in Section 6.5) +// - “url” (defined in Section 6.4) +// - Either “jwk” (JSON Web Key) or “kid” (Key ID) as specified below func validateJWS(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index afc61dd7..a5b403a4 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -51,22 +51,27 @@ const awsMetadataTokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds" // signature. // // The first certificate is used in: -// ap-northeast-2, ap-south-1, ap-southeast-1, ap-southeast-2 -// eu-central-1, eu-north-1, eu-west-1, eu-west-2, eu-west-3 -// us-east-1, us-east-2, us-west-1, us-west-2 -// ca-central-1, sa-east-1 +// +// ap-northeast-2, ap-south-1, ap-southeast-1, ap-southeast-2 +// eu-central-1, eu-north-1, eu-west-1, eu-west-2, eu-west-3 +// us-east-1, us-east-2, us-west-1, us-west-2 +// ca-central-1, sa-east-1 // // The second certificate is used in: -// eu-south-1 +// +// eu-south-1 // // The third certificate is used in: -// ap-east-1 +// +// ap-east-1 // // The fourth certificate is used in: -// af-south-1 +// +// af-south-1 // // The fifth certificate is used in: -// me-south-1 +// +// me-south-1 const awsCertificate = `-----BEGIN CERTIFICATE----- MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw diff --git a/ca/bootstrap.go b/ca/bootstrap.go index 0e0f0fe3..430f2e31 100644 --- a/ca/bootstrap.go +++ b/ca/bootstrap.go @@ -48,17 +48,18 @@ func Bootstrap(token string) (*Client, error) { // certificate after 2/3rd of the certificate's lifetime has expired. // // Usage: -// // Default example with certificate rotation. -// client, err := ca.BootstrapClient(ctx.Background(), token) // -// // Example canceling automatic certificate rotation. -// ctx, cancel := context.WithCancel(context.Background()) -// defer cancel() -// client, err := ca.BootstrapClient(ctx, token) -// if err != nil { -// return err -// } -// resp, err := client.Get("https://internal.smallstep.com") +// // Default example with certificate rotation. +// client, err := ca.BootstrapClient(ctx.Background(), token) +// +// // Example canceling automatic certificate rotation. +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// client, err := ca.BootstrapClient(ctx, token) +// if err != nil { +// return err +// } +// resp, err := client.Get("https://internal.smallstep.com") func BootstrapClient(ctx context.Context, token string, options ...TLSOption) (*http.Client, error) { b, err := createBootstrap(token) if err != nil { @@ -96,23 +97,24 @@ func BootstrapClient(ctx context.Context, token string, options ...TLSOption) (* // ca.AddClientCA(*x509.Certificate). // // Usage: -// // Default example with certificate rotation. -// srv, err := ca.BootstrapServer(context.Background(), token, &http.Server{ -// Addr: ":443", -// Handler: handler, -// }) // -// // Example canceling automatic certificate rotation. -// ctx, cancel := context.WithCancel(context.Background()) -// defer cancel() -// srv, err := ca.BootstrapServer(ctx, token, &http.Server{ -// Addr: ":443", -// Handler: handler, -// }) -// if err != nil { -// return err -// } -// srv.ListenAndServeTLS("", "") +// // Default example with certificate rotation. +// srv, err := ca.BootstrapServer(context.Background(), token, &http.Server{ +// Addr: ":443", +// Handler: handler, +// }) +// +// // Example canceling automatic certificate rotation. +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// srv, err := ca.BootstrapServer(ctx, token, &http.Server{ +// Addr: ":443", +// Handler: handler, +// }) +// if err != nil { +// return err +// } +// srv.ListenAndServeTLS("", "") func BootstrapServer(ctx context.Context, token string, base *http.Server, options ...TLSOption) (*http.Server, error) { if base.TLSConfig != nil { return nil, errors.New("server TLSConfig is already set") @@ -152,19 +154,20 @@ func BootstrapServer(ctx context.Context, token string, base *http.Server, optio // ca.AddClientCA(*x509.Certificate). // // Usage: -// inner, err := net.Listen("tcp", ":443") -// if err != nil { -// return nil -// } -// ctx, cancel := context.WithCancel(context.Background()) -// defer cancel() -// lis, err := ca.BootstrapListener(ctx, token, inner) -// if err != nil { -// return err -// } -// srv := grpc.NewServer() -// ... // register services -// srv.Serve(lis) +// +// inner, err := net.Listen("tcp", ":443") +// if err != nil { +// return nil +// } +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// lis, err := ca.BootstrapListener(ctx, token, inner) +// if err != nil { +// return err +// } +// srv := grpc.NewServer() +// ... // register services +// srv.Serve(lis) func BootstrapListener(ctx context.Context, token string, inner net.Listener, options ...TLSOption) (net.Listener, error) { b, err := createBootstrap(token) if err != nil { diff --git a/ca/ca.go b/ca/ca.go index 9252fff7..7c00bb6b 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -523,7 +523,7 @@ func (ca *CA) shouldServeSCEPEndpoints() bool { return ca.auth.GetSCEPService() != nil } -//nolint // ignore linters to allow keeping this function around for debugging +// nolint // ignore linters to allow keeping this function around for debugging func dumpRoutes(mux chi.Routes) { // helpful routine for logging all routes // walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index e3e956a9..34ff8506 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -32,7 +32,9 @@ func init() { var now = time.Now // The actual regular expression that matches a certificate authority is: -// ^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/locations/[a-z0-9-]+/caPools/[a-zA-Z0-9-_]+/certificateAuthorities/[a-zA-Z0-9-_]+$ +// +// ^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/locations/[a-z0-9-]+/caPools/[a-zA-Z0-9-_]+/certificateAuthorities/[a-zA-Z0-9-_]+$ +// // But we will allow a more flexible one to fail if this changes. var caRegexp = regexp.MustCompile("^projects/[^/]+/locations/[^/]+/caPools/[^/]+/certificateAuthorities/[^/]+$") diff --git a/cas/cloudcas/mock_client_test.go b/cas/cloudcas/mock_client_test.go index de5c2acb..90d1a2f9 100644 --- a/cas/cloudcas/mock_client_test.go +++ b/cas/cloudcas/mock_client_test.go @@ -5,12 +5,13 @@ package cloudcas import ( - privateca "cloud.google.com/go/security/privateca/apiv1" context "context" + reflect "reflect" + + privateca "cloud.google.com/go/security/privateca/apiv1" gomock "github.com/golang/mock/gomock" gax "github.com/googleapis/gax-go/v2" privateca0 "google.golang.org/genproto/googleapis/cloud/security/privateca/v1" - reflect "reflect" ) // MockCertificateAuthorityClient is a mock of CertificateAuthorityClient interface diff --git a/cas/cloudcas/mock_operation_server_test.go b/cas/cloudcas/mock_operation_server_test.go index ee2743d4..43dfa713 100644 --- a/cas/cloudcas/mock_operation_server_test.go +++ b/cas/cloudcas/mock_operation_server_test.go @@ -6,10 +6,11 @@ package cloudcas import ( context "context" + reflect "reflect" + gomock "github.com/golang/mock/gomock" longrunning "google.golang.org/genproto/googleapis/longrunning" emptypb "google.golang.org/protobuf/types/known/emptypb" - reflect "reflect" ) // MockOperationsServer is a mock of OperationsServer interface diff --git a/commands/onboard.go b/commands/onboard.go index ebd468f5..afecba9d 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -23,7 +23,8 @@ import ( // defaultOnboardingURL is the production onboarding url, to use a development // url use: -// export STEP_CA_ONBOARDING_URL=http://localhost:3002/onboarding/ +// +// export STEP_CA_ONBOARDING_URL=http://localhost:3002/onboarding/ const defaultOnboardingURL = "https://api.smallstep.com/onboarding/" type onboardingConfiguration struct { diff --git a/kms/azurekms/internal/mock/key_vault_client.go b/kms/azurekms/internal/mock/key_vault_client.go index 42bd55fd..37858854 100644 --- a/kms/azurekms/internal/mock/key_vault_client.go +++ b/kms/azurekms/internal/mock/key_vault_client.go @@ -6,9 +6,10 @@ package mock import ( context "context" + reflect "reflect" + keyvault "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" gomock "github.com/golang/mock/gomock" - reflect "reflect" ) // KeyVaultClient is a mock of KeyVaultClient interface diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index 65d06048..2f74f1ad 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -279,7 +279,8 @@ func (k *CloudKMS) createKeyRingIfNeeded(name string) error { // GetPublicKey gets from Google's Cloud KMS a public key by name. Key names // follow the pattern: -// projects/([^/]+)/locations/([a-zA-Z0-9_-]{1,63})/keyRings/([a-zA-Z0-9_-]{1,63})/cryptoKeys/([a-zA-Z0-9_-]{1,63})/cryptoKeyVersions/([a-zA-Z0-9_-]{1,63}) +// +// projects/([^/]+)/locations/([a-zA-Z0-9_-]{1,63})/keyRings/([a-zA-Z0-9_-]{1,63})/cryptoKeys/([a-zA-Z0-9_-]{1,63})/cryptoKeyVersions/([a-zA-Z0-9_-]{1,63}) func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { if req.Name == "" { return nil, errors.New("createKeyRequest 'name' cannot be empty") diff --git a/kms/pkcs11/opensc_test.go b/kms/pkcs11/opensc_test.go index b365e614..365c075c 100644 --- a/kms/pkcs11/opensc_test.go +++ b/kms/pkcs11/opensc_test.go @@ -14,12 +14,15 @@ var softHSM2Once sync.Once // mustPKCS11 configures a *PKCS11 KMS to be used with OpenSC, using for example // a Nitrokey HSM. To initialize these tests we should run: -// sc-hsm-tool --initialize --so-pin 3537363231383830 --pin 123456 -// Or: -// pkcs11-tool --module /usr/local/lib/opensc-pkcs11.so \ -// --init-token --init-pin \ -// --so-pin=3537363231383830 --new-pin=123456 --pin=123456 \ -// --label="pkcs11-test" +// +// sc-hsm-tool --initialize --so-pin 3537363231383830 --pin 123456 +// +// Or: +// +// pkcs11-tool --module /usr/local/lib/opensc-pkcs11.so \ +// --init-token --init-pin \ +// --so-pin=3537363231383830 --new-pin=123456 --pin=123456 \ +// --label="pkcs11-test" func mustPKCS11(t TBTesting) *PKCS11 { t.Helper() testModule = "OpenSC" diff --git a/kms/pkcs11/softhsm2_test.go b/kms/pkcs11/softhsm2_test.go index ed2ff208..6fc0c248 100644 --- a/kms/pkcs11/softhsm2_test.go +++ b/kms/pkcs11/softhsm2_test.go @@ -14,12 +14,14 @@ var softHSM2Once sync.Once // mustPKCS11 configures a *PKCS11 KMS to be used with SoftHSM2. To initialize // these tests, we should run: -// softhsm2-util --init-token --free \ -// --token pkcs11-test --label pkcs11-test \ -// --so-pin password --pin password +// +// softhsm2-util --init-token --free \ +// --token pkcs11-test --label pkcs11-test \ +// --so-pin password --pin password // // To delete we should run: -// softhsm2-util --delete-token --token pkcs11-test +// +// softhsm2-util --delete-token --token pkcs11-test func mustPKCS11(t TBTesting) *PKCS11 { t.Helper() testModule = "SoftHSM2" diff --git a/kms/pkcs11/yubihsm2_test.go b/kms/pkcs11/yubihsm2_test.go index 281aff54..49eb13d1 100644 --- a/kms/pkcs11/yubihsm2_test.go +++ b/kms/pkcs11/yubihsm2_test.go @@ -14,7 +14,8 @@ var yubiHSM2Once sync.Once // mustPKCS11 configures a *PKCS11 KMS to be used with YubiHSM2. To initialize // these tests, we should run: -// yubihsm-connector -d +// +// yubihsm-connector -d func mustPKCS11(t TBTesting) *PKCS11 { t.Helper() testModule = "YubiHSM2" diff --git a/logging/clf.go b/logging/clf.go index cee6c982..0e4d9ae9 100644 --- a/logging/clf.go +++ b/logging/clf.go @@ -19,7 +19,9 @@ type CommonLogFormat struct{} // Format implements the logrus.Formatter interface. It returns the given // logrus entry as a CLF line with the following format: -//