Add cursor and limit to ACME EAB DB interface
This commit is contained in:
parent
c3f2fd8ef0
commit
fd9845e9c7
11 changed files with 1313 additions and 1295 deletions
|
@ -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
155
acme/api/eab.go
Normal 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
1068
acme/api/eab_test.go
Normal file
File diff suppressed because it is too large
Load diff
12
acme/db.go
12
acme/db.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -36,6 +36,7 @@ func (r *CreateExternalAccountKeyRequest) Validate() error {
|
|||
// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses
|
||||
type GetExternalAccountKeysResponse struct {
|
||||
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)
|
||||
|
@ -190,6 +191,9 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
|
|||
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
|
||||
}
|
||||
|
@ -231,5 +241,6 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
api.JSON(w, &GetExternalAccountKeysResponse{
|
||||
EAKs: eaks,
|
||||
NextCursor: nextCursor,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue