2021-07-02 20:51:15 +00:00
package api
import (
2021-12-02 15:25:35 +00:00
"bytes"
"context"
2021-07-02 20:51:15 +00:00
"crypto/x509"
"encoding/base64"
"encoding/json"
2021-11-26 16:27:42 +00:00
"fmt"
2021-07-02 20:51:15 +00:00
"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"
2021-07-09 15:51:31 +00:00
"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" `
2021-07-09 15:51:31 +00:00
ReasonCode * int ` json:"reason,omitempty" `
2021-07-02 22:21:17 +00:00
}
2021-07-09 15:51:31 +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
}
2021-11-26 16:27:42 +00:00
payload , err := payloadFromContext ( 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
}
2021-11-26 16:27:42 +00:00
var p revokePayload
err = json . Unmarshal ( payload . value , & p )
2021-07-02 22:21:17 +00:00
if err != nil {
2021-07-09 15:51:31 +00:00
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-11-26 16:27:42 +00:00
certBytes , err := base64 . RawURLEncoding . DecodeString ( p . Certificate )
2021-07-02 20:51:15 +00:00
if err != nil {
2021-11-26 16:27:42 +00:00
// in this case the most likely cause is a client that didn't properly encode the certificate
api . WriteError ( w , acme . WrapError ( acme . ErrorMalformedType , err , "error base64url decoding payload certificate property" ) )
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 {
2021-11-26 16:27:42 +00:00
// in this case a client may have encoded something different than a certificate
api . WriteError ( w , acme . WrapError ( acme . ErrorMalformedType , err , "error parsing certificate" ) )
2021-07-09 15:51:31 +00:00
return
}
serial := certToBeRevoked . SerialNumber . String ( )
2021-12-02 15:25:35 +00:00
dbCert , err := h . db . GetCertificateBySerial ( ctx , serial )
2021-07-09 15:51:31 +00:00
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-12-02 15:25:35 +00:00
if ! bytes . Equal ( dbCert . Leaf . Raw , certToBeRevoked . Raw ) {
// this should never happen
api . WriteError ( w , acme . NewErrorISE ( "certificate raw bytes are not equal" ) )
return
}
2021-07-09 22:28:31 +00:00
if shouldCheckAccountFrom ( jws ) {
account , err := accountFromContext ( ctx )
if err != nil {
api . WriteError ( w , err )
return
}
2021-12-02 15:25:35 +00:00
acmeErr := h . isAccountAuthorized ( ctx , dbCert , certToBeRevoked , account )
if acmeErr != nil {
api . WriteError ( w , acmeErr )
2021-07-09 22:28:31 +00:00
return
}
} 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 {
2021-11-28 19:30:36 +00:00
// TODO(hs): possible to determine an error vs. unauthorized and thus provide an ISE vs. Unauthorized?
2021-12-02 15:25:35 +00:00
api . WriteError ( w , wrapUnauthorizedError ( certToBeRevoked , nil , "verification of jws using certificate public key failed" , err ) )
2021-07-09 22:28:31 +00:00
return
}
}
2021-07-02 22:21:17 +00:00
2021-11-26 16:27:42 +00:00
hasBeenRevokedBefore , err := h . ca . IsRevoked ( serial )
if err != nil {
api . WriteError ( w , acme . WrapErrorISE ( err , "error retrieving revocation status of certificate" ) )
return
}
if hasBeenRevokedBefore {
api . WriteError ( w , acme . NewError ( acme . ErrorAlreadyRevokedType , "certificate was already revoked" ) )
return
}
reasonCode := p . ReasonCode
2021-07-09 15:51:31 +00:00
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
}
2021-07-09 15:51:31 +00:00
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
}
2021-07-09 15:51:31 +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-12-02 15:25:35 +00:00
// isAccountAuthorized checks if an ACME account that was retrieved earlier is authorized
// to revoke the certificate. An Account must always be valid in order to revoke a certificate.
// In case the certificate retrieved from the database belongs to the Account, the Account is
// authorized. If the certificate retrieved from the database doesn't belong to the Account,
// the identifiers in the certificate are extracted and compared against the (valid) Authorizations
// that are stored for the ACME Account. If these sets match, the Account is considered authorized
// to revoke the certificate. If this check fails, the client will receive an unauthorized error.
func ( h * Handler ) isAccountAuthorized ( ctx context . Context , dbCert * acme . Certificate , certToBeRevoked * x509 . Certificate , account * acme . Account ) * acme . Error {
if ! account . IsValid ( ) {
return wrapUnauthorizedError ( certToBeRevoked , nil , fmt . Sprintf ( "account '%s' has status '%s'" , account . ID , account . Status ) , nil )
}
certificateBelongsToAccount := dbCert . AccountID == account . ID
if certificateBelongsToAccount {
return nil // return early; skip relatively expensive database check
}
requiredIdentifiers := extractIdentifiers ( certToBeRevoked )
if len ( requiredIdentifiers ) == 0 {
return wrapUnauthorizedError ( certToBeRevoked , nil , "cannot authorize revocation without providing identifiers to authorize" , nil )
}
authzs , err := h . db . GetAuthorizationsByAccountID ( ctx , account . ID )
if err != nil {
return acme . WrapErrorISE ( err , "error retrieving authorizations for Account %s" , account . ID )
}
authorizedIdentifiers := map [ string ] acme . Identifier { }
for _ , authz := range authzs {
// Only valid Authorizations are included
if authz . Status != acme . StatusValid {
continue
}
authorizedIdentifiers [ identifierKey ( authz . Identifier ) ] = authz . Identifier
}
if len ( authorizedIdentifiers ) == 0 {
unauthorizedIdentifiers := [ ] acme . Identifier { }
for _ , identifier := range requiredIdentifiers {
unauthorizedIdentifiers = append ( unauthorizedIdentifiers , identifier )
}
return wrapUnauthorizedError ( certToBeRevoked , unauthorizedIdentifiers , fmt . Sprintf ( "account '%s' does not have valid authorizations" , account . ID ) , nil )
}
unauthorizedIdentifiers := [ ] acme . Identifier { }
for key := range requiredIdentifiers {
_ , ok := authorizedIdentifiers [ key ]
if ! ok {
unauthorizedIdentifiers = append ( unauthorizedIdentifiers , requiredIdentifiers [ key ] )
}
}
if len ( unauthorizedIdentifiers ) != 0 {
return wrapUnauthorizedError ( certToBeRevoked , unauthorizedIdentifiers , fmt . Sprintf ( "account '%s' does not have authorizations for all identifiers" , account . ID ) , nil )
}
return nil
}
// identifierKey creates a unique key for an ACME identifier using
// the following format: ip|127.0.0.1; dns|*.example.com
func identifierKey ( identifier acme . Identifier ) string {
if identifier . Type == acme . IP {
return "ip|" + identifier . Value
}
if identifier . Type == acme . DNS {
return "dns|" + identifier . Value
}
return "unsupported|" + identifier . Value
}
// extractIdentifiers extracts ACME identifiers from an x509 certificate and
// creates a map from them. The map ensures that double SANs are deduplicated.
// The Subject CommonName is included, because RFC8555 7.4 states that DNS
// identifiers can come from either the CommonName or a DNS SAN or both. When
// authorizing issuance, the DNS identifier must be in the request and will be
// included in the validation (see Order.sans()) as of now. This means that the
// CommonName will in fact have an authorization available.
func extractIdentifiers ( cert * x509 . Certificate ) map [ string ] acme . Identifier {
result := map [ string ] acme . Identifier { }
for _ , name := range cert . DNSNames {
identifier := acme . Identifier {
Type : acme . DNS ,
Value : name ,
}
result [ identifierKey ( identifier ) ] = identifier
}
for _ , ip := range cert . IPAddresses {
identifier := acme . Identifier {
Type : acme . IP ,
Value : ip . String ( ) ,
}
result [ identifierKey ( identifier ) ] = identifier
}
// TODO(hs): should we include the CommonName or not?
if cert . Subject . CommonName != "" {
identifier := acme . Identifier {
// assuming only DNS can be in Common Name (RFC8555, 7.4); RFC8738
// IP Identifier Validation Extension does not state anything about this.
// This logic is in accordance with the logic in order.canonicalize()
Type : acme . DNS ,
Value : cert . Subject . CommonName ,
}
result [ identifierKey ( identifier ) ] = identifier
}
return result
}
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 ( )
2021-11-28 19:30:36 +00:00
if strings . Contains ( t , "is already revoked" ) {
2021-07-09 22:28:31 +00:00
return acme . NewError ( acme . ErrorAlreadyRevokedType , t )
}
return acme . WrapErrorISE ( err , "error when revoking certificate" )
}
2021-11-26 16:27:42 +00:00
// unauthorizedError returns an ACME error indicating the request was
// not authorized to revoke the certificate.
2021-12-02 15:25:35 +00:00
func wrapUnauthorizedError ( cert * x509 . Certificate , unauthorizedIdentifiers [ ] acme . Identifier , msg string , err error ) * acme . Error {
2021-11-26 16:27:42 +00:00
var acmeErr * acme . Error
if err == nil {
acmeErr = acme . NewError ( acme . ErrorUnauthorizedType , msg )
} else {
acmeErr = acme . WrapError ( acme . ErrorUnauthorizedType , err , msg )
}
2021-12-02 15:25:35 +00:00
acmeErr . Status = http . StatusForbidden // RFC8555 7.6 shows example with 403
switch {
case len ( unauthorizedIdentifiers ) > 0 :
identifier := unauthorizedIdentifiers [ 0 ] // picking the first; compound may be an option too?
acmeErr . Detail = fmt . Sprintf ( "No authorization provided for name %s" , identifier . Value )
case cert . Subject . String ( ) != "" :
acmeErr . Detail = fmt . Sprintf ( "No authorization provided for name %s" , cert . Subject . CommonName )
default :
acmeErr . Detail = "No authorization provided"
}
2021-11-26 16:27:42 +00:00
return acmeErr
}
2021-07-09 15:51:31 +00:00
// 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 )
}