Merge branch 'master' into entrust_source

This commit is contained in:
sirferl 2020-12-03 09:26:35 +01:00 committed by GitHub
commit 5651865347
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 205 additions and 60 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')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -37,18 +37,20 @@ Create a New Certificate
.. figure:: create_certificate.png .. figure:: create_certificate.png
Enter an owner, short description and the authority you wish to issue this certificate. Enter an owner, common name, short description and certificate authority you wish to issue this certificate.
Enter a common name into the certificate, if no validity range is selected two years is Depending upon the selected CA, the UI displays default validity of the certificate. You can select different
the default. validity by entering a custom date, if supported by the CA.
You can also add `Subject Alternate Names` or SAN for certificates that need to include more than one domains,
The first domain is the Common Name and all other domains are added here as DNSName entries.
You can add notification options and upload the created certificate to a destination, both You can add notification options and upload the created certificate to a destination, both
of these are editable features and can be changed after the certificate has been created. of these are editable features and can be changed after the certificate has been created.
.. figure:: certificate_extensions.png .. figure:: certificate_extensions.png
These options are typically for advanced users, the one exception is the `Subject Alternate Names` or SAN. These options are typically for advanced users. Lemur creates ECC based certificate (ECCPRIME256V1 in particular)
For certificates that need to include more than one domains, the first domain is the Common Name and all by default. One can change the key type using the dropdown option listed here.
other domains are added here as DNSName entries.
Import an Existing Certificate Import an Existing Certificate
@ -58,7 +60,7 @@ Import an Existing Certificate
Enter an owner, short description and public certificate. If there are intermediates and private keys Enter an owner, short description and public certificate. If there are intermediates and private keys
Lemur will track them just as it does if the certificate were created through Lemur. Lemur generates Lemur will track them just as it does if the certificate were created through Lemur. Lemur generates
a certificate name but you can override that by passing a value to the `Custom Name` field. a certificate name but you can override that by passing a value to the `Custom Certificate Name` field.
You can add notification options and upload the created certificate to a destination, both You can add notification options and upload the created certificate to a destination, both
of these are editable features and can be changed after the certificate has been created. of these are editable features and can be changed after the certificate has been created.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -323,6 +323,12 @@ unlock
Decrypts sensitive key material - used to decrypt the secrets stored in source during deployment. Decrypts sensitive key material - used to decrypt the secrets stored in source during deployment.
Automated celery tasks
~~~~~~~~~~~~~~~~~~~~~~
Please refer to :ref:`Periodic Tasks <PeriodicTasks>` to learn more about task scheduling in Lemur.
What's Next? What's Next?
------------ ------------

View File

