certificates/acme/api/revoke.go

217 lines
6.5 KiB
Go
Raw Normal View History

2021-07-02 20:51:15 +00:00
package api
import (
"crypto/x509"
"encoding/base64"
"encoding/json"
"net/http"
2021-07-09 22:28:31 +00:00
"strings"
2021-07-02 20:51:15 +00:00
2021-07-02 22:21:17 +00:00
"github.com/smallstep/certificates/acme"
2021-07-02 20:51:15 +00:00
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority"
2021-07-09 22:28:31 +00:00
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/logging"
2021-07-02 23:56:14 +00:00
"go.step.sm/crypto/jose"
2021-07-02 22:21:17 +00:00
"golang.org/x/crypto/ocsp"
2021-07-02 20:51:15 +00:00
)
2021-07-02 22:21:17 +00:00
type revokePayload struct {
Certificate string `json:"certificate"`
ReasonCode *int `json:"reason,omitempty"`
2021-07-02 22:21:17 +00:00
}
// RevokeCert attempts to revoke a certificate.
2021-07-02 20:51:15 +00:00
func (h *Handler) RevokeCert(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
2021-07-02 23:56:14 +00:00
jws, err := jwsFromContext(ctx)
2021-07-02 20:51:15 +00:00
if err != nil {
api.WriteError(w, err)
return
}
2021-07-09 22:28:31 +00:00
prov, err := provisionerFromContext(ctx)
2021-07-02 20:51:15 +00:00
if err != nil {
2021-07-02 22:21:17 +00:00
api.WriteError(w, err)
return
2021-07-02 20:51:15 +00:00
}
p, err := payloadFromContext(ctx)
if err != nil {
2021-07-02 22:21:17 +00:00
api.WriteError(w, err)
return
2021-07-02 20:51:15 +00:00
}
2021-07-02 22:21:17 +00:00
var payload revokePayload
err = json.Unmarshal(p.value, &payload)
if err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error unmarshaling payload"))
2021-07-02 22:21:17 +00:00
return
2021-07-02 20:51:15 +00:00
}
2021-07-02 22:21:17 +00:00
certBytes, err := base64.RawURLEncoding.DecodeString(payload.Certificate)
2021-07-02 20:51:15 +00:00
if err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error decoding base64 certificate"))
2021-07-02 22:21:17 +00:00
return
2021-07-02 20:51:15 +00:00
}
2021-07-02 22:21:17 +00:00
certToBeRevoked, err := x509.ParseCertificate(certBytes)
2021-07-02 20:51:15 +00:00
if err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error parsing certificate"))
return
}
serial := certToBeRevoked.SerialNumber.String()
2021-07-09 22:28:31 +00:00
existingCert, err := h.db.GetCertificateBySerial(ctx, serial)
if err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error retrieving certificate by serial"))
2021-07-02 22:21:17 +00:00
return
2021-07-02 20:51:15 +00:00
}
2021-07-09 22:28:31 +00:00
if shouldCheckAccountFrom(jws) {
account, err := accountFromContext(ctx)
if err != nil {
api.WriteError(w, err)
return
}
if !account.IsValid() {
api.WriteError(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' has status '%s'", account.ID, account.Status))
return
}
if existingCert.AccountID != account.ID {
api.WriteError(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own certificate '%s'", account.ID, existingCert.ID))
return
}
// TODO: check "an account that holds authorizations for all of the identifiers in the certificate."
} else {
// if account doesn't need to be checked, the JWS should be verified to be signed by the
// private key that belongs to the public key in the certificate to be revoked.
_, err := jws.Verify(certToBeRevoked.PublicKey)
if err != nil {
api.WriteError(w, acme.WrapError(acme.ErrorUnauthorizedType, err,
"verification of jws using certificate public key failed"))
return
}
}
2021-07-02 22:21:17 +00:00
reasonCode := payload.ReasonCode
acmeErr := validateReasonCode(reasonCode)
if acmeErr != nil {
api.WriteError(w, acmeErr)
2021-07-02 22:21:17 +00:00
return
2021-07-02 20:51:15 +00:00
}
2021-07-09 22:28:31 +00:00
// Authorize revocation by ACME provisioner
ctx = provisioner.NewContextWithMethod(ctx, provisioner.RevokeMethod)
err = prov.AuthorizeRevoke(ctx, "")
if err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error authorizing revocation on provisioner"))
return
}
options := revokeOptions(serial, certToBeRevoked, reasonCode)
2021-07-02 20:51:15 +00:00
err = h.ca.Revoke(ctx, options)
if err != nil {
2021-07-09 22:28:31 +00:00
api.WriteError(w, wrapRevokeErr(err))
2021-07-02 22:21:17 +00:00
return
2021-07-02 20:51:15 +00:00
}
logRevoke(w, options)
2021-07-02 23:56:14 +00:00
w.Header().Add("Link", link(h.linker.GetLink(ctx, DirectoryLinkType), "index"))
2021-07-02 20:51:15 +00:00
w.Write(nil)
}
2021-07-02 22:21:17 +00:00
2021-07-09 22:28:31 +00:00
// wrapRevokeErr is a best effort implementation to transform an error during
// revocation into an ACME error, so that clients can understand the error.
func wrapRevokeErr(err error) *acme.Error {
t := err.Error()
if strings.Contains(t, "has already been revoked") {
return acme.NewError(acme.ErrorAlreadyRevokedType, t)
}
return acme.WrapErrorISE(err, "error when revoking certificate")
}
// logRevoke logs successful revocation of certificate
func logRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {
if rl, ok := w.(logging.ResponseLogger); ok {
rl.WithFields(map[string]interface{}{
"serial": ri.Serial,
"reasonCode": ri.ReasonCode,
"reason": ri.Reason,
"passiveOnly": ri.PassiveOnly,
"ACME": ri.ACME,
})
}
}
// validateReasonCode validates the revocation reason
func validateReasonCode(reasonCode *int) *acme.Error {
if reasonCode != nil && ((*reasonCode < ocsp.Unspecified || *reasonCode > ocsp.AACompromise) || *reasonCode == 7) {
return acme.NewError(acme.ErrorBadRevocationReasonType, "reasonCode out of bounds")
}
// NOTE: it's possible to add additional requirements to the reason code:
// The server MAY disallow a subset of reasonCodes from being
// used by the user. If a request contains a disallowed reasonCode,
// then the server MUST reject it with the error type
// "urn:ietf:params:acme:error:badRevocationReason"
// No additional checks have been implemented so far.
return nil
}
// revokeOptions determines the the RevokeOptions for the Authority to use in revocation
func revokeOptions(serial string, certToBeRevoked *x509.Certificate, reasonCode *int) *authority.RevokeOptions {
opts := &authority.RevokeOptions{
Serial: serial,
ACME: true,
Crt: certToBeRevoked,
}
if reasonCode != nil { // NOTE: when implementing CRL and/or OCSP, and reason code is missing, CRL entry extension should be omitted
opts.Reason = reason(*reasonCode)
opts.ReasonCode = *reasonCode
}
return opts
}
// reason transforms an integer reason code to a
// textual description of the revocation reason.
2021-07-02 22:21:17 +00:00
func reason(reasonCode int) string {
switch reasonCode {
case ocsp.Unspecified:
return "unspecified reason"
case ocsp.KeyCompromise:
return "key compromised"
case ocsp.CACompromise:
return "ca compromised"
case ocsp.AffiliationChanged:
return "affiliation changed"
case ocsp.Superseded:
return "superseded"
case ocsp.CessationOfOperation:
return "cessation of operation"
case ocsp.CertificateHold:
return "certificate hold"
case ocsp.RemoveFromCRL:
return "remove from crl"
case ocsp.PrivilegeWithdrawn:
return "privilege withdrawn"
case ocsp.AACompromise:
return "aa compromised"
default:
return "unspecified reason"
}
}
2021-07-02 23:56:14 +00:00
2021-07-09 22:28:31 +00:00
// shouldCheckAccountFrom indicates whether an account should be
2021-07-02 23:56:14 +00:00
// retrieved from the context, so that it can be used for
2021-07-09 22:28:31 +00:00
// additional checks. This should only be done when no JWK
// can be extracted from the request, as that would indicate
// that the revocation request was signed with a certificate
// key pair (and not an account key pair). Looking up such
// a JWK would result in no Account being found.
func shouldCheckAccountFrom(jws *jose.JSONWebSignature) bool {
2021-07-02 23:56:14 +00:00
return !canExtractJWKFrom(jws)
}