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 -}