certificates/acme/api/handler.go

272 lines
9.7 KiB
Go
Raw Normal View History

2019-05-27 00:41:10 +00:00
package api
import (
2021-03-05 07:10:46 +00:00
"crypto/tls"
"encoding/json"
2019-05-27 00:41:10 +00:00
"fmt"
2021-03-05 07:10:46 +00:00
"net"
2019-05-27 00:41:10 +00:00
"net/http"
2021-03-05 07:10:46 +00:00
"time"
2019-05-27 00:41:10 +00:00
"github.com/go-chi/chi"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api"
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=\"%s\"", url, typ)
}
2021-03-05 07:10:46 +00:00
// Clock that returns time in UTC rounded to seconds.
type Clock int
// Now returns the UTC time rounded to seconds.
func (c *Clock) Now() time.Time {
return time.Now().UTC().Round(time.Second)
}
var clock = new(Clock)
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
// Handler is the ACME API request handler.
type Handler struct {
db acme.DB
backdate provisioner.Duration
ca acme.CertificateAuthority
linker *Linker
2019-05-27 00:41:10 +00:00
}
2021-03-05 07:10:46 +00:00
// HandlerOptions required to create a new ACME API request handler.
type HandlerOptions struct {
Backdate provisioner.Duration
// DB storage backend that impements the acme.DB interface.
DB acme.DB
// 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
// 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
CA acme.CertificateAuthority
2019-05-27 00:41:10 +00:00
}
2021-03-05 07:10:46 +00:00
// NewHandler returns a new ACME API handler.
func NewHandler(ops HandlerOptions) api.RouterHandler {
return &Handler{
ca: ops.CA,
db: ops.DB,
backdate: ops.Backdate,
linker: NewLinker(ops.DNS, ops.Prefix),
}
2019-05-27 00:41:10 +00:00
}
// Route traffic and implement the Router interface.
func (h *Handler) Route(r api.Router) {
2021-03-05 07:10:46 +00:00
getLink := h.linker.GetLinkExplicit
2019-05-27 00:41:10 +00:00
// Standard ACME API
2021-03-05 07:10:46 +00:00
r.MethodFunc("GET", getLink(NewNonceLinkType, "{provisionerID}", false, nil), h.baseURLFromRequest(h.lookupProvisioner(h.addNonce(h.GetNonce))))
r.MethodFunc("HEAD", getLink(NewNonceLinkType, "{provisionerID}", false, nil), h.baseURLFromRequest(h.lookupProvisioner(h.addNonce(h.GetNonce))))
r.MethodFunc("GET", getLink(DirectoryLinkType, "{provisionerID}", false, nil), h.baseURLFromRequest(h.lookupProvisioner(h.addNonce(h.GetDirectory))))
r.MethodFunc("HEAD", getLink(DirectoryLinkType, "{provisionerID}", false, nil), h.baseURLFromRequest(h.lookupProvisioner(h.addNonce(h.GetDirectory))))
2019-05-27 00:41:10 +00:00
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)))))))))
2019-05-27 00:41:10 +00:00
}
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)))))))))
}
2021-03-05 07:10:46 +00:00
r.MethodFunc("POST", getLink(NewAccountLinkType, "{provisionerID}", false, nil), extractPayloadByJWK(h.NewAccount))
r.MethodFunc("POST", getLink(AccountLinkType, "{provisionerID}", false, nil, "{accID}"), extractPayloadByKid(h.GetUpdateAccount))
r.MethodFunc("POST", getLink(KeyChangeLinkType, "{provisionerID}", false, nil, "{accID}"), extractPayloadByKid(h.NotImplemented))
r.MethodFunc("POST", getLink(NewOrderLinkType, "{provisionerID}", false, nil), extractPayloadByKid(h.NewOrder))
r.MethodFunc("POST", getLink(OrderLinkType, "{provisionerID}", false, nil, "{ordID}"), extractPayloadByKid(h.isPostAsGet(h.GetOrder)))
r.MethodFunc("POST", getLink(OrdersByAccountLinkType, "{provisionerID}", false, nil, "{accID}"), extractPayloadByKid(h.isPostAsGet(h.GetOrdersByAccount)))
r.MethodFunc("POST", getLink(FinalizeLinkType, "{provisionerID}", false, nil, "{ordID}"), extractPayloadByKid(h.FinalizeOrder))
r.MethodFunc("POST", getLink(AuthzLinkType, "{provisionerID}", false, nil, "{authzID}"), extractPayloadByKid(h.isPostAsGet(h.GetAuthz)))
r.MethodFunc("POST", getLink(ChallengeLinkType, "{provisionerID}", false, nil, "{authzID}", "{chID}"), extractPayloadByKid(h.GetChallenge))
r.MethodFunc("POST", getLink(CertificateLinkType, "{provisionerID}", false, nil, "{certID}"), extractPayloadByKid(h.isPostAsGet(h.GetCertificate)))
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.
func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNoContent)
}
}
2021-03-05 07:10:46 +00:00
// Directory represents an ACME directory for configuring clients.
type Directory struct {
NewNonce string `json:"newNonce,omitempty"`
NewAccount string `json:"newAccount,omitempty"`
NewOrder string `json:"newOrder,omitempty"`
NewAuthz string `json:"newAuthz,omitempty"`
RevokeCert string `json:"revokeCert,omitempty"`
KeyChange string `json:"keyChange,omitempty"`
}
// 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
}
type directory struct {
prefix, dns string
}
2019-05-27 00:41:10 +00:00
// GetDirectory is the ACME resource for returning a directory configuration
// for client configuration.
func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
ctx := r.Context()
api.JSON(w, &Directory{
NewNonce: h.linker.GetLink(ctx, NewNonceLinkType, true),
NewAccount: h.linker.GetLink(ctx, NewAccountLinkType, true),
NewOrder: h.linker.GetLink(ctx, NewOrderLinkType, true),
RevokeCert: h.linker.GetLink(ctx, RevokeCertLinkType, true),
KeyChange: h.linker.GetLink(ctx, KeyChangeLinkType, true),
})
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 (h *Handler) NotImplemented(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
api.WriteError(w, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented"))
}
2019-05-27 00:41:10 +00:00
// GetAuthz ACME api for retrieving an Authz.
func (h *Handler) GetAuthz(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
ctx := r.Context()
acc, err := accountFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
api.WriteError(w, err)
return
}
2021-03-05 07:10:46 +00:00
az, err := h.db.GetAuthorization(ctx, chi.URLParam(r, "authzID"))
2019-05-27 00:41:10 +00:00
if err != nil {
2021-03-05 07:10:46 +00:00
api.WriteError(w, acme.WrapErrorISE(err, "error retrieving authorization"))
return
}
if acc.ID != az.AccountID {
api.WriteError(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own authorization '%s'", acc.ID, az.ID))
2019-05-27 00:41:10 +00:00
return
}
2021-03-05 07:10:46 +00:00
if err = az.UpdateStatus(ctx, h.db); err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error updating authorization status"))
}
h.linker.LinkAuthorization(ctx, az)
2019-05-27 00:41:10 +00:00
2021-03-05 07:10:46 +00:00
w.Header().Set("Location", h.linker.GetLink(ctx, AuthzLinkType, true, az.ID))
api.JSON(w, az)
2019-05-27 00:41:10 +00:00
}
// GetChallenge ACME api for retrieving a Challenge.
func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
ctx := r.Context()
acc, err := accountFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
api.WriteError(w, err)
return
}
// Just verify that the payload was set, since we're not strictly adhering
// to ACME V2 spec for reasons specified below.
2021-03-05 07:10:46 +00:00
_, err = payloadFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
api.WriteError(w, err)
return
}
2021-03-05 07:10:46 +00:00
// NOTE: We should be checking ^^^ that the request is either a POST-as-GET, or
2019-05-27 00:41:10 +00:00
// that 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. For the time being
// we'll just ignore the body.
2021-03-05 07:10:46 +00:00
ch, err := h.db.GetChallenge(ctx, chi.URLParam(r, "chID"), chi.URLParam(r, "authzID"))
if err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error retrieving challenge"))
return
}
if acc.ID != ch.AccountID {
api.WriteError(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own challenge '%s'", acc.ID, ch.ID))
return
}
client := http.Client{
Timeout: time.Duration(30 * time.Second),
}
dialer := &net.Dialer{
Timeout: 30 * time.Second,
}
jwk, err := jwkFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
api.WriteError(w, err)
return
}
2021-03-05 07:10:46 +00:00
if err = ch.Validate(ctx, h.db, jwk, acme.ValidateOptions{
HTTPGet: client.Get,
LookupTxt: net.LookupTXT,
TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
return tls.DialWithDialer(dialer, network, addr, config)
},
}); err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error validating challenge"))
return
}
2019-05-27 00:41:10 +00:00
2021-03-05 07:10:46 +00:00
h.linker.LinkChallenge(ctx, ch)
w.Header().Add("Link", link(h.linker.GetLink(ctx, AuthzLinkType, true, ch.AuthzID), "up"))
w.Header().Set("Location", h.linker.GetLink(ctx, ChallengeLinkType, true, ch.AuthzID, ch.ID))
2019-05-27 00:41:10 +00:00
api.JSON(w, ch)
}
// GetCertificate ACME api for retrieving a Certificate.
func (h *Handler) GetCertificate(w http.ResponseWriter, r *http.Request) {
2021-03-05 07:10:46 +00:00
ctx := r.Context()
acc, err := accountFromContext(ctx)
2019-05-27 00:41:10 +00:00
if err != nil {
api.WriteError(w, err)
return
}
certID := chi.URLParam(r, "certID")
2021-03-05 07:10:46 +00:00
cert, err := h.db.GetCertificate(ctx, certID)
2019-05-27 00:41:10 +00:00
if err != nil {
2021-03-05 07:10:46 +00:00
api.WriteError(w, 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 {
api.WriteError(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own certificate '%s'", acc.ID, certID))
return
}
2021-03-05 07:10:46 +00:00
certBytes, err := cert.ToACME()
if err != nil {
2021-03-05 07:10:46 +00:00
api.WriteError(w, acme.WrapErrorISE(err, "error converting cert to ACME representation"))
return
}
2021-03-05 07:10:46 +00:00
api.LogCertificate(w, cert.Leaf)
2019-05-27 00:41:10 +00:00
w.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8")
w.Write(certBytes)
}