forked from TrueCloudLab/certificates
Improve tests for ACME account policy
This commit is contained in:
parent
0bb15e16f9
commit
256fe113f7
10 changed files with 571 additions and 102 deletions
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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-<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 {
|
||||
p := newProv()
|
||||
a, ok := p.(*provisioner.ACME)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -276,6 +278,7 @@ 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
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue