synta 0.2.3

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Tests for find_extension_value, encode_general_names, key_usage_bit,
signing_algorithm_der, and decode_public_key_info.
"""

import datetime
import traceback

import pytest

import synta
import synta.ext as ext
import synta.general_name as gn
import synta.oids as oids


def _make_ca_cert():
    key = synta.PrivateKey.generate_ec("P-256")
    name = synta.NameBuilder().common_name("Helper Test CA").build()
    now = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
    later = datetime.datetime(2027, 1, 1, tzinfo=datetime.timezone.utc)
    cert = (
        synta.CertificateBuilder()
        .issuer_name(name).subject_name(name)
        .public_key(key.public_key).serial_number(1)
        .not_valid_before_utc(now).not_valid_after_utc(later)
        .add_extension(str(oids.BASIC_CONSTRAINTS), True, ext.basic_constraints(ca=True))
        .add_extension(str(oids.KEY_USAGE), True, ext.key_usage(ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN))
        .sign(key, "sha256")
    )
    return key, cert


# ── find_extension_value ──────────────────────────────────────────────────────

def test_find_extension_value_present():
    _, cert = _make_ca_cert()
    ext_seq = cert.extensions_der
    assert ext_seq is not None
    value = synta.find_extension_value(ext_seq, "2.5.29.19")
    assert value is not None
    assert isinstance(value, bytes)
    assert len(value) > 0


def test_find_extension_value_absent():
    _, cert = _make_ca_cert()
    ext_seq = cert.extensions_der
    assert ext_seq is not None
    value = synta.find_extension_value(ext_seq, "1.3.6.1.5.5.7.1.11")
    assert value is None


def test_find_extension_value_with_oid_object():
    _, cert = _make_ca_cert()
    ext_seq = cert.extensions_der
    assert ext_seq is not None
    oid_obj = synta.ObjectIdentifier("2.5.29.19")
    value = synta.find_extension_value(ext_seq, oid_obj)
    assert value is not None


def test_find_extension_value_invalid_oid():
    _, cert = _make_ca_cert()
    ext_seq = cert.extensions_der
    assert ext_seq is not None
    with pytest.raises((ValueError, TypeError)):
        synta.find_extension_value(ext_seq, "not.an.oid")


# ── encode_general_names ──────────────────────────────────────────────────────

def test_encode_general_names_dns():
    result = synta.encode_general_names([(gn.DNS_NAME, b"example.com")])
    assert result is not None
    assert isinstance(result, bytes)
    assert result[0] == 0x30


def test_encode_general_names_multi():
    entries = [
        (gn.DNS_NAME, b"example.com"),
        (gn.DNS_NAME, b"www.example.com"),
        (gn.IP_ADDRESS, bytes([192, 168, 1, 1])),
    ]
    result = synta.encode_general_names(entries)
    assert result is not None
    assert isinstance(result, bytes)


def test_encode_general_names_roundtrip():
    entries = [(gn.DNS_NAME, b"roundtrip.example.com")]
    encoded = synta.encode_general_names(entries)
    assert encoded is not None
    parsed = synta.parse_general_names(encoded)
    assert len(parsed) == 1
    tag, content = parsed[0]
    assert tag == gn.DNS_NAME
    assert content == b"roundtrip.example.com"


def test_encode_general_names_empty():
    result = synta.encode_general_names([])
    if result is not None:
        assert isinstance(result, bytes)


# ── key_usage_bit ─────────────────────────────────────────────────────────────

def test_key_usage_bit_keyCertSign_set():
    _, cert = _make_ca_cert()
    ku_der = cert.get_extension_value_der("2.5.29.15")
    assert ku_der is not None
    assert synta.key_usage_bit(ku_der, 5) is True   # keyCertSign
    assert synta.key_usage_bit(ku_der, 6) is True   # cRLSign
    assert synta.key_usage_bit(ku_der, 0) is False  # digitalSignature not set


def test_key_usage_bit_digitalSignature_set():
    key = synta.PrivateKey.generate_ec("P-256")
    name = synta.NameBuilder().common_name("Leaf").build()
    now = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
    later = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
    cert = (
        synta.CertificateBuilder()
        .issuer_name(name).subject_name(name)
        .public_key(key.public_key).serial_number(2)
        .not_valid_before_utc(now).not_valid_after_utc(later)
        .add_extension(str(oids.KEY_USAGE), True, ext.key_usage(ext.KU_DIGITAL_SIGNATURE))
        .sign(key, "sha256")
    )
    ku_der = cert.get_extension_value_der("2.5.29.15")
    assert ku_der is not None
    assert synta.key_usage_bit(ku_der, 0) is True   # digitalSignature
    assert synta.key_usage_bit(ku_der, 5) is False  # keyCertSign not set


# ── signing_algorithm_der ─────────────────────────────────────────────────────

def test_signing_algorithm_der_ec_sha256():
    result = synta.signing_algorithm_der("1.2.840.10045.2.1", "sha256")
    if result is not None:
        assert isinstance(result, bytes)
        assert result[0] == 0x30


def test_signing_algorithm_der_rsa_sha256():
    result = synta.signing_algorithm_der("1.2.840.113549.1.1.1", "sha256")
    if result is not None:
        assert isinstance(result, bytes)
        assert result[0] == 0x30


def test_signing_algorithm_der_unknown_key():
    result = synta.signing_algorithm_der("1.2.3.4.5.6", "sha256")
    assert result is None


def test_signing_algorithm_der_with_oid_object():
    oid_obj = synta.ObjectIdentifier("1.2.840.113549.1.1.1")
    result = synta.signing_algorithm_der(oid_obj, "sha256")
    if result is not None:
        assert isinstance(result, bytes)


# ── decode_public_key_info ────────────────────────────────────────────────────

def test_decode_public_key_info_ec():
    key = synta.PrivateKey.generate_ec("P-256")
    info = synta.decode_public_key_info(key.public_key.to_der())
    assert isinstance(info, dict)
    assert "algorithm_oid" in info
    assert "key_bytes" in info
    assert info["algorithm_oid"] == "1.2.840.10045.2.1"


def test_decode_public_key_info_ec_curve():
    key = synta.PrivateKey.generate_ec("P-256")
    info = synta.decode_public_key_info(key.public_key.to_der())
    if "curve_nist_name" in info:
        assert info["curve_nist_name"] == "P-256"


def test_decode_public_key_info_ed25519():
    key = synta.PrivateKey.generate_ed25519()
    info = synta.decode_public_key_info(key.public_key.to_der())
    assert isinstance(info, dict)
    assert info["algorithm_oid"] == "1.3.101.112"


def test_decode_public_key_info_invalid():
    with pytest.raises(ValueError):
        synta.decode_public_key_info(b"\x00" * 10)


# ── Manual runner ─────────────────────────────────────────────────────────────

def main():
    tests = [
        test_find_extension_value_present,
        test_find_extension_value_absent,
        test_find_extension_value_with_oid_object,
        test_encode_general_names_dns,
        test_encode_general_names_multi,
        test_encode_general_names_roundtrip,
        test_encode_general_names_empty,
        test_key_usage_bit_keyCertSign_set,
        test_key_usage_bit_digitalSignature_set,
        test_signing_algorithm_der_ec_sha256,
        test_signing_algorithm_der_rsa_sha256,
        test_signing_algorithm_der_unknown_key,
        test_signing_algorithm_der_with_oid_object,
        test_decode_public_key_info_ec,
        test_decode_public_key_info_ec_curve,
        test_decode_public_key_info_ed25519,
        test_decode_public_key_info_invalid,
    ]
    passed = failed = 0
    for t in tests:
        try:
            t()
            print(f"  ok  {t.__name__}")
            passed += 1
        except Exception as e:
            print(f"  FAIL {t.__name__}: {e}")
            traceback.print_exc()
            failed += 1
    print(f"\n{passed} passed, {failed} failed")
    if failed:
        raise SystemExit(1)


if __name__ == "__main__":
    main()