Improve tests for ACME account policy

This commit is contained in:
Herman Slatman 2022-04-11 15:25:55 +02:00
parent 0bb15e16f9
commit 256fe113f7
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
10 changed files with 571 additions and 102 deletions

View file

@ -7,6 +7,8 @@ import (
"time" "time"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"github.com/smallstep/certificates/authority/policy"
) )
// Account is a subset of the internal account type containing only those // Account is a subset of the internal account type containing only those
@ -60,6 +62,25 @@ type Policy struct {
X509 X509Policy `json:"x509"` 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. // ExternalAccountKey is an ACME External Account Binding key.
type ExternalAccountKey struct { type ExternalAccountKey struct {
ID string `json:"id"` ID string `json:"id"`

View file

@ -2,7 +2,6 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -131,14 +130,11 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
return 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 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 { if err := eak.BindTo(acc); err != nil {
render.Error(w, err) render.Error(w, err)
return return
} }
fmt.Println("AFTER EAK BINDING")
if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil { if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil {
render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key")) render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key"))
return return

View file

@ -13,10 +13,12 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.step.sm/crypto/jose"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/jose"
) )
var ( var (
@ -41,6 +43,19 @@ func newProv() acme.Provisioner {
return p return p
} }
func newProvWithOptions(options *provisioner.Options) acme.Provisioner {
// Initialize provisioners
p := &provisioner.ACME{
Type: "ACME",
Name: "test@acme-<test>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 { func newACMEProv(t *testing.T) *provisioner.ACME {
p := newProv() p := newProv()
a, ok := p.(*provisioner.ACME) a, ok := p.(*provisioner.ACME)

View file

@ -56,12 +56,16 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc
return nil, acme.WrapErrorISE(err, "error retrieving external account key") return nil, acme.WrapErrorISE(err, "error retrieving external account key")
} }
if externalAccountKey.AlreadyBound() { if externalAccountKey == nil {
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) return nil, acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key")
} }
if len(externalAccountKey.KeyBytes) == 0 { 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) payload, err := eabJWS.Verify(externalAccountKey.KeyBytes)

View file

@ -428,6 +428,114 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
err: acme.NewErrorISE("error retrieving external account key"), 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 { "fail/db.GetExternalAccountKey-wrong-provisioner": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err) assert.FatalError(t, err)
@ -522,6 +630,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
Reference: "testeak", Reference: "testeak",
CreatedAt: createdAt, CreatedAt: createdAt,
AccountID: "some-account-id", AccountID: "some-account-id",
KeyBytes: []byte{1, 3, 3, 7},
BoundAt: boundAt, BoundAt: boundAt,
}, nil }, nil
}, },

View file

@ -17,6 +17,7 @@ import (
"github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority/policy"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
) )
@ -102,29 +103,35 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) {
return 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() // TODO(hs): gather all errors, so that we can build one response with subproblems; include the nor.Validate()
// error here too, like in example? // error here too, like in example?
eak, err := h.db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID) 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 { 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 // evaluate the provisioner level policy
orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value} orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value}
err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier) if err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier); err != nil {
if err != nil {
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
return return
} }
// evaluate the authority level policy // evaluate the authority level policy
err = h.ca.AreSANsAllowed(ctx, []string{identifier.Value}) if err = h.ca.AreSANsAllowed(ctx, []string{identifier.Value}); err != nil {
if err != nil {
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
return return
} }
@ -180,6 +187,27 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) {
render.JSONStatus(w, o, http.StatusCreated) 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 { func (h *Handler) newAuthorization(ctx context.Context, az *acme.Authorization) error {
if strings.HasPrefix(az.Identifier.Value, "*.") { if strings.HasPrefix(az.Identifier.Value, "*.") {
az.Wildcard = true az.Wildcard = true

View file

@ -16,9 +16,13 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.step.sm/crypto/pemutil"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/acme" "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) { 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"), 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 { "fail/error-h.newAuthorization": func(t *testing.T) test {
acc := &acme.Account{ID: "accID"} acc := &acme.Account{ID: "accID"}
fr := &NewOrderRequest{ fr := &NewOrderRequest{
@ -1360,6 +1546,109 @@ func TestHandler_NewOrder(t *testing.T) {
testBufferDur := 5 * time.Second testBufferDur := 5 * time.Second
orderExpiry := now.Add(defaultOrderExpiry) 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.ID, "ordID")
assert.Equals(t, o.Status, acme.StatusPending) assert.Equals(t, o.Status, acme.StatusPending)
assert.Equals(t, o.Identifiers, nor.Identifiers) assert.Equals(t, o.Identifiers, nor.Identifiers)

View file

@ -24,14 +24,16 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/pkg/errors" "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/assert"
"github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner" "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 // 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 { type mockCA struct {
MockIsRevoked func(sn string) (bool, error) MockIsRevoked func(sn string) (bool, error)
MockRevoke func(ctx context.Context, opts *authority.RevokeOptions) 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) { 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 { func (m *mockCA) AreSANsAllowed(ctx context.Context, sans []string) error {
if m.MockAreSANsallowed != nil {
return m.MockAreSANsallowed(ctx, sans)
}
return nil return nil
} }

View file

@ -5,7 +5,9 @@ import (
"net/http" "net/http"
"go.step.sm/linkedca" "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/api/render"
"github.com/smallstep/certificates/authority/admin" "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) { 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")) 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
}

View file

@ -4,7 +4,6 @@ import (
"net/http" "net/http"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"google.golang.org/protobuf/types/known/timestamppb"
"go.step.sm/linkedca" "go.step.sm/linkedca"
@ -148,78 +147,3 @@ func (h *Handler) loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc
next(w, r.WithContext(ctx)) 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
}