From 5ae872e1ea2916bd90e0655afa8847383553a03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Thu, 18 May 2017 22:29:49 +0300 Subject: [PATCH] api: Add preliminary SCEP support --- .gitignore | 4 + certidude/api/__init__.py | 5 + certidude/api/scep.py | 273 +++++++++++++++++++++++++ certidude/authority.py | 13 +- certidude/config.py | 2 + certidude/signer.py | 16 +- certidude/templates/server/server.conf | 3 + 7 files changed, 312 insertions(+), 4 deletions(-) create mode 100644 certidude/api/scep.py diff --git a/.gitignore b/.gitignore index 6c829c8..67ca749 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,7 @@ node_modules/ # Ignore autogenerated files certidude/static/js/nunjucks* certidude/static/js/templates.js + +# Ignore patch +*.orig +*.rej diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index b12a0a1..9e50cbd 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -210,6 +210,11 @@ def certidude_app(log_handlers=[]): # Bootstrap resource app.add_route("/api/bootstrap/", BootstrapResource()) + # Add SCEP handler if we have any whitelisted subnets + if config.SCEP_SUBNETS: + from .scep import SCEPResource + app.add_route("/api/scep/", SCEPResource()) + # Add sink for serving static files app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) diff --git a/certidude/api/scep.py b/certidude/api/scep.py new file mode 100644 index 0000000..44bb4e7 --- /dev/null +++ b/certidude/api/scep.py @@ -0,0 +1,273 @@ +from __future__ import unicode_literals, division, absolute_import, print_function +import click +import hashlib +import os +from asn1crypto import cms, algos, x509 +from asn1crypto.core import ObjectIdentifier, SetOf, PrintableString +from base64 import b64decode, b64encode +from certbuilder import pem_armor_certificate +from certidude import authority, push, config +from certidude.firewall import whitelist_subnets +from oscrypto import keys, asymmetric, symmetric +from oscrypto.errors import SignatureError + +# Monkey patch asn1crypto + +class SetOfPrintableString(SetOf): + _child_spec = PrintableString + +cms.CMSAttributeType._map['2.16.840.1.113733.1.9.2'] = "message_type" +cms.CMSAttributeType._map['2.16.840.1.113733.1.9.3'] = "pki_status" +cms.CMSAttributeType._map['2.16.840.1.113733.1.9.4'] = "fail_info" +cms.CMSAttributeType._map['2.16.840.1.113733.1.9.5'] = "sender_nonce" +cms.CMSAttributeType._map['2.16.840.1.113733.1.9.6'] = "recipient_nonce" +cms.CMSAttributeType._map['2.16.840.1.113733.1.9.7'] = "trans_id" + +cms.CMSAttribute._oid_specs['message_type'] = SetOfPrintableString +cms.CMSAttribute._oid_specs['pki_status'] = SetOfPrintableString +cms.CMSAttribute._oid_specs['fail_info'] = SetOfPrintableString +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 SCEPBadAlg(SCEPError): code = 0 +class SCEPBadMessageCheck(SCEPError): code = 1 +class SCEPBadRequest(SCEPError): code = 2 +class SCEPBadTime(SCEPError): code = 3 +class SCEPBadCertId(SCEPError): code = 4 + +class SCEPResource(object): + @whitelist_subnets(config.SCEP_SUBNETS) + def on_get(self, req, resp): + operation = req.get_param("operation") + if operation.lower() == "getcacert": + resp.stream = keys.parse_certificate(authority.ca_buf).dump() + resp.append_header("Content-Type", "application/x-x509-ca-cert") + return + + # Parse CA certificate + fh = open(config.AUTHORITY_CERTIFICATE_PATH) + server_certificate = asymmetric.load_certificate(fh.read()) + fh.close() + + # If we bump into exceptions later + encrypted_container = b"" + attr_list = [ + cms.CMSAttribute({ + 'type': "message_type", + 'values': ["3"] + }), + cms.CMSAttribute({ + 'type': "pki_status", + 'values': ["2"] # rejected + }) + ] + + try: + info = cms.ContentInfo.load(b64decode(req.get_param("message", required=True))) + + ############################################### + ### Verify signature of the outer container ### + ############################################### + + signed_envelope = info['content'] + encap_content_info = signed_envelope['encap_content_info'] + encap_content = encap_content_info['content'] + + # TODO: try except + current_certificate, = signed_envelope["certificates"] + signer, = signed_envelope["signer_infos"] + + # 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" + message_digest = None + transaction_id = None + sender_nonce = None + + for attr in signer["signed_attrs"]: + if attr["type"].native == "sender_nonce": + sender_nonce, = attr["values"] + elif attr["type"].native == "trans_id": + 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() + + assert message_digest + msg = signer["signed_attrs"].dump(force=True) + assert msg[0] == b"\xa0", repr(msg[0]) + + # Verify signature + try: + asymmetric.rsa_pkcs1v15_verify( + asymmetric.load_certificate(current_certificate.dump()), + signer["signature"].native, + b"\x31" + msg[1:], # wtf?! + "md5") + except SignatureError: + raise SCEPBadMessageCheck() + + ############################### + ### Decrypt inner container ### + ############################### + + info = cms.ContentInfo.load(encap_content.native) + encrypted_envelope = info['content'] + encrypted_content_info = encrypted_envelope['encrypted_content_info'] + iv = encrypted_content_info['content_encryption_algorithm']['parameters'].native + + if encrypted_content_info['content_encryption_algorithm']["algorithm"].native != "des": + raise SCEPBadAlgo() + + encrypted_content = encrypted_content_info['encrypted_content'].native + recipient, = encrypted_envelope['recipient_infos'] + + if recipient.native["rid"]["serial_number"] != authority.ca_cert.serial: + raise SCEPBadCertId() + + # Since CA private key is not directly readable here, we'll redirect it to signer socket + key = b64decode(authority.signer_exec("decrypt-pkcs7", b64encode(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 = authority.store_request(buf, overwrite=True) + cert, buf = authority.sign(common_name, overwrite=True) + signed_certificate = asymmetric.load_certificate(buf) + content = signed_certificate.asn1.dump() + + except SCEPError, e: + attr_list.append(cms.CMSAttribute({ + 'type': "fail_info", + 'values': ["%d" % e.code] + })) + else: + + ################################## + ### Degenerate inner container ### + ################################## + + degenerate = cms.ContentInfo({ + 'content_type': 'signed_data', + 'content': cms.SignedData({ + 'version': 'v1', + 'certificates': [signed_certificate.asn1], + 'digest_algorithms': [cms.DigestAlgorithm({ + 'algorithm':'md5' + })], + 'encap_content_info': { + 'content_type': 'data', + 'content': cms.ContentInfo({ + 'content_type': 'signed_data', + 'content': None + }).dump() + }, + 'signer_infos': [] + }) + }) + + + ################################ + ### Encrypt middle container ### + ################################ + + key = os.urandom(8) + iv, encrypted_content = symmetric.des_cbc_pkcs5_encrypt(key, degenerate.dump(), os.urandom(8)) + assert degenerate.dump() == symmetric.tripledes_cbc_pkcs5_decrypt(key*3, encrypted_content, iv) + + ri = cms.RecipientInfo({ + 'ktri': cms.KeyTransRecipientInfo({ + 'version': 'v0', + 'rid': cms.RecipientIdentifier({ + 'issuer_and_serial_number': cms.IssuerAndSerialNumber({ + 'issuer': current_certificate.chosen["tbs_certificate"]["issuer"], + 'serial_number': current_certificate.chosen["tbs_certificate"]["serial_number"], + }), + }), + 'key_encryption_algorithm': { + 'algorithm': 'rsa' + }, + 'encrypted_key': asymmetric.rsa_pkcs1v15_encrypt( + asymmetric.load_certificate(current_certificate.chosen.dump()), key) + }) + }) + + encrypted_container = cms.ContentInfo({ + 'content_type': 'enveloped_data', + 'content': cms.EnvelopedData({ + 'version': 'v1', + 'recipient_infos': [ri], + 'encrypted_content_info': { + 'content_type': 'data', + 'content_encryption_algorithm': { + 'algorithm': 'des', + 'parameters': iv + }, + 'encrypted_content': encrypted_content + } + }) + }).dump() + + attr_list = [ + cms.CMSAttribute({ + 'type': 'message_digest', + 'values': [hashlib.sha1(encrypted_container).digest()] + }), + cms.CMSAttribute({ + 'type': "message_type", + 'values': ["3"] + }), + cms.CMSAttribute({ + 'type': "pki_status", + 'values': ["0"] # ok + }) + ] + finally: + + ############################## + ### Signed outer container ### + ############################## + + attrs = cms.CMSAttributes(attr_list + [ + cms.CMSAttribute({ + 'type': "recipient_nonce", + 'values': [sender_nonce] + }), + cms.CMSAttribute({ + 'type': 'trans_id', + 'values': [transaction_id] + }) + ]) + + signer = cms.SignerInfo({ + "signed_attrs": attrs, + 'version':'v1', + 'sid': cms.SignerIdentifier({ + 'issuer_and_serial_number': cms.IssuerAndSerialNumber({ + 'issuer': server_certificate.asn1["tbs_certificate"]["issuer"], + 'serial_number': server_certificate.asn1["tbs_certificate"]["serial_number"], + }), + }), + 'digest_algorithm': algos.DigestAlgorithm({'algorithm': 'sha1'}), + 'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': 'rsassa_pkcs1v15'}), + 'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(b"\x31" + attrs.dump()[1:]))) + }) + + resp.append_header("Content-Type", "application/x-pki-message") + resp.body = cms.ContentInfo({ + 'content_type': 'signed_data', + 'content': cms.SignedData({ + 'version': 'v1', + 'certificates': [x509.Certificate.load(server_certificate.asn1.dump())], # wat + 'digest_algorithms': [cms.DigestAlgorithm({ + 'algorithm':'sha1' + })], + 'encap_content_info': { + 'content_type': 'data', + 'content': encrypted_container + }, + 'signer_infos': [signer] + }) + }).dump() diff --git a/certidude/authority.py b/certidude/authority.py index cde4810..3104ee0 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -12,6 +12,7 @@ from cryptography import x509 from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.serialization import Encoding from certidude import config, push, mailer, const from certidude import errors from jinja2 import Template @@ -81,7 +82,13 @@ def store_request(buf, overwrite=False): if not buf: raise ValueError("No signing request supplied") - csr = x509.load_pem_x509_csr(buf, backend=default_backend()) + if isinstance(buf, unicode): + csr = x509.load_pem_x509_csr(buf, backend=default_backend()) + elif isinstance(buf, str): + csr = x509.load_der_x509_csr(buf, backend=default_backend()) + buf = csr.public_bytes(Encoding.PEM) + else: + raise ValueError("Invalid type, expected str for PEM and bytes for DER") common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) # TODO: validate common name again @@ -92,7 +99,7 @@ def store_request(buf, overwrite=False): # If there is cert, check if it's the same - if os.path.exists(request_path): + if os.path.exists(request_path) and not overwrite: if open(request_path).read() == buf: raise errors.RequestExists("Request already exists") else: @@ -106,7 +113,7 @@ def store_request(buf, overwrite=False): mailer.send("request-stored.md", attachments=(attach_csr,), common_name=common_name.value) - return csr + return csr, common_name.value def signer_exec(cmd, *bits): diff --git a/certidude/config.py b/certidude/config.py index c4c7ae7..9b9545e 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -32,6 +32,8 @@ AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "autosign subnets").split(" ") if j]) REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS) +SCEP_SUBNETS = set([ipaddress.ip_network(j) for j in + cp.get("authorization", "scep subnets").split(" ") if j]) AUTHORITY_DIR = "/var/lib/certidude" AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") diff --git a/certidude/signer.py b/certidude/signer.py index 0a61445..2556c68 100644 --- a/certidude/signer.py +++ b/certidude/signer.py @@ -5,11 +5,13 @@ import socket import os import asyncore import asynchat +from base64 import b64decode, b64encode from certidude import const, config from cryptography import x509 from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives import hashes, padding, serialization from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.hazmat.primitives.asymmetric import padding from datetime import datetime, timedelta from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID import random @@ -64,6 +66,18 @@ class SignHandler(asynchat.async_chat): self.close() raise asyncore.ExitNow() + elif cmd == "sign-pkcs7": + signer = self.server.private_key.signer( + padding.PKCS1v15(), + hashes.SHA1() + ) + signer.update(b64decode(body)) + self.send(b64encode(signer.finalize())) + + elif cmd == "decrypt-pkcs7": + self.send(b64encode(self.server.private_key.decrypt(b64decode(body), padding.PKCS1v15()))) + self.close() + elif cmd == "sign-request": # Only common name and public key are used from request request = x509.load_pem_x509_csr(body, default_backend()) diff --git a/certidude/templates/server/server.conf b/certidude/templates/server/server.conf index 9742636..24d62e5 100644 --- a/certidude/templates/server/server.conf +++ b/certidude/templates/server/server.conf @@ -56,6 +56,9 @@ request subnets = 0.0.0.0/0 # Certificates are automatically signed for these subnets autosign subnets = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 +# Simple Certificate Enrollment Protocol enabled subnets +scep subnets = + [logging] # Disable logging ;backend =