mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 17:39:12 +00:00 
			
		
		
		
	api: Preliminary OCSP support
This commit is contained in:
		| @@ -69,6 +69,7 @@ Common: | ||||
|  | ||||
| * Standard request, sign, revoke workflow via web interface. | ||||
| * Kerberos and basic auth based web interface authentication. | ||||
| * Preliminary `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support. | ||||
| * PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind. | ||||
| * POSIX groups and Active Directory (LDAP) group membership based authorization. | ||||
| * Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``. | ||||
| @@ -94,8 +95,6 @@ HTTPS: | ||||
| TODO | ||||
| ---- | ||||
|  | ||||
| * `OCSP <https://tools.ietf.org/html/rfc4557>`_ support, needs a bit hacking since OpenSSL wrappers are not exposing the functionality. | ||||
| * `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support, a client implementation available `here <https://github.com/certnanny/sscep>`_. Not sure if we can implement server-side events within current standard. | ||||
| * WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_. | ||||
| * Use `pki.js <https://pkijs.org/>`_ for generating keypair in the browser when claiming a token. | ||||
| * Signer process logging. | ||||
|   | ||||
| @@ -215,6 +215,10 @@ def certidude_app(log_handlers=[]): | ||||
|         from .scep import SCEPResource | ||||
|         app.add_route("/api/scep/", SCEPResource()) | ||||
|  | ||||
|     if config.OCSP_SUBNETS: | ||||
|         from .ocsp import OCSPResource | ||||
|         app.add_route("/api/ocsp/", OCSPResource()) | ||||
|  | ||||
|     # Add sink for serving static files | ||||
|     app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) | ||||
|  | ||||
|   | ||||
							
								
								
									
										97
									
								
								certidude/api/ocsp.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								certidude/api/ocsp.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| from __future__ import unicode_literals, division, absolute_import, print_function | ||||
