CRL Reason for certificate revoke

This commit is contained in:
sayali 2020-11-30 20:06:37 -08:00
parent 817abb2ca8
commit 7a1f13dcb5
17 changed files with 151 additions and 35 deletions

View File

@ -104,7 +104,7 @@ The `IssuerPlugin` exposes four functions functions::
def create_certificate(self, csr, issuer_options): def create_certificate(self, csr, issuer_options):
# requests.get('a third party') # requests.get('a third party')
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
# requests.put('a third party') # requests.put('a third party')
def get_ordered_certificate(self, order_id): def get_ordered_certificate(self, order_id):
# requests.get('already existing certificate') # requests.get('already existing certificate')

View File

@ -623,7 +623,8 @@ def clear_pending():
@manager.option( @manager.option(
"-p", "--path", dest="path", help="Absolute file path to a Lemur query csv." "-p", "--path", dest="path", help="Absolute file path to a Lemur query csv."
) )
@manager.option("-r", "--reason", dest="reason", help="Reason to revoke certificate.") @manager.option("-r", "--reason", dest="reason", default="unspecified", help="CRL Reason as per RFC 5280 section 5.3.1")
@manager.option("-m", "--message", dest="message", help="Message explaining reason for revocation")
@manager.option( @manager.option(
"-c", "-c",
"--commit", "--commit",
@ -632,7 +633,7 @@ def clear_pending():
default=False, default=False,
help="Persist changes.", help="Persist changes.",
) )
def revoke(path, reason, commit): def revoke(path, reason, message, commit):
""" """
Revokes given certificate. Revokes given certificate.
""" """
@ -640,9 +641,10 @@ def revoke(path, reason, commit):
print("[!] Running in COMMIT mode.") print("[!] Running in COMMIT mode.")
print("[+] Starting certificate revocation.") print("[+] Starting certificate revocation.")
comments = {"comments": message, "crl_reason": reason}
with open(path, "r") as f: with open(path, "r") as f:
args = [[x, commit, reason] for x in f.readlines()[2:]] args = [[x, commit, comments] for x in f.readlines()[2:]]
with multiprocessing.Pool(processes=3) as pool: with multiprocessing.Pool(processes=3) as pool:
pool.starmap(worker, args) pool.starmap(worker, args)

View File

@ -16,7 +16,7 @@ from lemur.certificates import utils as cert_utils
from lemur.common import missing, utils, validators from lemur.common import missing, utils, validators
from lemur.common.fields import ArrowDateTime, Hex from lemur.common.fields import ArrowDateTime, Hex
from lemur.common.schema import LemurInputSchema, LemurOutputSchema from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.constants import CERTIFICATE_KEY_TYPES from lemur.constants import CERTIFICATE_KEY_TYPES, CRLReason
from lemur.destinations.schemas import DestinationNestedOutputSchema from lemur.destinations.schemas import DestinationNestedOutputSchema
from lemur.dns_providers.schemas import DnsProvidersNestedOutputSchema from lemur.dns_providers.schemas import DnsProvidersNestedOutputSchema
from lemur.domains.schemas import DomainNestedOutputSchema from lemur.domains.schemas import DomainNestedOutputSchema
@ -455,6 +455,7 @@ class CertificateNotificationOutputSchema(LemurOutputSchema):
class CertificateRevokeSchema(LemurInputSchema): class CertificateRevokeSchema(LemurInputSchema):
comments = fields.String() comments = fields.String()
crl_reason = fields.String(validate=validate.OneOf(CRLReason.__members__), missing="unspecified")
certificates_list_request_parser = RequestParser() certificates_list_request_parser = RequestParser()

View File

@ -828,6 +828,14 @@ def remove_from_destination(certificate, destination):
plugin.clean(certificate=certificate, options=destination.options) plugin.clean(certificate=certificate, options=destination.options)
def revoke(certificate, reason):
plugin = plugins.get(certificate.authority.plugin_name)
plugin.revoke_certificate(certificate, reason)
# Perform cleanup after revoke
return cleanup_after_revoke(certificate)
def cleanup_after_revoke(certificate): def cleanup_after_revoke(certificate):
""" """
Perform the needed cleanup for a revoked certificate. This includes - Perform the needed cleanup for a revoked certificate. This includes -

View File

