mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	api: Add preliminary SCEP support
This commit is contained in:
		
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -64,3 +64,7 @@ node_modules/ | ||||
| # Ignore autogenerated files | ||||
| certidude/static/js/nunjucks* | ||||
| certidude/static/js/templates.js | ||||
|  | ||||
| # Ignore patch | ||||
| *.orig | ||||
| *.rej | ||||
|   | ||||
| @@ -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"))) | ||||
|  | ||||
|   | ||||
							
								
								
									
										273
									
								
								certidude/api/scep.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								certidude/api/scep.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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 = | ||||
|   | ||||
		Reference in New Issue
	
	Block a user