From 80c4964899c730f9b208c88e7910c275729ebd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Thu, 3 Jun 2021 17:58:14 +0000 Subject: [PATCH] Initial commit --- .flake8 | 6 ++ .gitignore | 1 + .gitlint | 9 +++ .pre-commit-config.yaml | 16 +++++ Dockerfile | 8 +++ ocsp_responder.py | 150 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 190 insertions(+) create mode 100644 .flake8 create mode 100644 .gitignore create mode 100644 .gitlint create mode 100644 .pre-commit-config.yaml create mode 100644 Dockerfile create mode 100755 ocsp_responder.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..ff6c948 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +inline-quotes = " +multiline-quotes = """ +indent-size = 4 +max-line-length = 160 +ignore = Q003 E128 E704 E731 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a44989 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.save diff --git a/.gitlint b/.gitlint new file mode 100644 index 0000000..e3f0c82 --- /dev/null +++ b/.gitlint @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..23d9eb4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b1b19f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python +LABEL name="pinecrypt/ocsp-responder" \ + version="rc" \ + maintainer="Pinecrypt Labs " +RUN pip install asn1crypto motor oscrypto pytz sanic sanic_prometheus +ADD ocsp_responder.py /ocsp_responder.py +CMD /ocsp_responder.py +EXPOSE 5001 diff --git a/ocsp_responder.py b/ocsp_responder.py new file mode 100755 index 0000000..a3a07f1 --- /dev/null +++ b/ocsp_responder.py @@ -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)