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