Add ACME EAB policy

This commit is contained in:
Herman Slatman 2022-04-07 14:11:53 +02:00
parent 679e2945f2
commit 7df52dbb76
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
16 changed files with 622 additions and 56 deletions

View file

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

View file

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

View file

@ -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")

View file

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

View file

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

View file

@ -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 {

View file

@ -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()

View file

@ -11,7 +11,6 @@ import (
"testing"
"github.com/go-chi/chi"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"

View file

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

View file

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

View file

@ -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) {

View file

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

View file

@ -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()

View file

@ -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)

8
go.mod
View file

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

8
go.sum
View file

@ -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=