synta 0.1.12

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Example 8: OCSP response and request parsing/building.

Demonstrates: OCSPResponse.from_der, OCSPResponse.from_pem, OCSPResponse.to_pem,
and all OCSPResponse properties (status, response_type_oid, response_bytes, to_der);
OCSPResponseBuilder (responder_name, responder_key_hash, produced_at,
add_response, build_tbs, assemble); OCSPSingleResponse; NameBuilder;
OCSPRequest.from_der, OCSPRequest.from_pem, OCSPRequest.to_der, and all
OCSPRequest properties (request_list, requestor_name, request_extensions);
CertID properties (hash_algorithm_oid, issuer_name_hash, issuer_key_hash,
serial_number); OCSPRequestBuilder (add_request, requestor_name, build_tbs,
build_tbs_inner, assemble); OCSPCertIDSpec.
"""

import synta

# ── AlgorithmIdentifier helpers ───────────────────────────────────────────────


def _alg_id_with_null(oid: synta.ObjectIdentifier) -> bytes:
    """Build an AlgorithmIdentifier DER SEQUENCE { oid NULL } from an OID."""
    inner = synta.Encoder(synta.Encoding.DER)
    inner.encode_oid(oid)
    inner.encode_null()
    outer = synta.Encoder(synta.Encoding.DER)
    outer.encode_sequence(inner.finish())
    return bytes(outer.finish())


def _der_length(n: int) -> bytes:
    """Encode a DER length in the minimal number of octets."""
    if n < 0x80:
        return bytes([n])
    if n < 0x100:
        return bytes([0x81, n])
    return bytes([0x82, n >> 8, n & 0xFF])


# SHA-1 hash AlgorithmIdentifier (id-sha1, 1.3.14.3.2.26, OIW arc).
# synta.oids does not expose a standalone SHA1 constant (SHA-1 as a digest
# predates the NIST SHA-2 arc), so we construct it from the raw OID string.
_SHA1_ALG: bytes = _alg_id_with_null(synta.ObjectIdentifier("1.3.14.3.2.26"))

# sha256WithRSAEncryption AlgorithmIdentifier — uses the named synta.oids constant.
_SHA256_WITH_RSA_ALG: bytes = _alg_id_with_null(synta.oids.SHA256_WITH_RSA)

# Successful OCSP response (status=successful, contains BasicOCSPResponse)
_OCSP_SUCCESS_DER = bytes.fromhex(
    "308201c40a0100a08201bd308201b906092b0601050507300101048201aa3082"
    "01a630818fa216041451faf8a55a16074dc0703456a9a831e6f1f8d1b8180f32"
    "303236303331303136303033345a30643062303a300906052b0e03021a050004"
    "14bf7052c8b9c0f760c89123e099815eb2c0394226041451faf8a55a16074dc0"
    "703456a9a831e6f1f8d1b802012a8000180f32303236303130313030303030305a"
    "30110a0f32303236303130313030303030305a300d06092a864886f70d01010b"
    "050003820101004de5a70defb08fe67d2ae1c4ab1ce22db43707db4ca3b65537"
    "a99cf799acbda8194697f4aaf51ab13d7c36ff45abbff19ebd1071d329b30058"
    "025185837125cba733ae42413ab56899d408b934121b16d8325331392a343772"
    "083daa11da186476ea1fb4201c7bc1ac71d380357ff78071b18ecdbfdd6b1c0c"
    "0ab996dae5475d849d27b2c22780b4dc76371eaaaa487d11ed13d48bfd121c9f"
    "847f82ff70bf0143977d06f3f59e70cd8332976058e8dcf23bc1cd2b520ddc36"
    "5a61cc81d6e2e881719056d4db925eb4d86ab4f116f6fc376365c85912dea5b3"
    "4983cea9efe51cfbcbbabef08263aa39bc5f1742e9ecf0986b09025b56ed15a2"
    "a2f8a012fbaad5"
)

# Non-successful OCSP response: tryLater (status=3), no responseBytes
_OCSP_TRYLATER_DER = bytes.fromhex("30030a0103")


def section(title):
    print(f"\n{'' * 60}\n{title}\n{'' * 60}")


def demo_successful_response():
    section("Successful OCSP response")
    resp = synta.OCSPResponse.from_der(_OCSP_SUCCESS_DER)

    print(f"  status:              {resp.status}")
    print(f"  response_type_oid:   {resp.response_type_oid}")
    print(f"  response_bytes:      <{len(resp.response_bytes)} bytes>")
    print(f"  to_der():            <{len(resp.to_der())} bytes>")

    assert resp.status == "successful"
    assert resp.response_type_oid is not None
    # id-pkix-ocsp-basic = 1.3.6.1.5.5.7.48.1.1
    assert str(resp.response_type_oid) == "1.3.6.1.5.5.7.48.1.1"
    assert resp.response_bytes is not None


def demo_trylater_response():
    section("Non-successful OCSP response — tryLater")
    resp = synta.OCSPResponse.from_der(_OCSP_TRYLATER_DER)

    print(f"  status:            {resp.status}")
    print(f"  response_type_oid: {resp.response_type_oid!r}  (None for error responses)")
    print(f"  response_bytes:    {resp.response_bytes!r}  (None for error responses)")

    assert resp.status == "tryLater"
    assert resp.response_type_oid is None
    assert resp.response_bytes is None


def demo_all_status_values():
    section("All OCSP response status strings")
    status_values = [
        (0, "successful"),
        (1, "malformedRequest"),
        (2, "internalError"),
        (3, "tryLater"),
        (5, "sigRequired"),
        (6, "unauthorized"),
    ]
    for code, name in status_values:
        # Build minimal OCSPResponse DER: SEQUENCE { ENUMERATED(code) }
        der = bytes([0x30, 0x03, 0x0a, 0x01, code])
        resp = synta.OCSPResponse.from_der(der)
        print(f"  ENUMERATED({code}) → status={resp.status!r}")
        assert resp.status == name


def demo_to_der_roundtrip():
    section("to_der() round-trip")
    resp = synta.OCSPResponse.from_der(_OCSP_SUCCESS_DER)
    der2 = resp.to_der()
    assert der2 == _OCSP_SUCCESS_DER
    print("  to_der() round-trip: OK")


def demo_pem_roundtrip():
    section("from_pem / to_pem round-trip")
    resp = synta.OCSPResponse.from_der(_OCSP_SUCCESS_DER)
    pem = synta.OCSPResponse.to_pem(resp)
    assert pem.startswith(b"-----BEGIN OCSP RESPONSE-----")
    print(f"  to_pem(): {len(pem)} bytes")

    resp2 = synta.OCSPResponse.from_pem(pem)
    assert resp2.status == resp.status
    assert resp2.response_bytes == resp.response_bytes
    print("  from_pem() round-trip: OK")


def demo_ocsp_builder():
    section("OCSPResponseBuilder — build a complete OCSPResponse")

    # Build an issuer name and compute SHA-1 of its DER for the name hash
    name_der = synta.NameBuilder().common_name("Test CA").build()
    name_hash = synta.digest("sha1", name_der)

    # Use a fixed 20-byte key hash for demonstration
    key_hash = bytes(20)
    serial = bytes([0x01])

    # Construct a single response entry (certificate status = good).
    # OCSPSingleResponse is available via synta._synta (the native extension module).
    single = synta.OCSPSingleResponse(
        hash_algorithm_der=_SHA1_ALG,
        issuer_name_hash=name_hash,
        issuer_key_hash=key_hash,
        serial=serial,
        status=0,                       # 0 = good
        this_update="20260101000000Z",
        next_update="20260701000000Z",
    )

    # Build the ResponseData TBS SEQUENCE
    tbs = (
        synta.OCSPResponseBuilder()
        .responder_key_hash(key_hash)
        .produced_at("20260101000000Z")
        .add_response(single)
        .build_tbs()
    )
    print(f"  ResponseData DER: <{len(tbs)} bytes>")

    # Assemble the full OCSPResponse with a dummy 256-byte signature
    sig = bytes(256)
    ocsp_der = synta.OCSPResponseBuilder.assemble(tbs, _SHA256_WITH_RSA_ALG, sig)
    print(f"  OCSPResponse DER: <{len(ocsp_der)} bytes>")

    # Parse and verify
    resp = synta.OCSPResponse.from_der(ocsp_der)
    assert resp.status == "successful", f"expected 'successful', got {resp.status!r}"
    print(f"  status:            {resp.status}")
    print(f"  response_type_oid: {resp.response_type_oid}")
    assert resp.response_type_oid is not None
    # id-pkix-ocsp-basic = 1.3.6.1.5.5.7.48.1.1
    assert str(resp.response_type_oid) == "1.3.6.1.5.5.7.48.1.1"
    print("  OCSPResponseBuilder: OK")

    # Also demonstrate responder_name variant (rebuild the single response object
    # since the first add_response consumed it inside the builder)
    single2 = synta.OCSPSingleResponse(
        hash_algorithm_der=_SHA1_ALG,
        issuer_name_hash=name_hash,
        issuer_key_hash=key_hash,
        serial=serial,
        status=0,
        this_update="20260101000000Z",
        next_update="20260701000000Z",
    )
    tbs_name = (
        synta.OCSPResponseBuilder()
        .responder_name(name_der)
        .produced_at("20260101000000Z")
        .add_response(single2)
        .build_tbs()
    )
    ocsp_name_der = synta.OCSPResponseBuilder.assemble(tbs_name, _SHA256_WITH_RSA_ALG, sig)
    resp2 = synta.OCSPResponse.from_der(ocsp_name_der)
    assert resp2.status == "successful"
    print(f"  responder_name variant: status={resp2.status!r}  OK")


def demo_ocsp_request():
    section("OCSPRequestBuilder — build an unsigned OCSPRequest")

    name_der = synta.NameBuilder().common_name("Test CA").build()
    name_hash = synta.digest("sha1", name_der)
    key_hash = bytes(20)
    serial = bytes([0x42])

    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=_SHA1_ALG,
        issuer_name_hash=name_hash,
        issuer_key_hash=key_hash,
        serial=serial,
    )
    ocsp_der = synta.OCSPRequestBuilder().add_request(spec).build_tbs()
    print(f"  OCSPRequest DER: <{len(ocsp_der)} bytes>")

    req = synta.OCSPRequest.from_der(ocsp_der)
    assert len(req.request_list) == 1
    cert_id = req.request_list[0]
    assert cert_id.issuer_name_hash == name_hash
    assert cert_id.issuer_key_hash == key_hash
    assert cert_id.serial_number == int.from_bytes(serial, "big")
    print(f"  CertID: {cert_id!r}  OK")
    print(f"  requestor_name:      {req.requestor_name!r}  (None for unsigned)")
    print(f"  request_extensions:  {req.request_extensions!r}")


def demo_signed_ocsp_request():
    section("OCSPRequestBuilder — signed request via build_tbs_inner + assemble")

    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=_SHA1_ALG,
        issuer_name_hash=bytes(20),
        issuer_key_hash=bytes(20),
        serial=bytes([0x01]),
    )
    tbs_inner = synta.OCSPRequestBuilder().add_request(spec).build_tbs_inner()
    print(f"  TBSRequest DER: <{len(tbs_inner)} bytes>")

    # Assemble with a dummy signature
    sig = bytes(32)
    ocsp_der = synta.OCSPRequestBuilder.assemble(tbs_inner, _SHA256_WITH_RSA_ALG, sig)
    print(f"  Signed OCSPRequest DER: <{len(ocsp_der)} bytes>")

    req = synta.OCSPRequest.from_der(ocsp_der)
    assert len(req.request_list) == 1
    print("  signed request round-trip: OK")


def demo_requestor_name_request():
    section("OCSPRequestBuilder — with requestorName")

    # Build a Name DER and wrap it as a GeneralName [4] directoryName.
    name_der = synta.NameBuilder().common_name("Requestor").build()
    # directoryName [4] EXPLICIT Name: tag=0xa4, DER-encoded length, then Name DER
    gn_der = bytes([0xa4]) + _der_length(len(name_der)) + name_der

    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=_SHA1_ALG,
        issuer_name_hash=bytes(20),
        issuer_key_hash=bytes(20),
        serial=bytes([0x01]),
    )
    ocsp_der = (
        synta.OCSPRequestBuilder()
        .requestor_name(gn_der)
        .add_request(spec)
        .build_tbs()
    )
    req = synta.OCSPRequest.from_der(ocsp_der)
    assert req.requestor_name is not None
    print(f"  requestorName present: <{len(req.requestor_name)} bytes>  OK")


def main():
    print("=" * 60)
    print("Example 8: OCSP response and request parsing/building")
    print("=" * 60)
    demo_successful_response()
    demo_trylater_response()
    demo_all_status_values()
    demo_to_der_roundtrip()
    demo_pem_roundtrip()
    demo_ocsp_builder()
    demo_ocsp_request()
    demo_signed_ocsp_request()
    demo_requestor_name_request()
    print("\nAll OCSP examples completed.")


if __name__ == "__main__":
    main()