@ -20,7 +20,6 @@ from lemur.auth.permissions import AuthorityPermission, CertificatePermission
from lemur.certificates import service from lemur.certificates import service
from lemur.certificates.models import Certificate from lemur.certificates.models import Certificate
from lemur.extensions import sentry from lemur.extensions import sentry
from lemur.plugins.base import plugins
from lemur.certificates.schemas import ( from lemur.certificates.schemas import (
certificate_input_schema, certificate_input_schema,
certificate_output_schema, certificate_output_schema,
@ -29,6 +28,7 @@ from lemur.certificates.schemas import (
certificate_export_input_schema, certificate_export_input_schema,
certificate_edit_input_schema, certificate_edit_input_schema,
certificates_list_output_schema_factory, certificates_list_output_schema_factory,
certificate_revoke_schema,
) )
from lemur.roles import service as role_service from lemur.roles import service as role_service
@ -1398,7 +1398,7 @@ class CertificateRevoke(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(CertificateRevoke, self).__init__() super(CertificateRevoke, self).__init__()
@validate_schema(None, None) @validate_schema(certificate_revoke_schema, None)
def put(self, certificate_id, data=None): def put(self, certificate_id, data=None):
""" """
.. http:put:: /certificates/1/revoke .. http:put:: /certificates/1/revoke
@ -1459,13 +1459,9 @@ class CertificateRevoke(AuthenticatedResource):
403, 403,
) )
plugin = plugins.get(cert.authority.plugin_name) error_message = service.revoke(cert, data)
plugin.revoke_certificate(cert, data)
log_service.create(g.current_user, "revoke_cert", certificate=cert) log_service.create(g.current_user, "revoke_cert", certificate=cert)
# Perform cleanup after revoke
error_message = service.cleanup_after_revoke(cert)
if error_message: if error_message:
return dict(message=f"Certificate (id:{cert.id}) is revoked - {error_message}"), 400 return dict(message=f"Certificate (id:{cert.id}) is revoked - {error_message}"), 400
return dict(id=cert.id) return dict(id=cert.id)

View File

@ -3,6 +3,8 @@
:copyright: (c) 2018 by Netflix Inc. :copyright: (c) 2018 by Netflix Inc.
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
""" """
from enum import IntEnum
SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}" SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}"
DEFAULT_NAMING_TEMPLATE = "{subject}-{issuer}-{not_before}-{not_after}" DEFAULT_NAMING_TEMPLATE = "{subject}-{issuer}-{not_before}-{not_after}"
NONSTANDARD_NAMING_TEMPLATE = "{issuer}-{not_before}-{not_after}" NONSTANDARD_NAMING_TEMPLATE = "{issuer}-{not_before}-{not_after}"
@ -32,3 +34,16 @@ CERTIFICATE_KEY_TYPES = [
"ECCSECT409R1", "ECCSECT409R1",
"ECCSECT571R2", "ECCSECT571R2",
] ]
class CRLReason(IntEnum):
unspecified = 0,
keyCompromise = 1,
cACompromise = 2,
affiliationChanged = 3,
superseded = 4,
cessationOfOperation = 5,
certificateHold = 6,
removeFromCRL = 8,
privilegeWithdrawn = 9,
aACompromise = 10

View File

@ -23,7 +23,7 @@ class IssuerPlugin(Plugin):
def create_authority(self, options): def create_authority(self, options):
raise NotImplementedError raise NotImplementedError
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
raise NotImplementedError raise NotImplementedError
def get_ordered_certificate(self, certificate): def get_ordered_certificate(self, certificate):

View File

@ -221,7 +221,7 @@ class AcmeHandler(object):
current_app.logger.debug("Got these domains: {0}".format(domains)) current_app.logger.debug("Got these domains: {0}".format(domains))
return domains return domains
def revoke_certificate(self, certificate): def revoke_certificate(self, certificate, crl_reason=0):
if not self.reuse_account(certificate.authority): if not self.reuse_account(certificate.authority):
raise InvalidConfiguration("There is no ACME account saved, unable to revoke the certificate.") raise InvalidConfiguration("There is no ACME account saved, unable to revoke the certificate.")
acme_client, _ = self.setup_acme_client(certificate.authority) acme_client, _ = self.setup_acme_client(certificate.authority)
@ -231,7 +231,7 @@ class AcmeHandler(object):
OpenSSL.crypto.FILETYPE_PEM, certificate.body)) OpenSSL.crypto.FILETYPE_PEM, certificate.body))
try: try:
acme_client.revoke(fullchain_com, 0) # revocation reason = 0 acme_client.revoke(fullchain_com, crl_reason) # revocation reason as int (per RFC 5280 section 5.3.1)
except (errors.ConflictError, errors.ClientError, errors.Error) as e: except (errors.ConflictError, errors.ClientError, errors.Error) as e:
# Certificate already revoked. # Certificate already revoked.
current_app.logger.error("Certificate revocation failed with message: " + e.detail) current_app.logger.error("Certificate revocation failed with message: " + e.detail)

