0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OCSP responderを作ってみる

Posted at

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を行ってみた。なんか上手くいってそうでなにより。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?