synta 0.1.4

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Tests for synta.krb5 — Kerberos V5 ASN.1 Python bindings.

Run after installing the synta package:
    python3 tests/python/test_krb5.py
"""

import synta
import synta.krb5 as krb5


# ─── constants ────────────────────────────────────────────────────────────────

def test_krb5_principal_name_oid():
    """KRB5_PRINCIPAL_NAME_OID is the id-pkinit-san OID 1.3.6.1.5.2.2."""
    oid = krb5.KRB5_PRINCIPAL_NAME_OID
    assert str(oid) == "1.3.6.1.5.2.2"
    assert list(oid.components()) == [1, 3, 6, 1, 5, 2, 2]
    print("  ✓ KRB5_PRINCIPAL_NAME_OID: OK")


def test_nt_constants():
    """NT_* name-type constants have the expected RFC 4120 values."""
    assert krb5.NT_UNKNOWN == 0
    assert krb5.NT_PRINCIPAL == 1
    assert krb5.NT_SRV_INST == 2
    assert krb5.NT_SRV_HST == 3
    assert krb5.NT_SRV_XHST == 4
    assert krb5.NT_UID == 5
    assert krb5.NT_X500_PRINCIPAL == 6
    assert krb5.NT_SMTP_NAME == 7
    assert krb5.NT_ENTERPRISE == 10
    assert krb5.NT_WELLKNOWN == 11
    assert krb5.NT_SRV_HST_DOMAIN == 12
    print("  ✓ NT_* constants: OK")


def test_etype_constants():
    """ETYPE_* encryption-type constants match IANA Kerberos etype registry."""
    # Deprecated legacy types
    assert krb5.ETYPE_DES_CBC_CRC == 1
    assert krb5.ETYPE_DES_CBC_MD4 == 2
    assert krb5.ETYPE_DES_CBC_MD5 == 3
    assert krb5.ETYPE_DES3_CBC_MD5 == 5
    assert krb5.ETYPE_DES3_CBC_SHA1 == 7
    assert krb5.ETYPE_DES_HMAC_SHA1 == 8
    assert krb5.ETYPE_DES3_CBC_SHA1_KD == 16
    # AES / SHA-1 (RFC 3962)
    assert krb5.ETYPE_AES128_CTS_HMAC_SHA1_96 == 17
    assert krb5.ETYPE_AES256_CTS_HMAC_SHA1_96 == 18
    # AES / SHA-2 (RFC 8009)
    assert krb5.ETYPE_AES128_CTS_HMAC_SHA256_128 == 19
    assert krb5.ETYPE_AES256_CTS_HMAC_SHA384_192 == 20
    # RC4 (deprecated, MS-KILE)
    assert krb5.ETYPE_RC4_HMAC == 23
    assert krb5.ETYPE_RC4_HMAC_EXP == 24
    # Camellia (RFC 6803)
    assert krb5.ETYPE_CAMELLIA128_CTS_CMAC == 25
    assert krb5.ETYPE_CAMELLIA256_CTS_CMAC == 26
    print("  ✓ ETYPE_* constants: OK")


# ─── Krb5PrincipalName construction ──────────────────────────────────────────

def test_constructor_user_principal():
    p = krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_PRINCIPAL, ["alice"])
    assert p.realm == "EXAMPLE.COM"
    assert p.name_type == krb5.NT_PRINCIPAL
    assert list(p.components) == ["alice"]
    print("  ✓ constructor (user principal): OK")


def test_constructor_service_inst():
    p = krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_SRV_INST, ["krbtgt", "EXAMPLE.COM"])
    assert p.realm == "EXAMPLE.COM"
    assert p.name_type == krb5.NT_SRV_INST
    assert list(p.components) == ["krbtgt", "EXAMPLE.COM"]
    print("  ✓ constructor (service/inst): OK")


def test_constructor_rejects_non_ascii_realm():
    try:
        krb5.Krb5PrincipalName("RÉALM", krb5.NT_PRINCIPAL, ["alice"])
        assert False, "expected ValueError"
    except ValueError:
        pass
    print("  ✓ constructor rejects non-ASCII realm: OK")


def test_constructor_rejects_non_ascii_component():
    try:
        krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_PRINCIPAL, ["alïce"])
        assert False, "expected ValueError"
    except ValueError:
        pass
    print("  ✓ constructor rejects non-ASCII component: OK")


# ─── DER encode / decode roundtrip ───────────────────────────────────────────

def test_roundtrip_user_principal():
    p = krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_PRINCIPAL, ["alice"])
    der = p.to_der()
    assert isinstance(der, bytes)
    q = krb5.Krb5PrincipalName.from_der(der)
    assert q.realm == p.realm
    assert q.name_type == p.name_type
    assert list(q.components) == list(p.components)
    print("  ✓ roundtrip (user principal): OK")


def test_roundtrip_tgt_service():
    p = krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_SRV_INST, ["krbtgt", "EXAMPLE.COM"])
    der = p.to_der()
    q = krb5.Krb5PrincipalName.from_der(der)
    assert q.realm == "EXAMPLE.COM"
    assert q.name_type == krb5.NT_SRV_INST
    assert list(q.components) == ["krbtgt", "EXAMPLE.COM"]
    print("  ✓ roundtrip (TGT service): OK")


def test_roundtrip_host_principal():
    p = krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_SRV_HST, ["host", "server.example.com"])
    der = p.to_der()
    q = krb5.Krb5PrincipalName.from_der(der)
    assert q.realm == "EXAMPLE.COM"
    assert q.name_type == krb5.NT_SRV_HST
    assert list(q.components) == ["host", "server.example.com"]
    print("  ✓ roundtrip (host principal): OK")


def test_roundtrip_enterprise():
    p = krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_ENTERPRISE, ["user@domain.example"])
    der = p.to_der()
    q = krb5.Krb5PrincipalName.from_der(der)
    assert list(q.components) == ["user@domain.example"]
    print("  ✓ roundtrip (enterprise): OK")


# ─── DER encoding: known-good vector ─────────────────────────────────────────
#
# DER for KRB5PrincipalName { realm="EXAMPLE.COM",
#   principalName={ name-type=2, name-string=["krbtgt","EXAMPLE.COM"] } }
#
# Structure (manually annotated with correct lengths):
#   30 31              SEQUENCE (49 bytes) {
#     a0 0d              [0] (13 bytes) {
#       1b 0b              GeneralString "EXAMPLE.COM" (11 bytes)
#         45 58 41 4d 50 4c 45 2e 43 4f 4d
#     }
#     a1 20              [1] (32 bytes) {
#       30 1e              SEQUENCE (30 bytes) {
#         a0 03              [0] (3 bytes) {
#           02 01 02           INTEGER 2
#         }
#         a1 17              [1] (23 bytes) {
#           30 15              SEQUENCE (21 bytes) {
#             1b 06            GeneralString "krbtgt" (6 bytes)
#               6b 72 62 74 67 74
#             1b 0b            GeneralString "EXAMPLE.COM" (11 bytes)
#               45 58 41 4d 50 4c 45 2e 43 4f 4d
#           }
#         }
#       }
#     }
#   }
KNOWN_GOOD_DER = bytes.fromhex(
    "3031"
    "a00d" "1b0b" "4558414d504c452e434f4d"
    "a120"
        "301e"
            "a003" "020102"
            "a117"
                "3015"
                    "1b06" "6b7262746774"
                    "1b0b" "4558414d504c452e434f4d"
)


def test_decode_known_good_vector():
    p = krb5.Krb5PrincipalName.from_der(KNOWN_GOOD_DER)
    assert p.realm == "EXAMPLE.COM"
    assert p.name_type == krb5.NT_SRV_INST
    assert list(p.components) == ["krbtgt", "EXAMPLE.COM"]
    print("  ✓ decode known-good DER vector: OK")


def test_encode_matches_known_good_vector():
    p = krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_SRV_INST, ["krbtgt", "EXAMPLE.COM"])
    der = p.to_der()
    assert der == KNOWN_GOOD_DER, f"expected {KNOWN_GOOD_DER.hex()}, got {der.hex()}"
    print("  ✓ encode matches known-good DER vector: OK")


def test_from_der_invalid_raises():
    try:
        krb5.Krb5PrincipalName.from_der(b"\x00\x00\x00")
        assert False, "expected ValueError"
    except (ValueError, Exception):
        pass
    print("  ✓ from_der(invalid) raises: OK")


# ─── repr / equality ─────────────────────────────────────────────────────────

def test_repr():
    p = krb5.Krb5PrincipalName("EXAMPLE.COM", krb5.NT_PRINCIPAL, ["alice"])
    r = repr(p)
    assert "Krb5PrincipalName" in r
    assert "EXAMPLE.COM" in r
    assert "alice" in r
    print("  ✓ __repr__: OK")


def test_equality():
    a = krb5.Krb5PrincipalName("REALM", krb5.NT_PRINCIPAL, ["bob"])
    b = krb5.Krb5PrincipalName("REALM", krb5.NT_PRINCIPAL, ["bob"])
    c = krb5.Krb5PrincipalName("REALM", krb5.NT_PRINCIPAL, ["carol"])
    assert a == b
    assert not (a == c)
    print("  ✓ __eq__: OK")


# ─── submodule accessibility ──────────────────────────────────────────────────

def test_import_via_synta_krb5():
    import synta.krb5
    assert hasattr(synta.krb5, "Krb5PrincipalName")
    assert hasattr(synta.krb5, "NT_PRINCIPAL")
    print("  ✓ import synta.krb5: OK")


def test_accessible_as_synta_attr():
    import synta
    assert hasattr(synta, "krb5")
    assert hasattr(synta.krb5, "NT_SRV_INST")
    print("  ✓ synta.krb5 attribute: OK")


# ─── runner ──────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    tests = [
        test_krb5_principal_name_oid,
        test_nt_constants,
        test_etype_constants,
        test_constructor_user_principal,
        test_constructor_service_inst,
        test_constructor_rejects_non_ascii_realm,
        test_constructor_rejects_non_ascii_component,
        test_roundtrip_user_principal,
        test_roundtrip_tgt_service,
        test_roundtrip_host_principal,
        test_roundtrip_enterprise,
        test_decode_known_good_vector,
        test_encode_matches_known_good_vector,
        test_from_der_invalid_raises,
        test_repr,
        test_equality,
        test_import_via_synta_krb5,
        test_accessible_as_synta_attr,
    ]
    print("Running synta.krb5 tests...")
    failed = 0
    for t in tests:
        try:
            t()
        except Exception as e:
            print(f"{t.__name__}: FAILED — {e}")
            failed += 1
    if failed:
        print(f"\n{failed}/{len(tests)} tests FAILED")
        raise SystemExit(1)
    print(f"\nAll {len(tests)} tests passed.")