@ -5,7 +5,6 @@
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import multiprocessing
import sys import sys
from flask import current_app from flask import current_app
from flask_principal import Identity, identity_changed from flask_principal import Identity, identity_changed
@ -26,9 +25,10 @@ from lemur.certificates.service import (
get_all_valid_certs, get_all_valid_certs,
get, get,
get_all_certs_attached_to_endpoint_without_autorotate, get_all_certs_attached_to_endpoint_without_autorotate,
revoke as revoke_certificate,
) )
from lemur.certificates.verify import verify_string from lemur.certificates.verify import verify_string
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS, CRLReason
from lemur.deployment import service as deployment_service from lemur.deployment import service as deployment_service
from lemur.domains.models import Domain from lemur.domains.models import Domain
from lemur.endpoints import service as endpoint_service from lemur.endpoints import service as endpoint_service
@ -586,11 +586,10 @@ def worker(data, commit, reason):
parts = [x for x in data.split(" ") if x] parts = [x for x in data.split(" ") if x]
try: try:
cert = get(int(parts[0].strip())) cert = get(int(parts[0].strip()))
plugin = plugins.get(cert.authority.plugin_name)
print("[+] Revoking certificate. Id: {0} Name: {1}".format(cert.id, cert.name)) print("[+] Revoking certificate. Id: {0} Name: {1}".format(cert.id, cert.name))
if commit: if commit:
plugin.revoke_certificate(cert, reason) revoke_certificate(cert, reason)
metrics.send( metrics.send(
"certificate_revoke", "certificate_revoke",
@ -620,10 +619,10 @@ def clear_pending():
v.clear_pending_certificates() v.clear_pending_certificates()
@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("-id", "--certid", dest="cert_id", help="ID of the certificate to be revoked")
) @manager.option("-r", "--reason", dest="reason", default="unspecified", help="CRL Reason as per RFC 5280 section 5.3.1")
@manager.option("-r", "--reason", dest="reason", help="Reason to revoke certificate.") @manager.option("-m", "--message", dest="message", help="Message explaining reason for revocation")
@manager.option( @manager.option(
"-c", "-c",
"--commit", "--commit",
@ -632,20 +631,32 @@ def clear_pending():
default=False, default=False,
help="Persist changes.", help="Persist changes.",
) )
def revoke(path, reason, commit): def revoke(path, cert_id, reason, message, commit):
""" """
Revokes given certificate. Revokes given certificate.
""" """
if not path and not cert_id:
print("[!] No input certificates mentioned to revoke")
return
if path and cert_id:
print("[!] Please mention single certificate id (-id) or input file (-p)")
return
if commit: if commit:
print("[!] Running in COMMIT mode.") print("[!] Running in COMMIT mode.")
print("[+] Starting certificate revocation.") print("[+] Starting certificate revocation.")
with open(path, "r") as f: if reason not in CRLReason.__members__:
args = [[x, commit, reason] for x in f.readlines()[2:]] reason = CRLReason.unspecified.name
comments = {"comments": message, "crl_reason": reason}
with multiprocessing.Pool(processes=3) as pool: if cert_id:
pool.starmap(worker, args) worker(cert_id, commit, comments)
else:
with open(path, "r") as f:
for x in f.readlines()[2:]:
worker(x, commit, comments)
@manager.command @manager.command

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
@ -1413,6 +1413,11 @@ class CertificateRevoke(AuthenticatedResource):
Host: example.com Host: example.com
Accept: application/json, text/javascript Accept: application/json, text/javascript
{
"crlReason": "affiliationChanged",
"comments": "Additional details if any"
}
**Example response**: **Example response**:
.. sourcecode:: http .. sourcecode:: http
@ -1422,12 +1427,13 @@ class CertificateRevoke(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
'id': 1 "id": 1
} }
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated or cert attached to LB
:statuscode 400: encountered error, more details in error message
""" """
cert = service.get(certificate_id) cert = service.get(certificate_id)
@ -1459,16 +1465,18 @@ class CertificateRevoke(AuthenticatedResource):
403, 403,
) )
plugin = plugins.get(cert.authority.plugin_name) try:
plugin.revoke_certificate(cert, data) error_message = service.revoke(cert, data)
log_service.create(g.current_user, "revoke_cert", certificate=cert)
log_service.create(g.current_user, "revoke_cert", certificate=cert) if error_message:
return dict(message=f"Certificate (id:{cert.id}) is revoked - {error_message}"), 400
# Perform cleanup after revoke return dict(id=cert.id)
error_message = service.cleanup_after_revoke(cert) except NotImplementedError as ne:
if error_message: return dict(message="Revoke is not implemented for issuer of this certificate"), 400
return dict(message=f"Certificate (id:{cert.id}) is revoked - {error_message}"), 400 except Exception as e:
return dict(id=cert.id) sentry.captureException()
return dict(message=f"Failed to revoke: {str(e)}"), 400
api.add_resource( api.add_resource(

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,17 @@ CERTIFICATE_KEY_TYPES = [
"ECCSECT409R1", "ECCSECT409R1",
"ECCSECT571R2", "ECCSECT571R2",
] ]
# As per RFC 5280 section 5.3.1 (https://tools.ietf.org/html/rfc5280#section-5.3.1)
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
@ -257,16 +258,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",

View File

@ -1,7 +1,7 @@
# Note: python-ldap from requirements breaks due to readthedocs.io not having the correct header files # Note: python-ldap from requirements breaks due to readthedocs.io not having the correct header files
# The `make up-reqs` will update all requirement text files, and forcibly remove python-ldap # The `make up-reqs` will update all requirement text files, and forcibly remove python-ldap
# from requirements-docs.txt # from requirements-docs.txt
-r requirements.txt # However, dependabot doesn't use `make up-reqs`, so `-r requirements.txt` has been removed completely.
sphinx sphinx
sphinxcontrib-httpdomain sphinxcontrib-httpdomain
sphinx-rtd-theme sphinx-rtd-theme

View File

@ -79,7 +79,6 @@ pyrfc3339==1.1 # via -r requirements.txt, acme
python-dateutil==2.8.1 # via -r requirements.txt, alembic, arrow, botocore python-dateutil==2.8.1 # via -r requirements.txt, alembic, arrow, botocore
python-editor==1.0.4 # via -r requirements.txt, alembic python-editor==1.0.4 # via -r requirements.txt, alembic
python-json-logger==0.1.11 # via -r requirements.txt, logmatic-python python-json-logger==0.1.11 # via -r requirements.txt, logmatic-python
python-ldap==3.3.1 # via -r requirements.txt
pytz==2019.3 # via -r requirements.txt, acme, babel, celery, flask-restful, pyrfc3339 pytz==2019.3 # via -r requirements.txt, acme, babel, celery, flask-restful, pyrfc3339
pyyaml==5.3.1 # via -r requirements.txt, cloudflare pyyaml==5.3.1 # via -r requirements.txt, cloudflare
raven[flask]==6.10.0 # via -r requirements.txt raven[flask]==6.10.0 # via -r requirements.txt