View File

@ -17,6 +17,7 @@ from acme.messages import Error as AcmeError
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from flask import current_app from flask import current_app
from lemur.authorizations import service as authorization_service from lemur.authorizations import service as authorization_service
from lemur.constants import CRLReason
from lemur.dns_providers import service as dns_provider_service from lemur.dns_providers import service as dns_provider_service
from lemur.exceptions import InvalidConfiguration from lemur.exceptions import InvalidConfiguration
from lemur.extensions import metrics, sentry from lemur.extensions import metrics, sentry
@ -267,9 +268,13 @@ class ACMEIssuerPlugin(IssuerPlugin):
# Needed to override issuer function. # Needed to override issuer function.
pass pass
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
self.acme = AcmeDnsHandler() self.acme = AcmeDnsHandler()
return self.acme.revoke_certificate(certificate) crl_reason = CRLReason.unspecified
if "crl_reason" in reason:
crl_reason = CRLReason[reason["crl_reason"]]
return self.acme.revoke_certificate(certificate, crl_reason.value)
class ACMEHttpIssuerPlugin(IssuerPlugin): class ACMEHttpIssuerPlugin(IssuerPlugin):
@ -368,6 +373,11 @@ class ACMEHttpIssuerPlugin(IssuerPlugin):
# Needed to override issuer function. # Needed to override issuer function.
pass pass
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
self.acme = AcmeHandler() self.acme = AcmeHandler()
return self.acme.revoke_certificate(certificate)
crl_reason = CRLReason.unspecified
if "crl_reason" in reason:
crl_reason = CRLReason[reason["crl_reason"]]
return self.acme.revoke_certificate(certificate, crl_reason.value)

View File

@ -59,8 +59,8 @@ class ADCSIssuerPlugin(IssuerPlugin):
) )
return cert, chain, None return cert, chain, None
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
raise NotImplementedError("Not implemented\n", self, certificate, comments) raise NotImplementedError("Not implemented\n", self, certificate, reason)
def get_ordered_certificate(self, order_id): def get_ordered_certificate(self, order_id):
raise NotImplementedError("Not implemented\n", self, order_id) raise NotImplementedError("Not implemented\n", self, order_id)

View File

@ -18,6 +18,7 @@ from flask import current_app
from lemur.common.utils import parse_certificate from lemur.common.utils import parse_certificate
from lemur.common.utils import get_authority_key from lemur.common.utils import get_authority_key
from lemur.constants import CRLReason
from lemur.plugins.bases import IssuerPlugin from lemur.plugins.bases import IssuerPlugin
from lemur.plugins import lemur_cfssl as cfssl from lemur.plugins import lemur_cfssl as cfssl
from lemur.extensions import metrics from lemur.extensions import metrics
@ -102,16 +103,23 @@ class CfsslIssuerPlugin(IssuerPlugin):
role = {"username": "", "password": "", "name": "cfssl"} role = {"username": "", "password": "", "name": "cfssl"}
return current_app.config.get("CFSSL_ROOT"), "", [role] return current_app.config.get("CFSSL_ROOT"), "", [role]
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
"""Revoke a CFSSL certificate.""" """Revoke a CFSSL certificate."""
base_url = current_app.config.get("CFSSL_URL") base_url = current_app.config.get("CFSSL_URL")
create_url = "{0}/api/v1/cfssl/revoke".format(base_url) create_url = "{0}/api/v1/cfssl/revoke".format(base_url)
crl_reason = CRLReason.unspecified
if "crl_reason" in reason:
crl_reason = CRLReason[reason["crl_reason"]]
data = ( data = (
'{"serial": "' '{"serial": "'
+ certificate.external_id + certificate.external_id
+ '","authority_key_id": "' + '","authority_key_id": "'
+ get_authority_key(certificate.body) + get_authority_key(certificate.body)
+ '", "reason": "superseded"}' + '", "reason": "'
+ crl_reason
+ '"}'
) )
current_app.logger.debug("Revoking cert: {0}".format(data)) current_app.logger.debug("Revoking cert: {0}".format(data))
response = self.session.post( response = self.session.post(

View File

@ -368,7 +368,7 @@ class DigiCertIssuerPlugin(IssuerPlugin):
certificate_id, certificate_id,
) )
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
"""Revoke a Digicert certificate.""" """Revoke a Digicert certificate."""
base_url = current_app.config.get("DIGICERT_URL") base_url = current_app.config.get("DIGICERT_URL")
@ -376,6 +376,11 @@ class DigiCertIssuerPlugin(IssuerPlugin):
create_url = "{0}/services/v2/certificate/{1}/revoke".format( create_url = "{0}/services/v2/certificate/{1}/revoke".format(
base_url, certificate.external_id base_url, certificate.external_id
) )
comments = reason["comments"] if "comments" in reason else ''
if "crl_reason" in reason:
comments += '(' + reason["crl_reason"] + ')'
metrics.send("digicert_revoke_certificate", "counter", 1) metrics.send("digicert_revoke_certificate", "counter", 1)
response = self.session.put(create_url, data=json.dumps({"comments": comments})) response = self.session.put(create_url, data=json.dumps({"comments": comments}))
return handle_response(response) return handle_response(response)
@ -575,7 +580,7 @@ class DigiCertCISIssuerPlugin(IssuerPlugin):
data["id"], data["id"],
) )
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
"""Revoke a Digicert certificate.""" """Revoke a Digicert certificate."""
base_url = current_app.config.get("DIGICERT_CIS_URL") base_url = current_app.config.get("DIGICERT_CIS_URL")
@ -584,6 +589,10 @@ class DigiCertCISIssuerPlugin(IssuerPlugin):
base_url, certificate.external_id base_url, certificate.external_id
) )
metrics.send("digicert_revoke_certificate_success", "counter", 1) metrics.send("digicert_revoke_certificate_success", "counter", 1)
comments = reason["comments"] if "comments" in reason else ''
if "crl_reason" in reason:
comments += '(' + reason["crl_reason"] + ')'
response = self.session.put(revoke_url, data=json.dumps({"comments": comments})) response = self.session.put(revoke_url, data=json.dumps({"comments": comments}))
if response.status_code != 204: if response.status_code != 204:

View File

@ -5,6 +5,7 @@ import sys
from flask import current_app from flask import current_app
from retrying import retry from retrying import retry
from lemur.constants import CRLReason
from lemur.plugins import lemur_entrust as entrust from lemur.plugins import lemur_entrust as entrust
from lemur.plugins.bases import IssuerPlugin, SourcePlugin from lemur.plugins.bases import IssuerPlugin, SourcePlugin
from lemur.extensions import metrics from lemur.extensions import metrics
@ -256,16 +257,20 @@ class EntrustIssuerPlugin(IssuerPlugin):
return cert, chain, external_id return cert, chain, external_id
@retry(stop_max_attempt_number=3, wait_fixed=1000) @retry(stop_max_attempt_number=3, wait_fixed=1000)
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, reason):
"""Revoke an Entrust certificate.""" """Revoke an Entrust certificate."""
base_url = current_app.config.get("ENTRUST_URL") base_url = current_app.config.get("ENTRUST_URL")
# make certificate revoke request # make certificate revoke request
revoke_url = f"{base_url}/certificates/{certificate.external_id}/revocations" revoke_url = f"{base_url}/certificates/{certificate.external_id}/revocations"
if not comments or comments == '': if "comments" not in reason or reason["comments"] == '':
comments = "revoked via API" comments = "revoked via API"
crl_reason = CRLReason.unspecified
if "crl_reason" in reason:
crl_reason = CRLReason[reason["crl_reason"]]
data = { data = {
"crlReason": "superseded", # enum (keyCompromise, affiliationChanged, superseded, cessationOfOperation) "crlReason": crl_reason, # per RFC 5280 section 5.3.1
"revocationComment": comments "revocationComment": comments
} }
response = self.session.post(revoke_url, json=data) response = self.session.post(revoke_url, json=data)

View File

