Add cursor and limit to ACME EAB DB interface

This commit is contained in:
Herman Slatman 2022-01-24 14:03:56 +01:00
parent c3f2fd8ef0
commit fd9845e9c7
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
11 changed files with 1313 additions and 1295 deletions

View file

@ -1,7 +1,6 @@
package api
import (
"context"
"encoding/json"
"net/http"
@ -9,16 +8,8 @@ import (
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/logging"
"go.step.sm/crypto/jose"
)
// ExternalAccountBinding represents the ACME externalAccountBinding JWS
type ExternalAccountBinding struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Sig string `json:"signature"`
}
// NewAccountRequest represents the payload for a new account request.
type NewAccountRequest struct {
Contact []string `json:"contact"`
@ -241,142 +232,3 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {
api.JSON(w, orders)
logOrdersByAccount(w, orders)
}
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account.
func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) {
acmeProv, err := acmeProvisionerFromContext(ctx)
if err != nil {
return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context")
}
if !acmeProv.RequireEAB {
return nil, nil
}
if nar.ExternalAccountBinding == nil {
return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided")
}
eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)
if err != nil {
return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes")
}
eabJWS, err := jose.ParseJWS(string(eabJSONBytes))
if err != nil {
return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws")
}
// TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration?
keyID, acmeErr := validateEABJWS(ctx, eabJWS)
if acmeErr != nil {
return nil, acmeErr
}
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
if err != nil {
if _, ok := err.(*acme.Error); ok {
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
}
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)
}
payload, err := eabJWS.Verify(externalAccountKey.KeyBytes)
if err != nil {
return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
}
jwk, err := jwkFromContext(ctx)
if err != nil {
return nil, err
}
var payloadJWK *jose.JSONWebKey
if err = json.Unmarshal(payload, &payloadJWK); err != nil {
return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk")
}
if !keysAreEqual(jwk, payloadJWK) {
return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match")
}
return externalAccountKey, nil
}
// keysAreEqual performs an equality check on two JWKs by comparing
// the (base64 encoding) of the Key IDs.
func keysAreEqual(x, y *jose.JSONWebKey) bool {
if x == nil || y == nil {
return false
}
digestX, errX := acme.KeyToID(x)
digestY, errY := acme.KeyToID(y)
if errX != nil || errY != nil {
return false
}
return digestX == digestY
}
// validateEABJWS verifies the contents of the External Account Binding JWS.
// The protected header of the JWS MUST meet the following criteria:
// o The "alg" field MUST indicate a MAC-based algorithm
// o The "kid" field MUST contain the key identifier provided by the CA
// o The "nonce" field MUST NOT be present
// o The "url" field MUST be set to the same value as the outer JWS
func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) {
if jws == nil {
return "", acme.NewErrorISE("no JWS provided")
}
if len(jws.Signatures) != 1 {
return "", acme.NewError(acme.ErrorMalformedType, "JWS must have one signature")
}
header := jws.Signatures[0].Protected
algorithm := header.Algorithm
keyID := header.KeyID
nonce := header.Nonce
if !(algorithm == jose.HS256 || algorithm == jose.HS384 || algorithm == jose.HS512) {
return "", acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm '%s'", algorithm)
}
if keyID == "" {
return "", acme.NewError(acme.ErrorMalformedType, "'kid' field is required")
}
if nonce != "" {
return "", acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present")
}
jwsURL, ok := header.ExtraHeaders["url"]
if !ok {
return "", acme.NewError(acme.ErrorMalformedType, "'url' field is required")
}
outerJWS, err := jwsFromContext(ctx)
if err != nil {
return "", acme.WrapErrorISE(err, "could not retrieve outer JWS from context")
}
if len(outerJWS.Signatures) != 1 {
return "", acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature")
}
outerJWSURL, ok := outerJWS.Signatures[0].Protected.ExtraHeaders["url"]
if !ok {
return "", acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS")
}
if jwsURL != outerJWSURL {
return "", acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS")
}
return keyID, nil
}

File diff suppressed because it is too large Load diff

155
acme/api/eab.go Normal file
View file

@ -0,0 +1,155 @@
package api
import (
"context"
"encoding/json"
"github.com/smallstep/certificates/acme"
"go.step.sm/crypto/jose"
)
// ExternalAccountBinding represents the ACME externalAccountBinding JWS
type ExternalAccountBinding struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Sig string `json:"signature"`
}
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account.
func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) {
acmeProv, err := acmeProvisionerFromContext(ctx)
if err != nil {
return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context")
}
if !acmeProv.RequireEAB {
return nil, nil
}
if nar.ExternalAccountBinding == nil {
return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided")
}
eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)
if err != nil {
return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes")
}
eabJWS, err := jose.ParseJWS(string(eabJSONBytes))
if err != nil {
return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws")
}
// TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration?
keyID, acmeErr := validateEABJWS(ctx, eabJWS)
if acmeErr != nil {
return nil, acmeErr
}
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
if err != nil {
if _, ok := err.(*acme.Error); ok {
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
}
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)
}
payload, err := eabJWS.Verify(externalAccountKey.KeyBytes)
if err != nil {
return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
}
jwk, err := jwkFromContext(ctx)
if err != nil {
return nil, err
}
var payloadJWK *jose.JSONWebKey
if err = json.Unmarshal(payload, &payloadJWK); err != nil {
return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk")
}
if !keysAreEqual(jwk, payloadJWK) {
return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match")
}
return externalAccountKey, nil
}
// keysAreEqual performs an equality check on two JWKs by comparing
// the (base64 encoding) of the Key IDs.
func keysAreEqual(x, y *jose.JSONWebKey) bool {
if x == nil || y == nil {
return false
}
digestX, errX := acme.KeyToID(x)
digestY, errY := acme.KeyToID(y)
if errX != nil || errY != nil {
return false
}
return digestX == digestY
}
// validateEABJWS verifies the contents of the External Account Binding JWS.
// The protected header of the JWS MUST meet the following criteria:
// o The "alg" field MUST indicate a MAC-based algorithm
// o The "kid" field MUST contain the key identifier provided by the CA
// o The "nonce" field MUST NOT be present
// o The "url" field MUST be set to the same value as the outer JWS
func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) {
if jws == nil {
return "", acme.NewErrorISE("no JWS provided")
}
if len(jws.Signatures) != 1 {
return "", acme.NewError(acme.ErrorMalformedType, "JWS must have one signature")
}
header := jws.Signatures[0].Protected
algorithm := header.Algorithm
keyID := header.KeyID
nonce := header.Nonce
if !(algorithm == jose.HS256 || algorithm == jose.HS384 || algorithm == jose.HS512) {
return "", acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm '%s'", algorithm)
}
if keyID == "" {
return "", acme.NewError(acme.ErrorMalformedType, "'kid' field is required")
}
if nonce != "" {
return "", acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present")
}
jwsURL, ok := header.ExtraHeaders["url"]
if !ok {
return "", acme.NewError(acme.ErrorMalformedType, "'url' field is required")
}
outerJWS, err := jwsFromContext(ctx)
if err != nil {
return "", acme.WrapErrorISE(err, "could not retrieve outer JWS from context")
}
if len(outerJWS.Signatures) != 1 {
return "", acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature")
}
outerJWSURL, ok := outerJWS.Signatures[0].Protected.ExtraHeaders["url"]
if !ok {
return "", acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS")
}
if jwsURL != outerJWSURL {
return "", acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS")
}
return keyID, nil
}

1068
acme/api/eab_test.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,7 @@ type DB interface {
CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error)
GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)
GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error
UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
@ -58,7 +58,7 @@ type MockDB struct {
MockCreateExternalAccountKey func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
MockGetExternalAccountKey func(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
MockGetExternalAccountKeys func(ctx context.Context, provisionerID 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)
MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error
MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
@ -149,13 +149,13 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID
}
// GetExternalAccountKeys mock
func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error) {
func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) {
if m.MockGetExternalAccountKeys != nil {
return m.MockGetExternalAccountKeys(ctx, provisionerID)
return m.MockGetExternalAccountKeys(ctx, provisionerID, cursor, limit)
} else if m.MockError != nil {
return nil, m.MockError
return nil, "", m.MockError
}
return m.MockRet1.([]*ExternalAccountKey), m.MockError
return m.MockRet1.([]*ExternalAccountKey), "", m.MockError
}
// GetExternalAccountKeyByReference mock

View file

@ -159,20 +159,22 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID
}
// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner
func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) {
func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
externalAccountKeyMutex.RLock()
defer externalAccountKeyMutex.RUnlock()
// cursor and limit are ignored in open source, at least for now.
var eakIDs []string
r, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))
if err != nil {
if !nosqlDB.IsErrNotFound(err) {
return nil, errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID)
return nil, "", errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID)
}
// it may happen that no record is found; we'll continue with an empty slice
} else {
if err := json.Unmarshal(r, &eakIDs); err != nil {
return nil, errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID)
return nil, "", errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID)
}
}
@ -184,7 +186,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string)
eak, err := db.getDBExternalAccountKey(ctx, eakID)
if err != nil {
if !nosqlDB.IsErrNotFound(err) {
return nil, errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID)
return nil, "", errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID)
}
}
keys = append(keys, &acme.ExternalAccountKey{
@ -198,7 +200,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string)
})
}
return keys, nil
return keys, "", nil
}
// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference

View file

@ -576,7 +576,9 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
d := DB{db: tc.db}
if eaks, err := d.GetExternalAccountKeys(context.Background(), provID); err != nil {
cursor, limit := "", 0
if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil {
assert.Equals(t, "", nextCursor)
switch k := err.(type) {
case *acme.Error:
if assert.NotNil(t, tc.acmeErr) {
@ -593,6 +595,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
}
} else if assert.Nil(t, tc.err) {
assert.Equals(t, len(eaks), len(tc.eaks))
assert.Equals(t, "", nextCursor)
for i, eak := range eaks {
assert.Equals(t, eak.ID, tc.eaks[i].ID)
assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes)

View file

@ -35,7 +35,8 @@ func (r *CreateExternalAccountKeyRequest) Validate() error {
// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses
type GetExternalAccountKeysResponse struct {
EAKs []*linkedca.EABKey `json:"eaks"`
EAKs []*linkedca.EABKey `json:"eaks"`
NextCursor string `json:"nextCursor"`
}
// requireEABEnabled is a middleware that ensures ACME EAB is enabled
@ -43,7 +44,7 @@ type GetExternalAccountKeysResponse struct {
func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
provName := chi.URLParam(r, "prov")
provName := chi.URLParam(r, "provisionerName")
eabEnabled, prov, err := h.provisionerHasEABEnabled(ctx, provName)
if err != nil {
api.WriteError(w, err)
@ -187,9 +188,12 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques
func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
var (
key *acme.ExternalAccountKey
keys []*acme.ExternalAccountKey
err error
key *acme.ExternalAccountKey
keys []*acme.ExternalAccountKey
err error
cursor string
nextCursor string
limit int
)
ctx := r.Context()
@ -199,7 +203,13 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
return
}
reference := chi.URLParam(r, "ref")
if cursor, limit, err = api.ParseCursor(r); err != nil {
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err,
"error parsing cursor and limit from query params"))
return
}
reference := chi.URLParam(r, "reference")
if reference != "" {
if key, err = h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference))
@ -209,7 +219,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
keys = []*acme.ExternalAccountKey{key}
}
} else {
if keys, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId()); err != nil {
if keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId(), cursor, limit); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys"))
return
}
@ -230,6 +240,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
}
api.JSON(w, &GetExternalAccountKeysResponse{
EAKs: eaks,
EAKs: eaks,
NextCursor: nextCursor,
})
}

View file

