Merge branch 'master' into add-ca-cert-notifications
|
@ -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')
|
||||||
|
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 74 KiB |
|
@ -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.
|
||||||
|
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 83 KiB |
|
@ -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?
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 -
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|