Initial commit
This commit is contained in:
commit
80c4964899
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)
|
Loading…
Reference in New Issue
Block a user