From 0e56932e76367570b68f3b163c1266c236275629 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 3 Jul 2021 01:56:14 +0200 Subject: [PATCH] Add support for revocation using JWK --- acme/api/handler.go | 12 +++++++++--- acme/api/middleware.go | 31 +++++++++++++++++++++++++++++++ acme/api/revoke.go | 24 +++++++++++++++++++----- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/acme/api/handler.go b/acme/api/handler.go index 06bd4bb4..8ec12a93 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -94,11 +94,17 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("GET", getPath(DirectoryLinkType, "{provisionerID}"), h.baseURLFromRequest(h.lookupProvisioner(h.GetDirectory))) r.MethodFunc("HEAD", getPath(DirectoryLinkType, "{provisionerID}"), h.baseURLFromRequest(h.lookupProvisioner(h.GetDirectory))) + validatingMiddleware := func(next nextHTTP) nextHTTP { + return h.baseURLFromRequest(h.lookupProvisioner(h.addNonce(h.addDirLink(h.verifyContentType(h.parseJWS(next)))))) + } extractPayloadByJWK := func(next nextHTTP) nextHTTP { - return h.baseURLFromRequest(h.lookupProvisioner(h.addNonce(h.addDirLink(h.verifyContentType(h.parseJWS(h.validateJWS(h.extractJWK(h.verifyAndExtractJWSPayload(next))))))))) + return validatingMiddleware(h.extractJWK(h.verifyAndExtractJWSPayload(next))) } extractPayloadByKid := func(next nextHTTP) nextHTTP { - return h.baseURLFromRequest(h.lookupProvisioner(h.addNonce(h.addDirLink(h.verifyContentType(h.parseJWS(h.validateJWS(h.lookupJWK(h.verifyAndExtractJWSPayload(next))))))))) + return validatingMiddleware(h.lookupJWK(h.verifyAndExtractJWSPayload(next))) + } + extractPayloadByKidOrJWK := func(next nextHTTP) nextHTTP { + return validatingMiddleware(h.extractOrLookupJWK(h.verifyAndExtractJWSPayload(next))) } r.MethodFunc("POST", getPath(NewAccountLinkType, "{provisionerID}"), extractPayloadByJWK(h.NewAccount)) @@ -111,7 +117,7 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("POST", getPath(AuthzLinkType, "{provisionerID}", "{authzID}"), extractPayloadByKid(h.isPostAsGet(h.GetAuthorization))) r.MethodFunc("POST", getPath(ChallengeLinkType, "{provisionerID}", "{authzID}", "{chID}"), extractPayloadByKid(h.GetChallenge)) r.MethodFunc("POST", getPath(CertificateLinkType, "{provisionerID}", "{certID}"), extractPayloadByKid(h.isPostAsGet(h.GetCertificate))) - r.MethodFunc("POST", getPath(RevokeCertLinkType, "{provisionerID}"), extractPayloadByKid(h.RevokeCert)) // TODO: check kid vs. jws; revoke can do both + r.MethodFunc("POST", getPath(RevokeCertLinkType, "{provisionerID}"), extractPayloadByKidOrJWK(h.RevokeCert)) } // GetNonce just sets the right header since a Nonce is added to each response diff --git a/acme/api/middleware.go b/acme/api/middleware.go index 50f7146f..3cee79c5 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -352,6 +352,37 @@ func (h *Handler) lookupJWK(next nextHTTP) nextHTTP { } } +// extractOrLookupJWK forwards handling to either extractJWK or +// lookupJWK based on the presence of a JWK or a KID, respectively. +func (h *Handler) extractOrLookupJWK(next nextHTTP) nextHTTP { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + jws, err := jwsFromContext(ctx) + if err != nil { + api.WriteError(w, err) + return + } + + // at this point the JWS has already been verified (if correctly configured in middleware), + // and it can be used to check if a jwk exists. + if canExtractJWKFrom(jws) { + h.extractJWK(next)(w, r) + return + } + + // default to looking up the JWK based on KID + h.lookupJWK(next)(w, r) + } +} + +// canExtractJWKFrom checks if the JWS has a JWK that can be extracted +func canExtractJWKFrom(jws *jose.JSONWebSignature) bool { + if len(jws.Signatures) == 0 { + return false + } + return jws.Signatures[0].Protected.JSONWebKey != nil +} + // verifyAndExtractJWSPayload extracts the JWK from the JWS and saves it in the context. // Make sure to parse and validate the JWS before running this middleware. func (h *Handler) verifyAndExtractJWSPayload(next nextHTTP) nextHTTP { diff --git a/acme/api/revoke.go b/acme/api/revoke.go index 3dda7098..bf9c3906 100644 --- a/acme/api/revoke.go +++ b/acme/api/revoke.go @@ -9,6 +9,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" + "go.step.sm/crypto/jose" "golang.org/x/crypto/ocsp" ) @@ -19,16 +20,21 @@ type revokePayload struct { func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { - // TODO: support the non-kid case, i.e. JWK with the public key of the cert - // base the account + certificate JWK instead of the kid (which is now the case) - ctx := r.Context() - _, err := accountFromContext(ctx) + jws, err := jwsFromContext(ctx) if err != nil { api.WriteError(w, err) return } + if shouldCheckAccount(jws) { + _, err := accountFromContext(ctx) + if err != nil { + api.WriteError(w, err) + return + } + } + // TODO: do checks on account, i.e. is it still valid? is it allowed to do revocations? Revocations on the to be revoked cert? _, err = provisionerFromContext(ctx) @@ -65,7 +71,7 @@ func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { } certID := certToBeRevoked.SerialNumber.String() - // TODO: retrieving the certificate to verify the account does not seem to work? Results in certificate not found error. + // TODO: retrieving the certificate to verify the account does not seem to work, so far? Results in certificate not found error. // When Revoke is called, the certificate IS in fact found? The (h *Handler) GetCertificate function is fairly similar, too. // existingCert, err := h.db.GetCertificate(ctx, certID) // if err != nil { @@ -101,6 +107,7 @@ func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) { return } + w.Header().Add("Link", link(h.linker.GetLink(ctx, DirectoryLinkType), "index")) w.Write(nil) } @@ -130,3 +137,10 @@ func reason(reasonCode int) string { return "unspecified reason" } } + +// shouldUseAccount indicates whether an account should be +// retrieved from the context, so that it can be used for +// additional checks +func shouldCheckAccount(jws *jose.JSONWebSignature) bool { + return !canExtractJWKFrom(jws) +}