@ -419,8 +419,8 @@ angular.module('lemur')
$uibModalInstance.dismiss('cancel'); $uibModalInstance.dismiss('cancel');
}; };
$scope.revoke = function (certificate) { $scope.revoke = function (certificate, crlReason) {
CertificateService.revoke(certificate).then( CertificateService.revoke(certificate, crlReason).then(
function () { function () {
toaster.pop({ toaster.pop({
type: 'success', type: 'success',

View File

@ -4,13 +4,13 @@
<h3 class="modal-title">Revoke <span class="text-muted"><small>{{ certificate.name }}</small></span></h3> <h3 class="modal-title">Revoke <span class="text-muted"><small>{{ certificate.name }}</small></span></h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form name="revokeForm" ng-if="!certificate.endpoints.length" novalidate> <form name="revokeForm" novalidate>
<p><strong>Certificate revocation is final. Once revoked the certificate is no longer valid.</strong></p> <p><strong>Certificate revocation is final. Once revoked the certificate is no longer valid.</strong></p>
<div class="form-horizontal"> <div class="form-horizontal">
<div class="form-group" <div class="form-group"
ng-class="{'has-error': revokeForm.confirm.$invalid, 'has-success': !revokeForm.$invalid&&revokeForm.confirm.$dirty}"> ng-class="{'has-error': revokeForm.confirm.$invalid, 'has-success': !revokeForm.$invalid&&revokeForm.confirm.$dirty}">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
Confirm Revocation Confirm Certificate Name
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input name="confirm" ng-model="confirm" placeholder='{{ certificate.name }}' <input name="confirm" ng-model="confirm" placeholder='{{ certificate.name }}'
@ -23,6 +23,27 @@
You must confirm certificate revocation.</p> You must confirm certificate revocation.</p>
</div> </div>
</div> </div>
<div class="form-group">
<label class="control-label col-sm-2">
Reason
</label>
<div class="col-sm-10">
<select class="form-control" ng-model="crlReason"
ng-options="option.value as option.name for option in [
{'name': 'Unspecified', 'value': 'unspecified'},
{'name': 'Key Compromise', 'value': 'keyCompromise'},
{'name': 'CA Compromise', 'value': 'cACompromise'},
{'name': 'Affiliation Changed', 'value': 'affiliationChanged'},
{'name': 'Superseded', 'value': 'superseded'},
{'name': 'Cessation of Operation', 'value': 'cessationOfOperation'},
{'name': 'Certificate Hold', 'value': 'certificateHold'},
{'name': 'Remove from CRL', 'value': 'removeFromCRL'},
{'name': 'Privilege Withdrawn', 'value': 'privilegeWithdrawn'},
{'name': 'Attribute Authority Compromise', 'value': 'aACompromise'}]"
ng-init="crlReason = 'unspecified'"></select>
</div>
</div>
</div> </div>
</form> </form>
<div ng-if="certificate.endpoints.length"> <div ng-if="certificate.endpoints.length">
@ -40,7 +61,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" ng-click="revoke(certificate)" ng-disabled="revokeForm.confirm.$invalid" <button type="submit" ng-click="revoke(certificate, crlReason)" ng-disabled="revokeForm.confirm.$invalid"
class="btn btn-danger">Revoke class="btn btn-danger">Revoke
</button> </button>
<button ng-click="cancel()" class="btn">Cancel</button> <button ng-click="cancel()" class="btn">Cancel</button>

View File

@ -313,8 +313,8 @@ angular.module('lemur')
return certificate.customPOST(certificate.exportOptions, 'export'); return certificate.customPOST(certificate.exportOptions, 'export');
}; };
CertificateService.revoke = function (certificate) { CertificateService.revoke = function (certificate, crlReason) {
return certificate.customPUT({}, 'revoke'); return certificate.customPUT({'crlReason':crlReason}, 'revoke');
}; };
return CertificateService; return CertificateService;

View File

@ -103,6 +103,30 @@ def test_delete_cert(session):
assert not cert_exists assert not cert_exists
def test_cleanup_after_revoke(session, issuer_plugin, crypto_authority):
from lemur.certificates.service import cleanup_after_revoke, get
from lemur.tests.factories import CertificateFactory
revoke_this = CertificateFactory(name="REVOKEME")
session.commit()
to_be_revoked = get(revoke_this.id)
assert to_be_revoked
to_be_revoked.notify = True
to_be_revoked.rotation = True
# Assuming the cert is revoked by corresponding issuer, update the records in lemur
cleanup_after_revoke(to_be_revoked)
revoked_cert = get(to_be_revoked.id)
# then not exist after delete
assert revoked_cert
assert revoked_cert.status == "revoked"
assert not revoked_cert.notify
assert not revoked_cert.rotation
assert not revoked_cert.destinations
def test_get_by_attributes(session, certificate): def test_get_by_attributes(session, certificate):
from lemur.certificates.service import get_by_attributes from lemur.certificates.service import get_by_attributes
@ -658,6 +682,23 @@ def test_certificate_upload_schema_wrong_chain_2nd(client):
} }
def test_certificate_revoke_schema():
from lemur.certificates.schemas import CertificateRevokeSchema
input = {
"comments": "testing certificate revoke schema",
"crl_reason": "cessationOfOperation"
}
data, errors = CertificateRevokeSchema().load(input)
assert not errors
input["crl_reason"] = "fakeCrlReason"
data, errors = CertificateRevokeSchema().load(input)
assert errors == {
"crl_reason": ['Not a valid choice.']
}
def test_create_basic_csr(client): def test_create_basic_csr(client):
csr_config = dict( csr_config = dict(
common_name="example.com", common_name="example.com",