Skip to content

ACME External Account Binding #650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 59 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
f81d49d
Add first working version of External Account Binding
hslatman Jul 17, 2021
d44cd18
Add External Accounting Binding key "BoundAt" marking
hslatman Jul 17, 2021
2eb6963
Merge branch 'master' into hs/acme-eab
hslatman Jul 17, 2021
2110c77
Fix JWK payload key equality check
hslatman Jul 17, 2021
540d5fb
Fix marshaling -> marshalling
hslatman Jul 17, 2021
d669f3c
Fix misspelling
hslatman Jul 17, 2021
b65a588
Make authentication work for /admin/eak
hslatman Jul 22, 2021
c6bfc6e
Fix PR comments
hslatman Jul 22, 2021
c6a4c4e
Change ACME EAB endpoint
hslatman Jul 23, 2021
7dad703
Fix missing ACME EAB API endpoints
hslatman Jul 23, 2021
71b3f65
Add processing of RequireEAB through Linked CA
hslatman Aug 6, 2021
492256f
Add first test cases for EAB and make provisioner unique per EAB
hslatman Aug 9, 2021
f31ca4f
Add tests for validateExternalAccountBinding
hslatman Aug 10, 2021
1dba869
Use LinkedCA.EABKey type in ACME EAB API
hslatman Aug 27, 2021
a98fe03
Merge branch 'master' into hs/acme-eab
hslatman Aug 27, 2021
9d09f5e
Add support for deleting ACME EAB keys
hslatman Aug 27, 2021
a1afbce
Check EAB key exists before deleting it
hslatman Aug 27, 2021
f11c0cd
Add endpoint for listing ACME EAB keys
hslatman Aug 27, 2021
66464ae
Merge branch 'master' into hs/acme-eab
hslatman Sep 16, 2021
02cd3b6
Fix PR comments
hslatman Sep 16, 2021
9c00203
Add lookup by reference and make reference optional
hslatman Sep 17, 2021
746c5c9
Disallow creation of EAB keys with non-unique references
hslatman Sep 17, 2021
c2bc135
Add provisioner to remove endpoint and clear reference index on delete
hslatman Sep 17, 2021
9d4cafc
Merge branch 'master' into hs/acme-eab
hslatman Oct 8, 2021
0afea2e
Improve tests for already bound EAB keys
hslatman Oct 8, 2021
f34d688
Refactor retrieval of provisioner into middleware
hslatman Oct 8, 2021
c26041f
Add ACME EAB nosql tests
hslatman Oct 8, 2021
e0b495e
Merge branch 'master' into hs/acme-eab
hslatman Oct 8, 2021
94f8e58
Update go.step.sm/linkedca to v0.8.0
hslatman Oct 11, 2021
a4660f7
Fix some of the gocritic remarks
hslatman Oct 11, 2021
dd4b4b0
Fix remaining gocritic remarks
hslatman Oct 11, 2021
bcd1240
Merge branch 'master' into hs/acme-eab
hslatman Oct 16, 2021
d354d55
Improve handling duplicate ACME EAB references
hslatman Oct 16, 2021
bc5f0e4
Fix gocritic remark
hslatman Oct 17, 2021
4d726d6
Add pagination to ACME EAB credentials endpoint
hslatman Oct 17, 2021
60a1c34
Merge branch 'master' into hs/acme-eab
hslatman Oct 30, 2021
d0c2397
Merge branch 'master' into hs/acme-eab
hslatman Dec 6, 2021
23898e9
Improve EAB JWS validation and increase test coverage
hslatman Dec 7, 2021
6e11657
Refactor creation of (raw) EAB JWS contents
hslatman Dec 7, 2021
9885d42
Fix linting issues
hslatman Dec 7, 2021
2215a05
Add tests for ACME EAB Admin
hslatman Dec 8, 2021
63371a8
Add additional tests for ACME EAB Admin
hslatman Dec 9, 2021
d799359
Merge branch 'master' into hs/acme-eab
hslatman Dec 9, 2021
bd169f5
Add Admin API Middleware tests
hslatman Dec 9, 2021
43a78f4
Add tests for Admin API
hslatman Dec 9, 2021
5f224b7
Add tests for Provisioner Admin API
hslatman Dec 9, 2021
f9ae875
Use short if-style statements
hslatman Dec 20, 2021
22ff90f
Merge branch 'master' into hs/acme-eab
hslatman Dec 22, 2021
5fe9909
Refactor AdminAuthority interface
hslatman Dec 22, 2021
11a7f01
Simplify lookup cursor logic for ExternalAccountKeys
hslatman Dec 22, 2021
6929e31
Merge branch 'master' into hs/acme-eab
hslatman Jan 4, 2022
30859d3
Remove server-side paging logic for ExternalAccountKeys
hslatman Jan 6, 2022
ef16feb
Refactor ACME EAB queries
hslatman Jan 7, 2022
c0eb420
Remove special case for empty slices
hslatman Jan 20, 2022
8838961
Merge branch 'master' into hs/acme-eab
hslatman Jan 20, 2022
868cc4a
Increase test coverage for additional indexes
hslatman Jan 20, 2022
c3f2fd8
Add RW locks to prevent concurrent updates to the DB
hslatman Jan 20, 2022
fd9845e
Add cursor and limit to ACME EAB DB interface
hslatman Jan 24, 2022
bf21319
Fix PR comments and issue with empty string slices
hslatman Jan 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 80 additions & 19 deletions acme/api/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import (
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/logging"

squarejose "gopkg.in/square/go-jose.v2"
"go.step.sm/crypto/jose"
)

