1
0
mirror of https://github.com/laurivosandi/certidude synced 2025-10-30 08:59:13 +00:00

Several updates

* Subnets configuration option for Kerberos machine enrollment
* Configurable script snippets via [service] configuration section
* Preliminary revocation reason support
* Improved signature profile support
* Add domain components to DN to distinguish certificate CN's namespace
* Image builder improvements, add Elliptic Curve support
* Added GetCACaps operation and more digest algorithms for SCEP
* Generate certificate and CRL serial from timestamp (64+32bits) and random bytes (56bits)
* Move client storage pool to /etc/certidude/authority/
* Cleanups & bugfixes
This commit is contained in:
2018-04-27 07:48:15 +00:00
parent 94e5f72566
commit 5e9251f365
35 changed files with 1192 additions and 580 deletions

View File

@@ -8,6 +8,7 @@ import hashlib
from datetime import datetime
from xattr import listxattr, getxattr
from certidude.auth import login_required
from certidude.common import cert_to_dn
from certidude.user import User
from certidude.decorators import serialize, csrf_protection
from certidude import const, config, authority
@@ -54,7 +55,7 @@ class SessionResource(AuthorityHandler):
)
def serialize_revoked(g):
for common_name, path, buf, cert, signed, expired, revoked in g():
for common_name, path, buf, cert, signed, expired, revoked, reason in g():
yield dict(
serial = "%x" % cert.serial_number,
common_name = common_name,
@@ -62,6 +63,7 @@ class SessionResource(AuthorityHandler):
signed = signed,
expired = expired,
revoked = revoked,
reason = reason,
sha256sum = hashlib.sha256(buf).hexdigest())
def serialize_certificates(g):
@@ -69,7 +71,7 @@ class SessionResource(AuthorityHandler):
# Extract certificate tags from filesystem
try:
tags = []
for tag in getxattr(path, "user.xdg.tags").decode("ascii").split(","):
for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","):
if "=" in tag:
k, v = tag.split("=", 1)
else:
@@ -116,7 +118,7 @@ class SessionResource(AuthorityHandler):
extensions = dict([
(e["extn_id"].native, e["extn_value"].native)
for e in cert["tbs_certificate"]["extensions"]
if e["extn_value"] in ("extended_key_usage",)])
if e["extn_id"].native in ("extended_key_usage",)])
)
if req.context.get("user").is_admin():
@@ -131,6 +133,11 @@ class SessionResource(AuthorityHandler):
mail=req.context.get("user").mail
),
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
service = dict(
protocols = config.SERVICE_PROTOCOLS,
routers = [j[0] for j in authority.list_signed(
common_name=config.SERVICE_ROUTERS)]
),
authority = dict(
builder = dict(
profiles = config.IMAGE_BUILDER_PROFILES
@@ -143,13 +150,15 @@ class SessionResource(AuthorityHandler):
certificate = dict(
algorithm = authority.public_key.algorithm,
common_name = self.authority.certificate.subject.native["common_name"],
distinguished_name = cert_to_dn(self.authority.certificate),
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
blob = self.authority.certificate_buf.decode("ascii"),
),
mailer = dict(
name = config.MAILER_NAME,
address = config.MAILER_ADDRESS
) if config.MAILER_ADDRESS else None,
machine_enrollment_allowed=config.MACHINE_ENROLLMENT_ALLOWED,
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS,
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
@@ -278,8 +287,8 @@ def certidude_app(log_handlers=[]):
log_handlers.append(LogHandler(uri))
app.add_route("/api/log/", LogResource(uri))
elif config.LOGGING_BACKEND == "syslog":
from logging.handlers import SyslogHandler
log_handlers.append(SyslogHandler())
from logging.handlers import SysLogHandler
log_handlers.append(SysLogHandler())
# Browsing syslog via HTTP is obviously not possible out of the box
elif config.LOGGING_BACKEND:
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)

View File

@@ -26,14 +26,15 @@ class AttributeResource(object):
Results made available only to lease IP address.
"""
try:
path, buf, cert, attribs = self.authority.get_attributes(cn, namespace=self.namespace)
path, buf, cert, attribs = self.authority.get_attributes(cn,
namespace=self.namespace, flat=True)
except IOError:
raise falcon.HTTPNotFound()
else:
return attribs
@csrf_protection
@whitelist_subject # TODO: sign instead
@whitelist_subject
def on_post(self, req, resp, cn):
namespace = ("user.%s." % self.namespace).encode("ascii")
try:
@@ -43,7 +44,7 @@ class AttributeResource(object):
else:
for key in req.params:
if not re.match("[a-z0-9_\.]+$", key):
raise falcon.HTTPBadRequest("Invalid key")
raise falcon.HTTPBadRequest("Invalid key %s" % key)
valid = set()
for key, value in req.params.items():
identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii")

View File

@@ -4,8 +4,9 @@ import falcon
import logging
import os
import subprocess
from certidude import config, const
from certidude import config, const, authority
from certidude.auth import login_required, authorize_admin
from certidude.common import cert_to_dn
from jinja2 import Template
logger = logging.getLogger(__name__)
@@ -14,6 +15,8 @@ class ImageBuilderResource(object):
@login_required
@authorize_admin
def on_get(self, req, resp, profile, suggested_filename):
router = [j[0] for j in authority.list_signed(
common_name=config.cp2.get(profile, "router"))][0]
model = config.cp2.get(profile, "model")
build_script_path = config.cp2.get(profile, "command")
overlay_path = config.cp2.get(profile, "overlay")
@@ -35,7 +38,10 @@ class ImageBuilderResource(object):
stdout=open(log_path, "w"), stderr=subprocess.STDOUT,
close_fds=True, shell=False,
cwd=os.path.dirname(os.path.realpath(build_script_path)),
env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin",
env={"PROFILE": model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin",
"ROUTER": router,
"AUTHORITY_CERTIFICATE_ALGORITHM": authority.public_key.algorithm,
"AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME": cert_to_dn(authority.certificate),
"BUILD":build, "OVERLAY":build + "/overlay/"},
startupinfo=None, creationflags=0)
proc.communicate()

View File

@@ -1,6 +1,7 @@
import falcon
import logging
import os
import re
import xattr
from datetime import datetime
from certidude import config, push
@@ -32,10 +33,9 @@ class LeaseResource(AuthorityHandler):
@authorize_server
def on_post(self, req, resp):
client_common_name = req.get_param("client", required=True)
if "=" in client_common_name: # It's actually DN, resolve it to CN
_, client_common_name = client_common_name.split(" CN=", 1)
if "," in client_common_name:
client_common_name, _ = client_common_name.split(",", 1)
m = re.match("CN=(.+?),", client_common_name) # It's actually DN, resolve it to CN
if m:
client_common_name, = m.groups()
path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions
if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan

View File

@@ -53,24 +53,29 @@ class OCSPResource(AuthorityHandler):
assert serial > 0, "Serial number correctness check failed"
try:
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % serial))
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % serial))
assert link_target.startswith("../")
assert link_target.endswith(".pem")
path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4])
if serial != cert.serial_number:
logger.error("Certificate store integrity check failed, %s refers to certificate with serial %x" % (link_target, cert.serial_number))
logger.error("Certificate store integrity check failed, %s refers to certificate with serial %040x", link_target, cert.serial_number)
raise EnvironmentError("Integrity check failed")
logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'good'",
req.context.get("remote_addr"), cert.subject.native["common_name"], serial)
status = ocsp.CertStatus(name='good', value=None)
except EnvironmentError:
try:
path, buf, cert, signed, expires, revoked = self.authority.get_revoked(serial)
path, buf, cert, signed, expires, revoked, reason = self.authority.get_revoked(serial)
logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'revoked' due to %s",
req.context.get("remote_addr"), cert.subject.native["common_name"], serial, reason)
status = ocsp.CertStatus(
name='revoked',
value={
'revocation_time': revoked,
'revocation_reason': "key_compromise",
'revocation_reason': reason,
})
except EnvironmentError:
logger.info("OCSP responder queried for unknown serial %040x from %s", serial, req.context.get("remote_addr"))
status = ocsp.CertStatus(name="unknown", value=None)
responses.append({

View File

@@ -14,7 +14,7 @@ from certidude.profile import SignatureProfile
from datetime import datetime
from oscrypto import asymmetric
from oscrypto.errors import SignatureError
from xattr import getxattr
from xattr import getxattr, setxattr
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets, whitelist_content_types
@@ -59,26 +59,39 @@ class RequestListResource(AuthorityHandler):
common_name = csr["certification_request_info"]["subject"].native["common_name"]
"""
Determine whether autosign is allowed to overwrite already issued
certificates automatically
"""
overwrite_allowed = False
for subnet in config.OVERWRITE_SUBNETS:
if req.context.get("remote_addr") in subnet:
overwrite_allowed = True
break
"""
Handle domain computer automatic enrollment
"""
machine = req.context.get("machine")
if machine:
if config.MACHINE_ENROLLMENT_ALLOWED:
if common_name != machine:
raise falcon.HTTPBadRequest(
"Bad request",
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
reasons.append("machine enrollment not allowed from %s" % req.context.get("remote_addr"))
for subnet in config.MACHINE_ENROLLMENT_SUBNETS:
if req.context.get("remote_addr") in subnet:
if common_name != machine:
raise falcon.HTTPBadRequest(
"Bad request",
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
# Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-pem-file")
cert, resp.body = self.authority._sign(csr, body,
profile=config.PROFILES["rw"], overwrite=overwrite_allowed)
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
machine, req.context.get("remote_addr"))
return
# Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-pem-file")
cert, resp.body = self.authority._sign(csr, body,
profile=config.PROFILES["rw"], overwrite=True)
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
machine, req.context.get("remote_addr"))
return
else:
reasons.append("Machine enrollment not allowed")
"""
Attempt to renew certificate using currently valid key pair
@@ -94,58 +107,61 @@ class RequestListResource(AuthorityHandler):
# Same public key
if cert_pk == csr_pk:
buf = req.get_header("X-SSL-CERT")
# Used mutually authenticated TLS handshake, assume renewal
if buf:
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
# Used mutually authenticated TLS handshake, assume renewal
header, _, der_bytes = pem.unarmor(buf.replace("\t", "\n").replace("\n\n", "\n").encode("ascii"))
handshake_cert = x509.Certificate.load(der_bytes)
if handshake_cert.native == cert.native:
for subnet in config.RENEWAL_SUBNETS:
if req.context.get("remote_addr") in subnet:
resp.set_header("Content-Type", "application/x-x509-user-cert")
setxattr(path, "user.revocation.reason", "superseded")
_, resp.body = self.authority._sign(csr, body, overwrite=True,
profile=SignatureProfile.from_cert(cert))
logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return
# No header supplied, redirect to signed API call
resp.status = falcon.HTTP_SEE_OTHER
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
return
reasons.append("renewal failed")
else:
# No renewal requested, redirect to signed API call
resp.status = falcon.HTTP_SEE_OTHER
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
return
"""
Process automatic signing if the IP address is whitelisted,
autosigning was requested and certificate can be automatically signed
"""
if req.get_param_as_bool("autosign"):
if not self.authority.server_flags(common_name):
for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet:
try:
resp.set_header("Content-Type", "application/x-pem-file")
_, resp.body = self.authority._sign(csr, body, profile=config.PROFILES["rw"])
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return
except EnvironmentError:
logger.info("Autosign for %s from %s failed, signed certificate already exists",
common_name, req.context.get("remote_addr"))
reasons.append("Autosign failed, signed certificate already exists")
break
else:
reasons.append("Autosign failed, IP address not whitelisted")
for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet:
try:
resp.set_header("Content-Type", "application/x-pem-file")
_, resp.body = self.authority._sign(csr, body,
overwrite=overwrite_allowed, profile=config.PROFILES["rw"])
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return
except EnvironmentError:
logger.info("Autosign for %s from %s failed, signed certificate already exists",
common_name, req.context.get("remote_addr"))
reasons.append("autosign failed, signed certificate already exists")
break
else:
reasons.append("Autosign failed, only client certificates allowed to be signed automatically")
reasons.append("autosign failed, IP address not whitelisted")
else:
reasons.append("autosign not requested")
# Attempt to save the request otherwise
try:
request_path, _, _ = self.authority.store_request(body,
address=str(req.context.get("remote_addr")))
except errors.RequestExists:
reasons.append("Same request already uploaded exists")
reasons.append("same request already uploaded exists")
# We should still redirect client to long poll URL below
except errors.DuplicateCommonNameError:
# TODO: Certificate renewal
logger.warning("Rejected signing request with overlapping common name from %s",
logger.warning("rejected signing request with overlapping common name from %s",
req.context.get("remote_addr"))
raise falcon.HTTPConflict(
"CSR with such CN already exists",
@@ -154,14 +170,15 @@ class RequestListResource(AuthorityHandler):
push.publish("request-submitted", common_name)
# Wait the certificate to be signed if waiting is requested
logger.info("Stored signing request %s from %s", common_name, req.context.get("remote_addr"))
logger.info("Stored signing request %s from %s, reasons: %s", common_name, req.context.get("remote_addr"), reasons)
if req.get_param("wait"):
# Redirect to nginx pub/sub
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
click.echo("Redirecting to: %s" % url)
resp.status = falcon.HTTP_SEE_OTHER
resp.set_header("Location", url)
logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url)
logger.debug("Redirecting signing request from %s to %s, reasons: %s", req.context.get("remote_addr"), url, ", ".join(reasons))
else:
# Request was accepted, but not processed
resp.status = falcon.HTTP_202

View File

@@ -1,4 +1,7 @@
import click
import falcon
import hashlib
import logging
import os
from asn1crypto import cms, algos
from asn1crypto.core import SetOf, PrintableString
@@ -9,6 +12,8 @@ from oscrypto.errors import SignatureError
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets
logger = logging.getLogger(__name__)
# Monkey patch asn1crypto
class SetOfPrintableString(SetOf):
@@ -28,21 +33,54 @@ cms.CMSAttribute._oid_specs['sender_nonce'] = cms.SetOfOctetString
cms.CMSAttribute._oid_specs['recipient_nonce'] = cms.SetOfOctetString
cms.CMSAttribute._oid_specs['trans_id'] = SetOfPrintableString
class SCEPError(Exception): code = 25 # system failure
class SCEPBadAlgo(SCEPError): code = 0
class SCEPBadMessageCheck(SCEPError): code = 1
class SCEPBadRequest(SCEPError): code = 2
class SCEPBadTime(SCEPError): code = 3
class SCEPBadCertId(SCEPError): code = 4
class SCEPError(Exception):
code = 25 # system failure
explaination = "General system failure"
class SCEPBadAlgo(SCEPError):
code = 0
explaination = "Unsupported algorithm in SCEP request"
class SCEPBadMessageCheck(SCEPError):
code = 1
explaination = "Integrity check failed for SCEP request"
class SCEPBadRequest(SCEPError):
code = 2
explaination = "Bad request"
class SCEPBadTime(SCEPError):
code = 3
explaination = "Bad time"
class SCEPBadCertId(SCEPError):
code = 4
explaination = "Certificate authority mismatch"
class SCEPDigestMismatch(SCEPBadMessageCheck):
explaination = "Digest mismatch"
class SCEPSignatureMismatch(SCEPBadMessageCheck):
explaination = "Signature mismatch"
class SCEPResource(AuthorityHandler):
@whitelist_subnets(config.SCEP_SUBNETS)
def on_get(self, req, resp):
operation = req.get_param("operation", required=True)
if operation.lower() == "getcacert":
if operation == "GetCACert":
resp.body = keys.parse_certificate(self.authority.certificate_buf).dump()
resp.append_header("Content-Type", "application/x-x509-ca-cert")
return
elif operation == "GetCACaps":
# TODO: return renewal flag based on renewal subnets config option
resp.body = "Renewal\nMD5\nSHA-1\nSHA-256\nSHA-512\nDES3\n"
return
elif operation == "PKIOperation":
pass
else:
raise falcon.HTTPBadRequest(
"Bad request",
"Unknown operation %s" % operation)
# If we bump into exceptions later
encrypted_container = b""
@@ -74,8 +112,14 @@ class SCEPResource(AuthorityHandler):
# TODO: compare cert to current one if we are renewing
assert signer["digest_algorithm"]["algorithm"].native == "md5"
assert signer["signature_algorithm"]["algorithm"].native == "rsassa_pkcs1v15"
digest_algorithm = signer["digest_algorithm"]["algorithm"].native
signature_algorithm = signer["signature_algorithm"]["algorithm"].native
if digest_algorithm not in ("md5", "sha1", "sha256", "sha512"):
raise SCEPBadAlgo()
if signature_algorithm != "rsassa_pkcs1v15":
raise SCEPBadAlgo()
message_digest = None
transaction_id = None
sender_nonce = None
@@ -87,8 +131,13 @@ class SCEPResource(AuthorityHandler):
transaction_id, = attr["values"]
elif attr["type"].native == "message_digest":
message_digest, = attr["values"]
if hashlib.md5(encap_content.native).digest() != message_digest.native:
raise SCEPBadMessageCheck()
if getattr(hashlib, digest_algorithm)(encap_content.native).digest() != message_digest.native:
raise SCEPDigestMismatch()
if not sender_nonce:
raise SCEPBadRequest()
if not transaction_id:
raise SCEPBadRequest()
assert message_digest
msg = signer["signed_attrs"].dump(force=True)
@@ -102,7 +151,8 @@ class SCEPResource(AuthorityHandler):
b"\x31" + msg[1:], # wtf?!
"md5")
except SignatureError:
raise SCEPBadMessageCheck()
raise SCEPSignatureMismatch()
###############################
### Decrypt inner container ###
@@ -122,14 +172,15 @@ class SCEPResource(AuthorityHandler):
if recipient.native["rid"]["serial_number"] != self.authority.certificate.serial_number:
raise SCEPBadCertId()
# Since CA private key is not directly readable here, we'll redirect it to signer socket
key = asymmetric.rsa_pkcs1v15_decrypt(
self.authority.private_key,
recipient.native["encrypted_key"])
if len(key) == 8: key = key * 3 # Convert DES to 3DES
buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv)
_, _, common_name = self.authority.store_request(buf, overwrite=True)
cert, buf = self.authority.sign(common_name, overwrite=True)
logger.info("SCEP client from %s requested with %s digest algorithm, %s signature",
req.context["remote_addr"], digest_algorithm, signature_algorithm)
cert, buf = self.authority.sign(common_name, profile=config.PROFILES["gw"], overwrite=True)
signed_certificate = asymmetric.load_certificate(buf)
content = signed_certificate.asn1.dump()
@@ -138,6 +189,7 @@ class SCEPResource(AuthorityHandler):
'type': "fail_info",
'values': ["%d" % e.code]
}))
logger.info("Failed to sign SCEP request due to: %s" % e.explaination)
else:
##################################
@@ -150,7 +202,8 @@ class SCEPResource(AuthorityHandler):
'version': "v1",
'certificates': [signed_certificate.asn1],
'digest_algorithms': [cms.DigestAlgorithm({
'algorithm': "md5"
'algorithm': digest_algorithm
})],
'encap_content_info': {
'content_type': "data",
@@ -208,7 +261,7 @@ class SCEPResource(AuthorityHandler):
attr_list = [
cms.CMSAttribute({
'type': "message_digest",
'values': [hashlib.sha1(encrypted_container).digest()]
'values': [getattr(hashlib, digest_algorithm)(encrypted_container).digest()]
}),
cms.CMSAttribute({
'type': "message_type",
@@ -245,12 +298,12 @@ class SCEPResource(AuthorityHandler):
'serial_number': self.authority.certificate.serial_number,
}),
}),
'digest_algorithm': algos.DigestAlgorithm({'algorithm': "sha1"}),
'digest_algorithm': algos.DigestAlgorithm({'algorithm': digest_algorithm}),
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': "rsassa_pkcs1v15"}),
'signature': asymmetric.rsa_pkcs1v15_sign(
self.authority.private_key,
b"\x31" + attrs.dump()[1:],
"sha1"
digest_algorithm
)
})
@@ -261,7 +314,7 @@ class SCEPResource(AuthorityHandler):
'version': "v1",
'certificates': [self.authority.certificate],
'digest_algorithms': [cms.DigestAlgorithm({
'algorithm': "sha1"
'algorithm': digest_algorithm
})],
'encap_content_info': {
'content_type': "data",

View File

@@ -44,7 +44,7 @@ class SignedCertificateDetailResource(AuthorityHandler):
resp.body = json.dumps(dict(
common_name = cn,
signer = signer_username,
serial = "%x" % cert.serial_number,
serial = "%040x" % cert.serial_number,
organizational_unit = cert.subject.native.get("organizational_unit_name"),
signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
@@ -54,7 +54,8 @@ class SignedCertificateDetailResource(AuthorityHandler):
extensions = dict([
(e["extn_id"].native, e["extn_value"].native)
for e in cert["tbs_certificate"]["extensions"]
if e["extn_value"] in ("extended_key_usage",)])
if e["extn_id"].native in ("extended_key_usage",)])
))
logger.debug("Served certificate %s to %s as application/json",
cn, req.context.get("remote_addr"))
@@ -69,5 +70,6 @@ class SignedCertificateDetailResource(AuthorityHandler):
def on_delete(self, req, resp, cn):
logger.info("Revoked certificate %s by %s from %s",
cn, req.context.get("user"), req.context.get("remote_addr"))
self.authority.revoke(cn)
self.authority.revoke(cn,
reason=req.get_param("reason", default="key_compromise"))