mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	api: Preliminary OCSP support
This commit is contained in:
		| @@ -69,6 +69,7 @@ Common: | |||||||
|  |  | ||||||
| * Standard request, sign, revoke workflow via web interface. | * Standard request, sign, revoke workflow via web interface. | ||||||
| * Kerberos and basic auth based web interface authentication. | * 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. | * PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind. | ||||||
| * POSIX groups and Active Directory (LDAP) group membership based authorization. | * POSIX groups and Active Directory (LDAP) group membership based authorization. | ||||||
| * Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``. | * Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``. | ||||||
| @@ -94,8 +95,6 @@ HTTPS: | |||||||
| TODO | 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>`_. | * 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. | * Use `pki.js <https://pkijs.org/>`_ for generating keypair in the browser when claiming a token. | ||||||
| * Signer process logging. | * Signer process logging. | ||||||
|   | |||||||
| @@ -215,6 +215,10 @@ def certidude_app(log_handlers=[]): | |||||||
|         from .scep import SCEPResource |         from .scep import SCEPResource | ||||||
|         app.add_route("/api/scep/", 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 |     # Add sink for serving static files | ||||||
|     app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) |     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 |         # Attempt to save the request otherwise | ||||||
|         try: |         try: | ||||||
|             csr = authority.store_request(body) |             csr = authority.store_request(body.decode("ascii")) | ||||||
|         except errors.RequestExists: |         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 |             # 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()) |         return path, buf, x509.load_pem_x509_certificate(buf, default_backend()) | ||||||
|  |  | ||||||
| def get_revoked(serial): | 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: |     with open(path) as fh: | ||||||
|         buf = fh.read() |         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): | def get_attributes(cn): | ||||||
| @@ -83,7 +84,7 @@ def store_request(buf, overwrite=False): | |||||||
|         raise ValueError("No signing request supplied") |         raise ValueError("No signing request supplied") | ||||||
|  |  | ||||||
|     if isinstance(buf, unicode): |     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): |     elif isinstance(buf, str): | ||||||
|         csr = x509.load_der_x509_csr(buf, backend=default_backend()) |         csr = x509.load_der_x509_csr(buf, backend=default_backend()) | ||||||
|         buf = csr.public_bytes(Encoding.PEM) |         buf = csr.public_bytes(Encoding.PEM) | ||||||
| @@ -134,10 +135,11 @@ def revoke(common_name): | |||||||
|     """ |     """ | ||||||
|     Revoke valid certificate |     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) |     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.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) |     push.publish("certificate-revoked", common_name) | ||||||
|  |  | ||||||
|     # Publish CRL for long polls |     # 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")) |     attachments.append((cert_buf, "application/x-pem-file", common_name.value + ".crt")) | ||||||
|     cert_serial_hex = "%x" % cert.serial |     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 |     # Copy filesystem attributes to newly signed certificate | ||||||
|     if revoked_path: |     if revoked_path: | ||||||
|         for key in listxattr(revoked_path): |         for key in listxattr(revoked_path): | ||||||
|   | |||||||
| @@ -736,7 +736,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | |||||||
|     pip("gssapi falcon cryptography humanize ipaddress simplepam humanize requests pyopenssl") |     pip("gssapi falcon cryptography humanize ipaddress simplepam humanize requests pyopenssl") | ||||||
|     click.echo("Software dependencies installed") |     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("add-apt-repository -y ppa:nginx/stable") | ||||||
|     os.system("apt-get update") |     os.system("apt-get update") | ||||||
|     if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): |     if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): | ||||||
| @@ -773,6 +772,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | |||||||
|     # Expand variables |     # Expand variables | ||||||
|     ca_key = os.path.join(directory, "ca_key.pem") |     ca_key = os.path.join(directory, "ca_key.pem") | ||||||
|     ca_crt = os.path.join(directory, "ca_crt.pem") |     ca_crt = os.path.join(directory, "ca_crt.pem") | ||||||
|  |     sqlite_path = os.path.join(directory, "meta", "db.sqlite") | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         pwd.getpwnam("certidude") |         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())) |             fh.write(env.get_template("server/server.conf").render(vars())) | ||||||
|         click.echo("Generated %s" % const.CONFIG_PATH) |         click.echo("Generated %s" % const.CONFIG_PATH) | ||||||
|  |  | ||||||
|     if os.path.lexists(directory): |     # Create directories with 770 permissions | ||||||
|         click.echo("CA directory %s already exists, remove to regenerate" % directory) |     os.umask(0o027) | ||||||
|     else: |     if not os.path.exists(directory): | ||||||
|         click.echo("CA configuration files are saved to: {}".format(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) |         click.echo("Generating %d-bit RSA key..." % const.KEY_SIZE) | ||||||
|  |  | ||||||
|         key = rsa.generate_private_key( |         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) |         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 |         # Set permission bits to 640 | ||||||
|         os.umask(0o137) |         os.umask(0o137) | ||||||
|         with open(ca_crt, "wb") as fh: |         with open(ca_crt, "wb") as fh: | ||||||
| @@ -1106,6 +1108,13 @@ def certidude_serve(port, listen, fork, exit_handler): | |||||||
|  |  | ||||||
|     from certidude import config |     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 |     # Process directories | ||||||
|     if not os.path.exists(const.RUN_DIR): |     if not os.path.exists(const.RUN_DIR): | ||||||
|         click.echo("Creating: %s" % 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) |     cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS) | ||||||
| SCEP_SUBNETS = set([ipaddress.ip_network(j) for j in | SCEP_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
|     cp.get("authorization", "scep subnets").split(" ") if j]) |     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_DIR = "/var/lib/certidude" | ||||||
| AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") | AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") | ||||||
| AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") | AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") | ||||||
| REQUESTS_DIR = cp.get("authority", "requests dir") | REQUESTS_DIR = cp.get("authority", "requests dir") | ||||||
| SIGNED_DIR = cp.get("authority", "signed 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") | REVOKED_DIR = cp.get("authority", "revoked dir") | ||||||
| EXPIRED_DIR = cp.get("authority", "expired 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 | # Simple Certificate Enrollment Protocol enabled subnets | ||||||
| scep subnets = | scep subnets = | ||||||
|  |  | ||||||
|  | # Online Certificate Status Protocol enabled subnets | ||||||
|  | ocsp subnets = 0.0.0.0/0 | ||||||
|  |  | ||||||
| [logging] | [logging] | ||||||
| # Disable logging | # Disable logging | ||||||
| ;backend = | ;backend = | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user