@ -46,7 +46,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
var tests = map[string]func(t *testing.T) test{
"fail/h.provisionerHasEABEnabled": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
@ -65,7 +65,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
},
"ok/eab-disabled": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
@ -105,7 +105,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
},
"ok/eab-enabled": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
@ -498,7 +498,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
var tests = map[string]func(t *testing.T) test{
"fail/ReadJSON": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
body := []byte("{!?}")
return test{
@ -516,7 +516,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
},
"fail/validate": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
req := CreateExternalAccountKeyRequest{
Reference: strings.Repeat("A", 257),
@ -538,7 +538,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
},
"fail/no-provisioner-in-context": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
req := CreateExternalAccountKeyRequest{
Reference: "aRef",
@ -560,7 +560,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
},
"fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
@ -591,7 +591,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
},
"fail/reference-conflict-409": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
@ -629,7 +629,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
},
"fail/acmeDB.CreateExternalAccountKey-no-reference": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
@ -659,7 +659,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
},
"fail/acmeDB.CreateExternalAccountKey-with-reference": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
@ -694,7 +694,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
},
"ok/no-reference": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
@ -731,7 +731,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
},
"ok/with-reference": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
@ -831,7 +831,7 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) {
var tests = map[string]func(t *testing.T) test{
"fail/no-provisioner-in-context": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
return test{
ctx: ctx,
@ -846,7 +846,7 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) {
},
"fail/acmeDB.DeleteExternalAccountKey": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
chiCtx.URLParams.Add("id", "keyID")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
@ -871,7 +871,7 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) {
},
"ok": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
chiCtx.URLParams.Add("id", "keyID")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
@ -948,7 +948,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
var tests = map[string]func(t *testing.T) test{
"fail/no-provisioner-in-context": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
req := httptest.NewRequest("GET", "/foo", nil)
return test{
@ -963,10 +963,28 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
},
}
},
"fail/parse-cursor": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("provisionerName", "provName")
req := httptest.NewRequest("GET", "/foo?limit=A", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
return test{
ctx: ctx,
statusCode: 400,
req: req,
err: &admin.Error{
Status: 400,
Type: admin.ErrorBadRequestType.String(),
Detail: "bad request",
Message: "error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \"A\": invalid syntax",
},
}
},
"fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("ref", "an-external-key-reference")
chiCtx.URLParams.Add("provisionerName", "provName")
chiCtx.URLParams.Add("reference", "an-external-key-reference")
req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
@ -992,14 +1010,16 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
},
"fail/acmeDB.GetExternalAccountKeys": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) {
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
assert.Equals(t, "provID", provisionerID)
return nil, errors.New("force")
assert.Equals(t, "", cursor)
assert.Equals(t, 0, limit)
return nil, "", errors.New("force")
},
}
return test{
@ -1017,8 +1037,8 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
},
"ok/reference-not-found": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("ref", "an-external-key-reference")
chiCtx.URLParams.Add("provisionerName", "provName")
chiCtx.URLParams.Add("reference", "an-external-key-reference")
req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
@ -1042,8 +1062,8 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
},
"ok/reference-found": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("ref", "an-external-key-reference")
chiCtx.URLParams.Add("provisionerName", "provName")
chiCtx.URLParams.Add("reference", "an-external-key-reference")
req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
@ -1082,7 +1102,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
},
"ok/multiple-keys": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("provisionerName", "provName")
req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
@ -1090,8 +1110,10 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
var boundAt time.Time
boundAtSet := time.Now().Add(-12 * time.Hour)
db := &acme.MockDB{
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) {
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "", cursor)
assert.Equals(t, 0, limit)
return []*acme.ExternalAccountKey{
{
ID: "eakID1",
@ -1116,7 +1138,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
BoundAt: boundAtSet,
AccountID: "accountID",
},
}, nil
}, "", nil
},
}
return test{

View file

@ -47,8 +47,8 @@ func (h *Handler) Route(r api.Router) {
r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin))
// ACME External Account Binding Keys
r.MethodFunc("GET", "/acme/eab/{prov}/{ref}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
r.MethodFunc("GET", "/acme/eab/{prov}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
r.MethodFunc("POST", "/acme/eab/{prov}", authnz(requireEABEnabled(h.CreateExternalAccountKey)))
r.MethodFunc("DELETE", "/acme/eab/{prov}/{id}", authnz(requireEABEnabled(h.DeleteExternalAccountKey)))
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.CreateExternalAccountKey)))
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.DeleteExternalAccountKey)))
}

View file

@ -667,49 +667,6 @@ retry:
return nil
}
// GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA.
func (c *AdminClient) GetExternalAccountKeys(provisionerName, reference string, opts ...AdminOption) ([]*linkedca.EABKey, error) {
var retried bool
o := new(adminOptions)
if err := o.apply(opts); err != nil {
return nil, err
}
p := path.Join(adminURLPrefix, "acme/eab", provisionerName)
if reference != "" {
p = path.Join(p, "/", reference)
}
u := c.endpoint.ResolveReference(&url.URL{
Path: p,
RawQuery: o.rawQuery(),
})
tok, err := c.generateAdminToken(u.Path)
if err != nil {
return nil, errors.Wrapf(err, "error generating admin token")
}
req, err := http.NewRequest("GET", u.String(), http.NoBody)
if err != nil {
return nil, errors.Wrapf(err, "create GET %s request failed", u)
}
req.Header.Add("Authorization", tok)
retry:
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "client GET %s failed", u)
}
if resp.StatusCode >= 400 {
if !retried && c.retryOnError(resp) {
retried = true
goto retry
}
return nil, readAdminError(resp.Body)
}
var body = new(adminAPI.GetExternalAccountKeysResponse)
if err := readJSON(resp.Body, body); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
}
return body.EAKs, 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()