| import click | ||||
| import hashlib | ||||
| import os | ||||
| from asn1crypto.util import timezone | ||||
| from datetime import datetime, timedelta | ||||
|  | ||||
| from asn1crypto import cms, algos, x509, ocsp | ||||
| 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 | ||||
|  | ||||
| # openssl ocsp -issuer /var/lib/certidude/ca2.koodur.lan/ca_crt.pem -cert /var/lib/certidude/ca2.koodur.lan/signed/lauri-c720p.pem -text -url http://ca2.koodur.lan/api/ocsp/ | ||||
|  | ||||
| class OCSPResource(object): | ||||
|     def on_post(self, req, resp): | ||||
|         fh = open(config.AUTHORITY_CERTIFICATE_PATH) | ||||
|         server_certificate = asymmetric.load_certificate(fh.read()) | ||||
|         fh.close() | ||||
|  | ||||
|         ocsp_req = ocsp.OCSPRequest.load(req.stream.read()) | ||||
|         print(ocsp_req["tbs_request"].native) | ||||
|  | ||||
|         now = datetime.now(timezone.utc) | ||||
|         response_extensions = [] | ||||
|  | ||||
|         for ext in ocsp_req["tbs_request"]["request_extensions"]: | ||||
|             if ext["extn_id"] == "nonce": | ||||
|                 response_extensions.append( | ||||
|                     ocsp.ResponseDataExtension({ | ||||
|                         'extn_id': "nonce", | ||||
|                         'critical': False, | ||||
|                         'extn_value': ext["extn_value"] | ||||
|                     }) | ||||
|                 ) | ||||
|  | ||||
|         responses = [] | ||||
|         for item in ocsp_req["tbs_request"]["request_list"]: | ||||
|             serial = item["req_cert"]["serial_number"].native | ||||
|  | ||||
|             try: | ||||
|                 link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % serial)) | ||||
|                 assert link_target.startswith("../") | ||||
|                 assert link_target.endswith(".pem") | ||||
|                 path, buf, cert = authority.get_signed(link_target[3:-4]) | ||||
|                 if serial != cert.serial: | ||||
|                     raise EnvironmentError("integrity check failed") | ||||
|                 status = ocsp.CertStatus(name='good', value=None) | ||||
|             except EnvironmentError: | ||||
|                 try: | ||||
|                     path, buf, cert, revoked = authority.get_revoked(serial) | ||||
|                     status = ocsp.CertStatus( | ||||
|                         name='revoked', | ||||
|                         value={ | ||||
|                             'revocation_time': revoked, | ||||
|                             'revocation_reason': "key_compromise", | ||||
|                         }) | ||||
|                 except EnvironmentError: | ||||
|                     status = ocsp.CertStatus(name="unknown", value=None) | ||||
|  | ||||
|             responses.append({ | ||||
|                 'cert_id': { | ||||
|                     'hash_algorithm': { | ||||
|                         'algorithm': "sha1" | ||||
|                     }, | ||||
|                     'issuer_name_hash': server_certificate.asn1.subject.sha1, | ||||
|                     'issuer_key_hash': server_certificate.public_key.asn1.sha1, | ||||
|                     'serial_number': serial, | ||||
|                 }, | ||||
|                 'cert_status': status, | ||||
|                 'this_update': now, | ||||
|                 'single_extensions': [] | ||||
|             }) | ||||
|  | ||||
|         response_data = ocsp.ResponseData({ | ||||
|             'responder_id': ocsp.ResponderId(name='by_key', value=server_certificate.public_key.asn1.sha1), | ||||
|             'produced_at': now, | ||||
|             'responses': responses, | ||||
|             'response_extensions': response_extensions | ||||
|         }) | ||||
|  | ||||
|         resp.body = ocsp.OCSPResponse({ | ||||
|             'response_status': "successful", | ||||
|             'response_bytes': { | ||||
|                 'response_type': 'basic_ocsp_response', | ||||
|                 'response': { | ||||
|                     'tbs_response_data': response_data, | ||||
|                     'signature_algorithm': {'algorithm': "sha1_rsa"}, | ||||
|                     'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(response_data.dump()))), | ||||
|                     'certs': [server_certificate.asn1] | ||||
|                 } | ||||
|             } | ||||
|         }).dump() | ||||
|  | ||||
| @@ -139,7 +139,7 @@ class RequestListResource(object): | ||||
|  | ||||
|         # Attempt to save the request otherwise | ||||
|         try: | ||||
|             csr = authority.store_request(body) | ||||
|             csr = authority.store_request(body.decode("ascii")) | ||||
|         except errors.RequestExists: | ||||
|             reasons.append("Same request already uploaded exists") | ||||
|             # We should still redirect client to long poll URL below | ||||
|   | ||||
| @@ -50,10 +50,11 @@ def get_signed(common_name): | ||||
|         return path, buf, x509.load_pem_x509_certificate(buf, default_backend()) | ||||
|  | ||||
| def get_revoked(serial): | ||||
|     path = os.path.join(config.REVOKED_DIR, serial + ".pem") | ||||
|     path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial) | ||||
|     with open(path) as fh: | ||||
|         buf = fh.read() | ||||
|         return path, buf, x509.load_pem_x509_certificate(buf, default_backend()) | ||||
|         return path, buf, x509.load_pem_x509_certificate(buf, default_backend()), \ | ||||
|             datetime.utcfromtimestamp(os.stat(path).st_ctime) | ||||
|  | ||||
|  | ||||
| def get_attributes(cn): | ||||
| @@ -83,7 +84,7 @@ def store_request(buf, overwrite=False): | ||||
|         raise ValueError("No signing request supplied") | ||||
|  | ||||
|     if isinstance(buf, unicode): | ||||
|         csr = x509.load_pem_x509_csr(buf, backend=default_backend()) | ||||
|         csr = x509.load_pem_x509_csr(buf.encode("ascii"), backend=default_backend()) | ||||
|     elif isinstance(buf, str): | ||||
|         csr = x509.load_der_x509_csr(buf, backend=default_backend()) | ||||
|         buf = csr.public_bytes(Encoding.PEM) | ||||
| @@ -134,10 +135,11 @@ def revoke(common_name): | ||||
|     """ | ||||
|     Revoke valid certificate | ||||
|     """ | ||||
|     path, buf, cert = get_signed(common_name) | ||||
|     signed_path, buf, cert = get_signed(common_name) | ||||
|     revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial) | ||||
|     signed_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name) | ||||
|     os.rename(signed_path, revoked_path) | ||||
|     os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial)) | ||||
|  | ||||
|     push.publish("certificate-revoked", common_name) | ||||
|  | ||||
|     # Publish CRL for long polls | ||||
| @@ -362,6 +364,11 @@ def _sign(csr, buf, overwrite=False): | ||||
|     attachments.append((cert_buf, "application/x-pem-file", common_name.value + ".crt")) | ||||
|     cert_serial_hex = "%x" % cert.serial | ||||
|  | ||||
|     # Create symlink | ||||
|     os.symlink( | ||||
|         "../%s.pem" % common_name.value, | ||||
|         os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial)) | ||||
|  | ||||
|     # Copy filesystem attributes to newly signed certificate | ||||
|     if revoked_path: | ||||
|         for key in listxattr(revoked_path): | ||||
|   | ||||
| @@ -736,9 +736,8 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | ||||
|     pip("gssapi falcon cryptography humanize ipaddress simplepam humanize requests pyopenssl") | ||||
|     click.echo("Software dependencies installed") | ||||
|  | ||||
|     if not os.path.exists("/etc/apt/sources.list.d/nginx-stable-trusty.list"): | ||||
|         os.system("add-apt-repository -y ppa:nginx/stable") | ||||
|         os.system("apt-get update") | ||||
|     os.system("add-apt-repository -y ppa:nginx/stable") | ||||
|     os.system("apt-get update") | ||||
|     if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): | ||||
|         os.system("apt-get install -y libnginx-mod-nchan") | ||||
|     if not os.path.exists("/usr/sbin/nginx"): | ||||
| @@ -773,6 +772,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | ||||
|     # Expand variables | ||||
|     ca_key = os.path.join(directory, "ca_key.pem") | ||||
|     ca_crt = os.path.join(directory, "ca_crt.pem") | ||||
|     sqlite_path = os.path.join(directory, "meta", "db.sqlite") | ||||
|  | ||||
|     try: | ||||
|         pwd.getpwnam("certidude") | ||||
| @@ -858,11 +858,29 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | ||||
|             fh.write(env.get_template("server/server.conf").render(vars())) | ||||
|         click.echo("Generated %s" % const.CONFIG_PATH) | ||||
|  | ||||
|     if os.path.lexists(directory): | ||||
|         click.echo("CA directory %s already exists, remove to regenerate" % directory) | ||||
|     else: | ||||
|         click.echo("CA configuration files are saved to: {}".format(directory)) | ||||
|     # Create directories with 770 permissions | ||||
|     os.umask(0o027) | ||||
|     if not os.path.exists(directory): | ||||
|         os.makedirs(directory) | ||||
|  | ||||
|     # Create subdirectories with 770 permissions | ||||
|     os.umask(0o007) | ||||
|     for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta"): | ||||
|         path = os.path.join(directory, subdir) | ||||
|         if not os.path.exists(path): | ||||
|             click.echo("Creating directory %s" % path) | ||||
|             os.mkdir(path) | ||||
|         else: | ||||
|             click.echo("Directory already exists %s" % path) | ||||
|  | ||||
|     # Create SQLite database file with correct permissions | ||||
|     if not os.path.exists(sqlite_path): | ||||
|         os.umask(0o117) | ||||
|         with open(sqlite_path, "wb") as fh: | ||||
|             pass | ||||
|  | ||||
|     # Generate and sign CA key | ||||
|     if not os.path.exists(ca_key): | ||||
|         click.echo("Generating %d-bit RSA key..." % const.KEY_SIZE) | ||||
|  | ||||
|         key = rsa.generate_private_key( | ||||
| @@ -920,22 +938,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | ||||
|  | ||||
|         click.echo("Signing %s..." % cert.subject) | ||||
|  | ||||
|         # Create directories with 770 permissions | ||||
|         os.umask(0o027) | ||||
|         if not os.path.exists(directory): | ||||
|             os.makedirs(directory) | ||||
|  | ||||
|         # Create subdirectories with 770 permissions | ||||
|         os.umask(0o007) | ||||
|         for subdir in ("signed", "requests", "revoked", "expired", "meta"): | ||||
|             if not os.path.exists(os.path.join(directory, subdir)): | ||||
|                 os.mkdir(os.path.join(directory, subdir)) | ||||
|  | ||||
|         # Create SQLite database file with correct permissions | ||||
|         os.umask(0o117) | ||||
|         with open(os.path.join(directory, "meta", "db.sqlite"), "wb") as fh: | ||||
|             pass | ||||
|  | ||||
|         # Set permission bits to 640 | ||||
|         os.umask(0o137) | ||||
|         with open(ca_crt, "wb") as fh: | ||||
| @@ -1106,6 +1108,13 @@ def certidude_serve(port, listen, fork, exit_handler): | ||||
|  | ||||
|     from certidude import config | ||||
|  | ||||
|     # Rebuild reverse mapping | ||||
|     for cn, path, buf, cert, server in authority.list_signed(): | ||||
|         by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial) | ||||
|         if not os.path.exists(by_serial): | ||||
|             click.echo("Linking %s to ../%s.pem" % (by_serial, cn)) | ||||
|             os.symlink("../%s.pem" % cn, by_serial) | ||||
|  | ||||
|     # Process directories | ||||
|     if not os.path.exists(const.RUN_DIR): | ||||
|         click.echo("Creating: %s" % const.RUN_DIR) | ||||
|   | ||||
| @@ -34,12 +34,15 @@ 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]) | ||||
| OCSP_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||
|     cp.get("authorization", "ocsp subnets").split(" ") if j]) | ||||
|  | ||||
| AUTHORITY_DIR = "/var/lib/certidude" | ||||
| AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") | ||||
| AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") | ||||
| REQUESTS_DIR = cp.get("authority", "requests dir") | ||||
| SIGNED_DIR = cp.get("authority", "signed dir") | ||||
| SIGNED_BY_SERIAL_DIR = os.path.join(SIGNED_DIR, "by-serial") | ||||
| REVOKED_DIR = cp.get("authority", "revoked dir") | ||||
| EXPIRED_DIR = cp.get("authority", "expired dir") | ||||
|  | ||||
|   | ||||
| @@ -59,6 +59,9 @@ 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 = | ||||
|  | ||||
| # Online Certificate Status Protocol enabled subnets | ||||
| ocsp subnets = 0.0.0.0/0 | ||||
|  | ||||
| [logging] | ||||
| # Disable logging | ||||
| ;backend = | ||||
|   | ||||
		Reference in New Issue
	
	Block a user