smallstep-certificates/acme/api/handler.go

400 lines
13 KiB
Go
Raw Normal View History

2019-05-27 00:41:10 +00:00
package api
import (
"context"
2021-03-11 07:05:46 +00:00
"crypto/x509"
2021-03-05 07:10:46 +00:00
"encoding/json"
2021-03-11 07:05:46 +00:00
"encoding/pem"
2019-05-27 00:41:10 +00:00
"fmt"
"net/http"
2021-03-05 07:10:46 +00:00
"time"
2019-05-27 00:41:10 +00:00
"github.com/go-chi/chi/v5"
2019-05-27 00:41:10 +00:00
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/api/render"
2022-04-27 22:42:26 +00:00
"github.com/smallstep/certificates/authority"
2021-03-05 07:10:46 +00:00
"github.com/smallstep/certificates/authority/provisioner"
2019-05-27 00:41:10 +00:00
)
func link(url, typ string) string {
return fmt.Sprintf("<%s>;rel=%q", url, typ)
2019-05-27 00:41:10 +00:00
}
2021-03-05 07:10:46 +00:00
// Clock that returns time in UTC rounded to seconds.
2021-03-29 19:04:14 +00:00
type Clock struct{}
2021-03-05 07:10:46 +00:00
// Now returns the UTC time rounded to seconds.
func (c *Clock) Now() time.Time {
return time.Now().UTC().Truncate(time.Second)
2021-03-05 07:10:46 +00:00
}
2021-03-29 19:04:14 +00:00
var clock Clock
2021-03-05 07:10:46 +00:00
2019-05-27 00:41:10 +00:00
type payloadInfo struct {
value []byte
isPostAsGet bool
isEmptyJSON bool
}
2021-03-05 07:10:46 +00:00
// HandlerOptions required to create a new ACME API request handler.
type HandlerOptions struct {
2022-05-03 01:47:47 +00:00
// DB storage backend that implements the acme.DB interface.
2022-04-27 22:42:26 +00:00
//
// Deprecated: use acme.NewContex(context.Context, acme.DB)
2021-03-05 07:10:46 +00:00
DB acme.DB
2022-04-27 22:42:26 +00:00
// CA is the certificate authority interface.
//
// Deprecated: use authority.NewContext(context.Context, *authority.Authority)
CA acme.CertificateAuthority
2022-05-03 01:47:47 +00:00
// Backdate is the duration that the CA will subtract from the current time
2022-04-27 22:42:26 +00:00
// to set the NotBefore in the certificate.
Backdate provisioner.Duration
2021-03-05 07:10:46 +00:00
// DNS the host used to generate accurate ACME links. By default the authority
// will use the Host from the request, so this value will only be used if
// request.Host is empty.
DNS string
2022-04-27 22:42:26 +00:00
2021-03-05 07:10:46 +00:00
// Prefix is a URL path prefix under which the ACME api is served. This
// prefix is required to generate accurate ACME links.
// E.g. https://ca.smallstep.com/acme/my-acme-provisioner/new-account --
// "acme" is the prefix from which the ACME api is accessed.
Prefix string
2022-04-27 22:42:26 +00:00
// PrerequisitesChecker checks if all prerequisites for serving ACME are
// met by the CA configuration.
PrerequisitesChecker func(ctx context.Context) (bool, error)
2022-04-27 22:42:26 +00:00
}
var mustAuthority = func(ctx context.Context) acme.CertificateAuthority {
return authority.MustFromContext(ctx)
}
2022-04-29 02:15:18 +00:00
// handler is the ACME API request handler.
type handler struct {
2022-04-27 22:42:26 +00:00
opts *HandlerOptions
}
// Route traffic and implement the Router interface. For backward compatibility
// this route adds will add a new middleware that will set the ACME components
// on the context.
//
// Note: this method is deprecated in step-ca, other applications can still use
// this to support ACME, but the recommendation is to use use
// api.Route(api.Router) and acme.NewContext() instead.
2022-04-29 02:15:18 +00:00
func (h *handler) Route(r api.Router) {
client := acme.NewClient()
linker := acme.NewLinker(h.opts.DNS, h.opts.Prefix)
route(r, func(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if ca, ok := h.opts.CA.(*authority.Authority); ok && ca != nil {
ctx = authority.NewContext(ctx, ca)
}
ctx = acme.NewContext(ctx, h.opts.DB, client, linker, h.opts.PrerequisitesChecker)
next(w, r.WithContext(ctx))
}
})
2019-05-27 00:41:10 +00:00
}
2021-03-05 07:10:46 +00:00
// NewHandler returns a new ACME API handler.
//
// Note: this method is deprecated in step-ca, other applications can still use
// this to support ACME, but the recommendation is to use use
// api.Route(api.Router) and acme.NewContext() instead.
2022-04-29 02:15:18 +00:00
func NewHandler(opts HandlerOptions) api.RouterHandler {
return &handler{
opts: &opts,
2022-04-27 22:42:26 +00:00
}
}
2022-04-29 02:15:18 +00:00
// Route traffic and implement the Router interface. This method requires that
// all the acme components, authority, db, client, linker, and prerequisite
// checker to be present in the context.
func Route(r api.Router) {
route(r, nil)
}
2022-04-27 22:42:26 +00:00
func route(r api.Router, middleware func(next nextHTTP) nextHTTP) {
2022-04-29 02:15:18 +00:00
commonMiddleware := func(next nextHTTP) nextHTTP {
handler := func(w http.ResponseWriter, r *http.Request) {
2022-04-29 02:15:18 +00:00
// Linker middleware gets the provisioner and current url from the
// request and sets them in the context.
linker := acme.MustLinkerFromContext(r.Context())
linker.Middleware(http.HandlerFunc(checkPrerequisites(next))).ServeHTTP(w, r)
}
if middleware != nil {
handler = middleware(handler)
}
return handler
2022-04-29 02:15:18 +00:00
}
2021-07-02 23:56:14 +00:00
validatingMiddleware := func(next nextHTTP) nextHTTP {
2022-04-29 02:15:18 +00:00
return commonMiddleware(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next))))))
2021-07-02 23:56:14 +00:00
}
2019-05-27 00:41:10 +00:00
extractPayloadByJWK := func(next nextHTTP) nextHTTP {
2022-04-29 02:15:18 +00:00
return validatingMiddleware(extractJWK(verifyAndExtractJWSPayload(next)))
2019-05-27 00:41:10 +00:00
}
extractPayloadByKid := func(next nextHTTP) nextHTTP {
2022-04-29 02:15:18 +00:00
return validatingMiddleware(lookupJWK(verifyAndExtractJWSPayload(next)))
2021-07-02 23:56:14 +00:00
}
extractPayloadByKidOrJWK := func(next nextHTTP) nextHTTP {
2022-04-29 02:15:18 +00:00
return validatingMiddleware(extractOrLookupJWK(verifyAndExtractJWSPayload(next)))
}
2022-04-29 02:15:18 +00:00
getPath := acme.GetUnescapedPathSuffix
2022-04-27 22:42:26 +00:00
// Standard ACME API
2022-04-29 02:15:18 +00:00
r.MethodFunc("GET", getPath(acme.NewNonceLinkType, "{provisionerID}"),
commonMiddleware(addNonce(addDirLink(GetNonce))))
r.MethodFunc("HEAD", getPath(acme.NewNonceLinkType, "{provisionerID}"),
commonMiddleware(addNonce(addDirLink(GetNonce))))
r.MethodFunc("GET", getPath(acme.DirectoryLinkType, "{provisionerID}"),
commonMiddleware(GetDirectory))
r.MethodFunc("HEAD", getPath(acme.DirectoryLinkType, "{provisionerID}"),
commonMiddleware(GetDirectory))
r.MethodFunc("POST", getPath(acme.NewAccountLinkType, "{provisionerID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByJWK(NewAccount))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.AccountLinkType, "{provisionerID}", "{accID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(GetOrUpdateAccount))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.KeyChangeLinkType, "{provisionerID}", "{accID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(NotImplemented))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.NewOrderLinkType, "{provisionerID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(NewOrder))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.OrderLinkType, "{provisionerID}", "{ordID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(isPostAsGet(GetOrder)))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.OrdersByAccountLinkType, "{provisionerID}", "{accID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(isPostAsGet(GetOrdersByAccountID)))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.FinalizeLinkType, "{provisionerID}", "{ordID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(FinalizeOrder))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.AuthzLinkType, "{provisionerID}", "{authzID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(isPostAsGet(GetAuthorization)))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.ChallengeLinkType, "{provisionerID}", "{authzID}", "{chID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(GetChallenge))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.CertificateLinkType, "{provisionerID}", "{certID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKid(isPostAsGet(GetCertificate)))
2022-04-29 02:15:18 +00:00
r.MethodFunc("POST", getPath(acme.RevokeCertLinkType, "{provisionerID}"),
2022-04-27 22:42:26 +00:00
extractPayloadByKidOrJWK(RevokeCert))
2019-05-27 00:41:10 +00:00
}
// GetNonce just sets the right header since a Nonce is added to each response
// by middleware by default.
2022-04-27 22:42:26 +00:00
func GetNonce(w http.ResponseWriter, r *http.Request) {
2019-05-27 00:41:10 +00:00
if r.Method == "HEAD" {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNoContent)
}
}
type Meta struct {
TermsOfService string `json:"termsOfService,omitempty"`
Website string `json:"website,omitempty"`
CaaIdentities []string `json:"caaIdentities,omitempty"`
ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
}
2021-03-05 07:10:46 +00:00
// Directory represents an ACME directory for configuring clients.
type Directory struct {
NewNonce string `json:"newNonce"`
NewAccount string `json:"newAccount"`
NewOrder string `json:"newOrder"`
RevokeCert string `json:"revokeCert"`
KeyChange string `json:"keyChange"`
Meta *Meta `json:"meta,omitempty"`
2021-03-05 07:10:46 +00:00
}
// ToLog enables response logging for the Directory type.
func (d *Directory) ToLog() (interface{}, error) {
b, err := json.Marshal(d)
if err != nil {
return nil, acme.WrapErrorISE(err, "error marshaling directory for logging")
}
return string(b), nil
}
2019-05-27 00:41:10 +00:00
// GetDirectory is the ACME resource for returning a directory configuration
// for client configuration.
2022-04-27 22:42:26 +00:00
func GetDirectory(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
ctx := r.Context()
2021-07-22 21:48:41 +00:00
acmeProv, err := acmeProvisionerFromContext(ctx)
if err != nil {
render.Error(w, r, err)
return
}
2022-04-29 02:15:18 +00:00
linker := acme.MustLinkerFromContext(ctx)
2022-11-07 14:35:42 +00:00
render.JSON(w, r, &Directory{
2022-04-29 02:15:18 +00:00
NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType),
NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),
NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType),
RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType),
KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType),
Meta: createMetaObject(acmeProv),
2022-11-07 14:35:42 +00:00
})
}
// createMetaObject creates a Meta object if the ACME provisioner
// has one or more properties that are written in the ACME directory output.
// It returns nil if none of the properties are set.
func createMetaObject(p *provisioner.ACME) *Meta {
if shouldAddMetaObject(p) {
return &Meta{
TermsOfService: p.TermsOfService,
Website: p.Website,
CaaIdentities: p.CaaIdentities,
ExternalAccountRequired: p.RequireEAB,
}
}
return nil
}
// shouldAddMetaObject returns whether or not the ACME provisioner
// has properties configured that must be added to the ACME directory object.
func shouldAddMetaObject(p *provisioner.ACME) bool {
switch {
case p.TermsOfService != "":
return true
case p.Website != "":
return true
2022-11-07 14:35:42 +00:00
case len(p.CaaIdentities) > 0:
return true
case p.RequireEAB:
return true
default:
return false
}
2019-05-27 00:41:10 +00:00
}
// NotImplemented returns a 501 and is generally a placeholder for functionality which
// MAY be added at some point in the future but is not in any way a guarantee of such.
func NotImplemented(w http.ResponseWriter, r *http.Request) {
render.Error(w, r, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented"))
}
// GetAuthorization ACME api for retrieving an Authz.
2022-04-27 22:42:26 +00:00
func GetAuthorization(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
ctx := r.Context()
2022-04-29 02:15:18 +00:00
db := acme.MustDatabaseFromContext(ctx)
linker := acme.MustLinkerFromContext(ctx)
2022-04-27 22:42:26 +00:00
2021-03-05 07:10:46 +00:00
acc, err := accountFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, r, err)
2019-05-27 00:41:10 +00:00
return
}
2022-04-27 22:42:26 +00:00
az, err := db.GetAuthorization(ctx, chi.URLParam(r, "authzID"))
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving authorization"))
2021-03-05 07:10:46 +00:00
return
}
if acc.ID != az.AccountID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
2021-03-05 07:10:46 +00:00
"account '%s' does not own authorization '%s'", acc.ID, az.ID))
2019-05-27 00:41:10 +00:00
return
}
2022-04-27 22:42:26 +00:00
if err = az.UpdateStatus(ctx, db); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error updating authorization status"))
2021-03-11 07:59:02 +00:00
return
2021-03-05 07:10:46 +00:00
}
2022-04-29 02:15:18 +00:00
linker.LinkAuthorization(ctx, az)
2019-05-27 00:41:10 +00:00
2022-04-29 02:15:18 +00:00
w.Header().Set("Location", linker.GetLink(ctx, acme.AuthzLinkType, az.ID))
render.JSON(w, r, az)
2019-05-27 00:41:10 +00:00
}
// GetChallenge ACME api for retrieving a Challenge.
2022-04-27 22:42:26 +00:00
func GetChallenge(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
ctx := r.Context()
2022-04-29 02:15:18 +00:00
db := acme.MustDatabaseFromContext(ctx)
linker := acme.MustLinkerFromContext(ctx)
2022-04-27 22:42:26 +00:00
2021-03-05 07:10:46 +00:00
acc, err := accountFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, r, err)
2019-05-27 00:41:10 +00:00
return
}
2022-04-02 02:56:05 +00:00
payload, err := payloadFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, r, err)
2019-05-27 00:41:10 +00:00
return
}
// NOTE: We should be checking that the request is either a POST-as-GET, or
2022-09-08 18:06:17 +00:00
// that for all challenges except for device-attest-01, the payload is an
// empty JSON block ({}). However, older ACME clients still send a vestigial
// body (rather than an empty JSON block) and strict enforcement would
// render these clients broken.
2021-03-05 07:10:46 +00:00
2021-03-25 07:23:57 +00:00
azID := chi.URLParam(r, "authzID")
2022-04-27 22:42:26 +00:00
ch, err := db.GetChallenge(ctx, chi.URLParam(r, "chID"), azID)
2021-03-05 07:10:46 +00:00
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving challenge"))
2021-03-05 07:10:46 +00:00
return
}
2021-03-29 19:04:14 +00:00
ch.AuthorizationID = azID
2021-03-05 07:10:46 +00:00
if acc.ID != ch.AccountID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
2021-03-05 07:10:46 +00:00
"account '%s' does not own challenge '%s'", acc.ID, ch.ID))
return
}
jwk, err := jwkFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, r, err)
2019-05-27 00:41:10 +00:00
return
}
2022-04-02 02:56:05 +00:00
if err = ch.Validate(ctx, db, jwk, payload.value); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error validating challenge"))
2021-03-05 07:10:46 +00:00
return
}
2019-05-27 00:41:10 +00:00
2022-04-29 02:15:18 +00:00
linker.LinkChallenge(ctx, ch, azID)
2021-03-05 07:10:46 +00:00
2022-04-29 02:15:18 +00:00
w.Header().Add("Link", link(linker.GetLink(ctx, acme.AuthzLinkType, azID), "up"))
w.Header().Set("Location", linker.GetLink(ctx, acme.ChallengeLinkType, azID, ch.ID))
render.JSON(w, r, ch)
2019-05-27 00:41:10 +00:00
}
// GetCertificate ACME api for retrieving a Certificate.
2022-04-27 22:42:26 +00:00
func GetCertificate(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
ctx := r.Context()
2022-04-29 02:15:18 +00:00
db := acme.MustDatabaseFromContext(ctx)
2022-04-27 22:42:26 +00:00
2021-03-05 07:10:46 +00:00
acc, err := accountFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, r, err)
2019-05-27 00:41:10 +00:00
return
}
2021-03-05 07:10:46 +00:00
2022-04-27 22:42:26 +00:00
certID := chi.URLParam(r, "certID")
cert, err := db.GetCertificate(ctx, certID)
2019-05-27 00:41:10 +00:00
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving certificate"))
2019-05-27 00:41:10 +00:00
return
}
2021-03-05 07:10:46 +00:00
if cert.AccountID != acc.ID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
2021-03-05 07:10:46 +00:00
"account '%s' does not own certificate '%s'", acc.ID, certID))
return
}
2021-03-05 07:10:46 +00:00
2021-03-11 07:05:46 +00:00
var certBytes []byte
for _, c := range append([]*x509.Certificate{cert.Leaf}, cert.Intermediates...) {
certBytes = append(certBytes, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
})...)
}
2021-03-05 07:10:46 +00:00
api.LogCertificate(w, cert.Leaf)
w.Header().Set("Content-Type", "application/pem-certificate-chain")
2019-05-27 00:41:10 +00:00
w.Write(certBytes)
}