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:
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user