Initial commit
This commit is contained in:
		
							
								
								
									
										6
									
								
								.flake8
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.flake8
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | [flake8] | ||||||
|  | inline-quotes = " | ||||||
|  | multiline-quotes = """ | ||||||
|  | indent-size = 4 | ||||||
|  | max-line-length = 160 | ||||||
|  | ignore = Q003 E128 E704 E731 | ||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | *.save | ||||||
							
								
								
									
										9
									
								
								.gitlint
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.gitlint
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | [general] | ||||||
|  | ignore=body-is-missing,T3 | ||||||
|  | ignore-stdin=true | ||||||
|  |  | ||||||
|  | [title-match-regex] | ||||||
|  | regex=[A-Z] | ||||||
|  |  | ||||||
|  | [author-valid-email] | ||||||
|  | regex=[^@]+@pinecrypt.com | ||||||
							
								
								
									
										16
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | repos: | ||||||
|  | -   repo: https://github.com/PyCQA/flake8 | ||||||
|  |     rev: 3.9.2 | ||||||
|  |     hooks: | ||||||
|  |     -   id: flake8 | ||||||
|  |         additional_dependencies: [flake8-typing-imports==1.10.0,flake8-quotes==3.2.0] | ||||||
|  |  | ||||||
|  | -   repo: https://github.com/jorisroovers/gitlint | ||||||
|  |     rev: v0.15.1 | ||||||
|  |     hooks: | ||||||
|  |     -   id: gitlint | ||||||
|  |  | ||||||
|  | -   repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs | ||||||
|  |     rev: v1.1.1 | ||||||
|  |     hooks: | ||||||
|  |     -   id: dockerfile_lint | ||||||
							
								
								
									
										8
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | FROM python | ||||||
|  | LABEL name="pinecrypt/ocsp-responder" \ | ||||||
|  |       version="rc" \ | ||||||
|  |       maintainer="Pinecrypt Labs <info@pinecrypt.com>" | ||||||
|  | RUN pip install asn1crypto motor oscrypto pytz sanic sanic_prometheus | ||||||
|  | ADD ocsp_responder.py /ocsp_responder.py | ||||||
|  | CMD /ocsp_responder.py | ||||||
|  | EXPOSE 5001 | ||||||
							
								
								
									
										150
									
								
								ocsp_responder.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										150
									
								
								ocsp_responder.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,150 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import pytz | ||||||
|  | from asn1crypto.util import timezone | ||||||
|  | from asn1crypto import ocsp, pem | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from math import inf | ||||||
|  | from motor.motor_asyncio import AsyncIOMotorClient | ||||||
|  | from oscrypto import asymmetric | ||||||
|  | from prometheus_client import Counter, Histogram | ||||||
|  | from sanic import Sanic, response | ||||||
|  | from sanic_prometheus import monitor | ||||||
|  |  | ||||||
|  | ocsp_request_valid = Counter("pinecrypt_ocsp_request_valid", | ||||||
|  |     "Valid OCSP requests") | ||||||
|  | ocsp_request_list_size = Histogram("pinecrypt_ocsp_request_list_size", | ||||||
|  |     "Histogram of OCSP request list size", | ||||||
|  |     buckets=(1, 2, 3, inf)) | ||||||
|  | ocsp_request_size_bytes = Histogram("pinecrypt_ocsp_request_size_bytes", | ||||||
|  |     "Histogram of OCSP request size in bytes", | ||||||
|  |     buckets=(100, 200, 500, 1000, 2000, 5000, 10000, inf)) | ||||||
|  | ocsp_request_nonces = Histogram("pinecrypt_ocsp_request_nonces", | ||||||
|  |     "Histogram of nonce count per request", | ||||||
|  |     buckets=(1, 2, 3, inf)) | ||||||
|  | ocsp_response_status = Counter("pinecrypt_ocsp_response_status", | ||||||
|  |     "Status responses", ["status"]) | ||||||
|  |  | ||||||
|  | app = Sanic("events") | ||||||
|  | monitor(app).expose_endpoint() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Load CA certificate | ||||||
|  | with open("/server-secrets/ca_cert.pem", "rb") as fh: | ||||||
|  |     authority_cert = asymmetric.load_certificate(fh.read()) | ||||||
|  |  | ||||||
|  | # Load CA private key | ||||||
|  | with open("/authority-secrets/ca_key.pem", "rb") as fh: | ||||||
|  |     key_buf = fh.read() | ||||||
|  |     header, _, key_der_bytes = pem.unarmor(key_buf) | ||||||
|  |     private_key = asymmetric.load_private_key(key_der_bytes) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) | ||||||
|  | DEBUG = bool(os.getenv("DEBUG")) | ||||||
|  | MONGO_URI = os.getenv("MONGO_URI", "mongodb://127.0.0.1:27017/default?replicaSet=rs0") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.listener("before_server_start") | ||||||
|  | async def setup_db(app, loop): | ||||||
|  |     # TODO: find cleaner way to do this, for more see | ||||||
|  |     # https://github.com/sanic-org/sanic/issues/919 | ||||||
|  |     app.ctx.db = AsyncIOMotorClient(MONGO_URI).get_default_database() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route("/api/ocsp/", methods=["POST"]) | ||||||
|  | async def view_ocsp_responder(request): | ||||||
|  |     sign_algo = { | ||||||
|  |         "ec": "sha1_ecdsa", | ||||||
|  |         "rsa": "sha1_rsa" | ||||||
|  |     }[authority_cert.public_key.algorithm] | ||||||
|  |     sign_func = { | ||||||
|  |         "ec": asymmetric.ecdsa_sign, | ||||||
|  |         "rsa": asymmetric.rsa_pkcs1v15_sign | ||||||
|  |     }[authority_cert.public_key.algorithm] | ||||||
|  |  | ||||||
|  |     ocsp_request_size_bytes.observe(len(request.body)) | ||||||
|  |     ocsp_req = ocsp.OCSPRequest.load(request.body) | ||||||
|  |  | ||||||
|  |     now = datetime.now(timezone.utc).replace(microsecond=0) | ||||||
|  |     response_extensions = [] | ||||||
|  |  | ||||||
|  |     nonces = 0 | ||||||
|  |     for ext in ocsp_req["tbs_request"]["request_extensions"]: | ||||||
|  |         if ext["extn_id"].native == "nonce": | ||||||
|  |             nonces += 1 | ||||||
|  |             response_extensions.append( | ||||||
|  |                 ocsp.ResponseDataExtension({ | ||||||
|  |                     "extn_id": "nonce", | ||||||
|  |                     "critical": False, | ||||||
|  |                     "extn_value": ext["extn_value"] | ||||||
|  |                 }) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     ocsp_request_nonces.observe(nonces) | ||||||
|  |     ocsp_request_valid.inc() | ||||||
|  |  | ||||||
|  |     responses = [] | ||||||
|  |  | ||||||
|  |     ocsp_request_list_size.observe(len(ocsp_req["tbs_request"]["request_list"])) | ||||||
|  |     for item in ocsp_req["tbs_request"]["request_list"]: | ||||||
|  |         serial = item["req_cert"]["serial_number"].native | ||||||
|  |         assert serial > 0, "Serial number correctness check failed" | ||||||
|  |  | ||||||
|  |         doc = await app.ctx.db.certidude_certificates.find_one({"serial_number": "%x" % serial}) | ||||||
|  |         if doc: | ||||||
|  |             if doc["status"] == "signed": | ||||||
|  |                 status = ocsp.CertStatus(name="good", value=None) | ||||||
|  |                 ocsp_response_status.labels("good").inc() | ||||||
|  |             elif doc["status"] == "revoked": | ||||||
|  |                 status = ocsp.CertStatus( | ||||||
|  |                     name="revoked", | ||||||
|  |                     value={ | ||||||
|  |                         "revocation_time": doc["revoked"].replace(tzinfo=pytz.UTC), | ||||||
|  |                         "revocation_reason": doc["revocation_reason"], | ||||||
|  |                     }) | ||||||
|  |                 ocsp_response_status.labels("revoked").inc() | ||||||
|  |             else: | ||||||
|  |                 # This should not happen, if it does database is mangled | ||||||
|  |                 raise ValueError("Invalid/unknown certificate status '%s'" % doc["status"]) | ||||||
|  |         else: | ||||||
|  |             status = ocsp.CertStatus(name="unknown", value=None) | ||||||
|  |             ocsp_response_status.labels("unknown").inc() | ||||||
|  |  | ||||||
|  |         responses.append({ | ||||||
|  |             "cert_id": { | ||||||
|  |                 "hash_algorithm": { | ||||||
|  |                     "algorithm": "sha1" | ||||||
|  |                 }, | ||||||
|  |                 "issuer_name_hash": authority_cert.asn1.subject.sha1, | ||||||
|  |                 "issuer_key_hash": authority_cert.public_key.asn1.sha1, | ||||||
|  |                 "serial_number": serial, | ||||||
|  |             }, | ||||||
|  |             "cert_status": status, | ||||||
|  |             "this_update": now - CLOCK_SKEW_TOLERANCE, | ||||||
|  |             "next_update": now + timedelta(minutes=15) + CLOCK_SKEW_TOLERANCE, | ||||||
|  |             "single_extensions": [] | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |     response_data = ocsp.ResponseData({ | ||||||
|  |         "responder_id": ocsp.ResponderId(name="by_key", value=authority_cert.public_key.asn1.sha1), | ||||||
|  |         "produced_at": now, | ||||||
|  |         "responses": responses, | ||||||
|  |         "response_extensions": response_extensions | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     return response.raw(ocsp.OCSPResponse({ | ||||||
|  |         "response_status": "successful", | ||||||
|  |         "response_bytes": { | ||||||
|  |             "response_type": "basic_ocsp_response", | ||||||
|  |             "response": { | ||||||
|  |                 "tbs_response_data": response_data, | ||||||
|  |                 "certs": [authority_cert.asn1], | ||||||
|  |                 "signature_algorithm": {"algorithm": sign_algo}, | ||||||
|  |                 "signature": sign_func(private_key, response_data.dump(), "sha1") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }).dump(), headers={"Content-Type": "application/ocsp-response"}) | ||||||
|  |  | ||||||
|  | app.run(port=5001, debug=DEBUG) | ||||||
		Reference in New Issue
	
	Block a user