// ExternalAccountBinding represents the ACME externalAccountBinding JWS
Expand Down Expand Up @@ -94,20 +93,14 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
return
}

eak, err := h.validateExternalAccountBinding(ctx, &nar)
if err != nil {
api.WriteError(w, err)
return
}

prov, err := acmeProvisionerFromContext(ctx)
if err != nil {
api.WriteError(w, err)
return
}

httpStatus := http.StatusCreated
acc, err := accountFromContext(r.Context())
acc, err := accountFromContext(ctx)
if err != nil {
acmeErr, ok := err.(*acme.Error)
if !ok || acmeErr.Status != http.StatusBadRequest {
Expand All @@ -122,12 +115,19 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
"account does not exist"))
return
}

jwk, err := jwkFromContext(ctx)
if err != nil {
api.WriteError(w, err)
return
}

eak, err := h.validateExternalAccountBinding(ctx, &nar)
if err != nil {
api.WriteError(w, err)
return
}

acc = &acme.Account{
Key: jwk,
Contact: nar.Contact,
Expand All @@ -137,6 +137,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
api.WriteError(w, acme.WrapErrorISE(err, "error creating account"))
return
}

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 {
Expand Down Expand Up @@ -256,26 +257,27 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc
return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided")
}

// TODO: extract the EAB in a similar manner as JWS, JWK, payload, etc? That would probably move a lot/all of
// the logic of this function into the middleware. Should not be too hard, because the middleware does know
// about the handler and thus about its dependencies.
eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)
if err != nil {
return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes")
}

eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes))
eabJWS, err := jose.ParseJWS(string(eabJSONBytes))
if err != nil {
return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws")
}

// TODO: implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration
// 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
}

keyID := eabJWS.Signatures[0].Protected.KeyID
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.Name, keyID)
if err != nil {
if _, ok := err.(*acme.Error); ok {
return nil, err
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
}
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
}
Expand All @@ -294,22 +296,22 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc
return nil, err
}

var payloadJWK *squarejose.JSONWebKey
var payloadJWK *jose.JSONWebKey
err = json.Unmarshal(payload, &payloadJWK)
if err != nil {
return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk")
}

if !keysAreEqual(jwk, payloadJWK) {
return nil, acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match") // TODO: decide ACME error type to use
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 *squarejose.JSONWebKey) bool {
func keysAreEqual(x, y *jose.JSONWebKey) bool {
if x == nil || y == nil {
return false
}
Expand All @@ -320,3 +322,62 @@ func keysAreEqual(x, y *squarejose.JSONWebKey) bool {
}
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
}
Loading