OCSPとは
ocsp(Online Certificate Status Protocol)とは、証明書の失効状態を確認するためのプロトコルです。証明書の秘密鍵が漏れたなど、管理者によって証明書が無効化されたときにリアルタイムで情報を取得するとこができます。
また、CRL(certificate revocation list)も同様に無効化された証明書情報を取得できますが、こちらはサービスによって定期的な更新が行われてそれをローカルで確認する方法なため、時間的に正確な無効情報は取得できません。
実際にpythonでやってみる
ocsp.py
from datetime import datetime, timedelta, timezone
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from cryptography.x509 import Certificate
from cryptography.x509.ocsp import (
load_der_ocsp_request,
OCSPResponseBuilder,
OCSPCertStatus,
OCSPResponderEncoding,
)
from cryptography.x509.extensions import Extension, OCSPNonce, ExtensionNotFound
from fastapi import FastAPI, APIRouter, Request, Response
import uvicorn
def load_cert(path: str) -> Certificate:
with open(path, "rb") as f:
cert_data = f.read()
return x509.load_pem_x509_certificate(cert_data, default_backend())
def load_private_key(path: str):
with open(path, "rb") as f:
return serialization.load_pem_private_key(
f.read(),
password=None, # パスフレーズがある場合は b"yourpassword"
backend=default_backend()
)
cert = load_cert('./user.cert.pem')
issuer_cert = load_cert('./ca.cert.pem')
responder_cert = load_cert('./responder.cert.pem')
responder_key = load_private_key('./responder.key.pem')
# issuer_name_hash
name_hasher = hashes.Hash(hashes.SHA1())
name_hasher.update(issuer_cert.subject.public_bytes())
issuer_name_hash = name_hasher.finalize()
# issuer_key_hash
key_bytes = issuer_cert.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.PKCS1
)
key_hasher = hashes.Hash(hashes.SHA1())
key_hasher.update(key_bytes)
issuer_key_hash = key_hasher.finalize()
app = FastAPI()
router = APIRouter()
@router.post("/")
async def ocsp(request: Request) -> None:
if request.headers.get("content-Type") != "application/ocsp-request":
return Response("Invalid content-type", status=400) # BuildUnSuccessとかでいいかも?
body = await request.body()
try:
ocsp_req = load_der_ocsp_request(body)
nonce_ext: Extension[OCSPNonce] | None = None
try:
nonce_ext = ocsp_req.extensions.get_extension_for_class(OCSPNonce)
except ExtensionNotFound:
nonce_ext = None
print(nonce_ext)
builder = OCSPResponseBuilder()
builder = builder.add_response_by_hash(
issuer_name_hash = issuer_name_hash,
issuer_key_hash = issuer_key_hash,
serial_number = ocsp_req.serial_number,
algorithm = hashes.SHA1(),
# 本来はDBなので証明書が無効化されているか確認して
# OCSPCertStatus.GOOD | REVOKEDのどちらかを返す
cert_status = OCSPCertStatus.REVOKED,
this_update = datetime.now(timezone.utc),
next_update = datetime.now(timezone.utc) + timedelta(days=1),
revocation_time = datetime.now(timezone.utc),
revocation_reason = None)
# certファイルが直接読むことができるならこっちでも同じ結果を得られる
# builder = builder.add_response(
# cert = cert,
# issuer = issuer_cert,
# algorithm = hashes.SHA1(),
# cert_status = OCSPCertStatus.GOOD,
# this_update = datetime.now(timezone.utc),
# next_update = datetime.now(timezone.utc) + timedelta(days=1),
# revocation_time = None,
# revocation_reason = None)
builder = builder.responder_id(OCSPResponderEncoding.HASH, responder_cert)
if nonce_ext is not None:
builder = builder.add_extension(nonce_ext.value, critical=False)
ocsp_response = builder.sign(private_key=responder_key, algorithm=hashes.SHA256())
return Response(content=ocsp_response.public_bytes(serialization.Encoding.DER),
media_type="application/ocsp-response")
except Exception as ex:
print(ex)
app.include_router(router)
uvicorn.run(app)
opensslで試す
openssl ocsp -issuer ca.cert.pem -cert user.cert.pem -VAfile responder.cert.pem --url http://localhost:8000/ -text
OCSP Request Data:
Version: 1 (0x0)
Requestor List:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 202724D59DEAB651FE531C97A6DD8730E6C00A8F
Issuer Key Hash: EF768B52BFCDE9CDD851705F5D1E243DE1D16E87
Serial Number: 06AB22242B2E345AE727FE573302B8BD118A2507
Request Extensions:
OCSP Nonce:
0410E29B9086E369FE4062987AE2539DBFA2
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response
Version: 1 (0x0)
Responder Id: 6125796276549BE25F73B6332E595F9C3681569F
Produced At: Oct 19 01:41:00 2025 GMT
Responses:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 202724D59DEAB651FE531C97A6DD8730E6C00A8F
Issuer Key Hash: EF768B52BFCDE9CDD851705F5D1E243DE1D16E87
Serial Number: 06AB22242B2E345AE727FE573302B8BD118A2507
Cert Status: revoked
Revocation Time: Oct 19 01:41:00 2025 GMT
This Update: Oct 19 01:41:00 2025 GMT
Next Update: Oct 20 01:41:00 2025 GMT
Response Extensions:
OCSP Nonce:
0410E29B9086E369FE4062987AE2539DBFA2
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
78:68:3a:8b:5e:66:c7:e0:ca:ab:d2:4c:9d:9f:dc:eb:fc:02:
1c:95:7e:4e:d0:e4:70:0f:97:27:2c:00:f4:56:24:e7:28:9c:
0d:53:5e:bb:6a:6c:b1:c7:a6:dc:7d:9a:0b:6c:7b:c0:59:39:
7f:ad:d1:4d:75:9f:6e:e7:14:6f:2b:46:1f:32:bb:a0:cc:4d:
bc:aa:fd:c4:e9:0f:1b:01:3c:3a:31:f7:92:12:78:74:91:59:
a2:fe:49:c7:0b:f2:be:d6:6e:77:b7:ba:d8:05:74:9a:ea:6b:
7e:e9:5e:2a:ab:94:92:c1:4b:2a:04:6a:41:51:13:66:d5:85:
6d:49:d9:ad:9b:e1:a2:a4:27:a7:7c:cc:91:39:db:9d:ac:7a:
24:4e:27:ef:05:d2:75:9b:90:88:33:7c:40:32:34:d8:f6:d5:
e5:d5:b7:76:6b:45:98:5c:66:0d:68:ee:c4:24:eb:ac:2a:dc:
1f:c4:0e:7f:e5:89:93:23:4f:47:7e:a4:61:1f:16:e7:71:12:
4f:57:21:36:90:75:36:47:f0:d8:50:d7:46:10:40:77:a1:5a:
09:75:b4:e5:26:3e:03:d5:2d:74:fd:fe:c3:65:f4:46:cd:df:
b9:41:10:51:52:30:dc:6f:20:3d:3e:42:05:aa:66:73:05:a7:
d9:ef:12:9c
Response verify OK
user.cert.pem: revoked
This Update: Oct 19 01:41:00 2025 GMT
Next Update: Oct 20 01:41:00 2025 GMT
Revocation Time: Oct 19 01:41:00 2025 GMT
まとめ
pythonで簡易なocsp responderを行ってみた。なんか上手くいってそうでなにより。