synta 0.1.12

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
"""
Regression tests for OCSPRequest parsed type, CertID, OCSPCertIDSpec,
and OCSPRequestBuilder Python bindings (RFC 6960).
"""

import pytest
import synta

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


def _alg_id_with_null(oid: synta.ObjectIdentifier) -> bytes:
    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())


SHA1_ALG = _alg_id_with_null(synta.ObjectIdentifier("1.3.14.3.2.26"))
SHA256_WITH_RSA_ALG = _alg_id_with_null(synta.oids.SHA256_WITH_RSA)

# Fixed 20-byte values — issuer hash fields are opaque bytes in OCSP tests.
NAME_HASH = bytes(20)
KEY_HASH = bytes(20)
SERIAL = bytes([0x42])


# ── OCSPCertIDSpec ────────────────────────────────────────────────────────────


def test_cert_id_spec_fields():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    # OCSPCertIDSpec is frozen — verify it can be re-used without errors
    assert spec is not None


# ── OCSPRequestBuilder — unsigned path ───────────────────────────────────────


def test_build_unsigned_request():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    der = synta.OCSPRequestBuilder().add_request(spec).build_tbs()
    assert isinstance(der, bytes)
    assert len(der) > 0


def test_unsigned_request_round_trip():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    der = synta.OCSPRequestBuilder().add_request(spec).build_tbs()
    req = synta.OCSPRequest.from_der(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")
    assert req.requestor_name is None
    assert req.request_extensions is None


def test_multiple_requests_accumulated():
    serial2 = bytes([0x01, 0x99])  # two-byte positive serial (leading 0x00 not needed; MSB=0)
    spec1 = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    spec2 = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=serial2,
    )
    der = (
        synta.OCSPRequestBuilder()
        .add_request(spec1)
        .add_request(spec2)
        .build_tbs()
    )
    req = synta.OCSPRequest.from_der(der)
    assert len(req.request_list) == 2
    serials = {cid.serial_number for cid in req.request_list}
    assert int.from_bytes(SERIAL, "big") in serials
    assert int.from_bytes(serial2, "big") in serials


def test_build_tbs_double_call_raises():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    builder = synta.OCSPRequestBuilder().add_request(spec)
    builder.build_tbs()
    with pytest.raises(ValueError, match="already called"):
        builder.build_tbs()


def test_build_tbs_inner_double_call_raises():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    builder = synta.OCSPRequestBuilder().add_request(spec)
    builder.build_tbs_inner()
    with pytest.raises(ValueError, match="already called"):
        builder.build_tbs_inner()


def test_empty_request_list_raises():
    with pytest.raises(ValueError):
        synta.OCSPRequestBuilder().build_tbs()


def test_invalid_alg_id_raises():
    with pytest.raises(ValueError):
        synta.OCSPRequestBuilder().add_request(
            synta.OCSPCertIDSpec(
                hash_algorithm_der=b"\xff\x00",  # garbage
                issuer_name_hash=NAME_HASH,
                issuer_key_hash=KEY_HASH,
                serial=SERIAL,
            )
        ).build_tbs()


# ── OCSPRequestBuilder — signed path ─────────────────────────────────────────


def test_build_signed_request():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    tbs_inner = synta.OCSPRequestBuilder().add_request(spec).build_tbs_inner()
    assert isinstance(tbs_inner, bytes)
    assert len(tbs_inner) > 0

    sig = bytes(32)
    ocsp_der = synta.OCSPRequestBuilder.assemble(tbs_inner, SHA256_WITH_RSA_ALG, sig)
    req = synta.OCSPRequest.from_der(ocsp_der)
    assert len(req.request_list) == 1


def test_assemble_invalid_tbs_raises():
    with pytest.raises(ValueError):
        synta.OCSPRequestBuilder.assemble(b"\xff\x00", SHA256_WITH_RSA_ALG, bytes(32))


def test_assemble_invalid_sig_alg_raises():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    tbs_inner = synta.OCSPRequestBuilder().add_request(spec).build_tbs_inner()
    with pytest.raises(ValueError):
        synta.OCSPRequestBuilder.assemble(tbs_inner, b"\xff\x00", bytes(32))


# ── OCSPRequestBuilder — requestorName ───────────────────────────────────────


def _der_length(n: int) -> bytes:
    if n < 0x80:
        return bytes([n])
    if n < 0x100:
        return bytes([0x81, n])
    return bytes([0x82, n >> 8, n & 0xFF])


def test_requestor_name_present():
    rn_der = synta.NameBuilder().common_name("Requestor").build()
    # directoryName [4] EXPLICIT Name
    gn_der = bytes([0xa4]) + _der_length(len(rn_der)) + rn_der

    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    der = (
        synta.OCSPRequestBuilder()
        .requestor_name(gn_der)
        .add_request(spec)
        .build_tbs()
    )
    req = synta.OCSPRequest.from_der(der)
    assert req.requestor_name is not None
    assert isinstance(req.requestor_name, bytes)
    assert len(req.requestor_name) > 0


def test_requestor_name_invalid_der_raises():
    """Invalid GeneralName DER must be rejected at build time (deferred validation)."""
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    with pytest.raises(ValueError):
        synta.OCSPRequestBuilder().requestor_name(b"\xff\xff").add_request(spec).build_tbs()


# ── OCSPRequest parsed type ───────────────────────────────────────────────────


def test_ocsp_request_to_der_roundtrip():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    der = synta.OCSPRequestBuilder().add_request(spec).build_tbs()
    req = synta.OCSPRequest.from_der(der)
    assert req.to_der() == der


def test_ocsp_request_repr():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    der = synta.OCSPRequestBuilder().add_request(spec).build_tbs()
    req = synta.OCSPRequest.from_der(der)
    r = repr(req)
    assert "OCSPRequest" in r


# ── CertID properties ─────────────────────────────────────────────────────────


def test_cert_id_properties():
    spec = synta.OCSPCertIDSpec(
        hash_algorithm_der=SHA1_ALG,
        issuer_name_hash=NAME_HASH,
        issuer_key_hash=KEY_HASH,
        serial=SERIAL,
    )
    der = synta.OCSPRequestBuilder().add_request(spec).build_tbs()
    req = synta.OCSPRequest.from_der(der)
    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")
    # hash_algorithm_oid should be SHA-1 OID
    assert str(cert_id.hash_algorithm_oid) == "1.3.14.3.2.26"
    r = repr(cert_id)
    assert "CertID" in r