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 cms.SignedDataBuilder."""

import datetime
import traceback

import pytest

import synta
import synta.cms as cms
import synta.ext as ext
import synta.oids as oids


def _make_ca():
    key = synta.PrivateKey.generate_ec("P-256")
    name = synta.NameBuilder().common_name("CMS 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))
        .sign(key, "sha256")
    )
    return key, cert


def _skip_if_unavailable(exc):
    if isinstance(exc, (ValueError, NotImplementedError, AttributeError)):
        pytest.skip(f"SignedDataBuilder not available: {exc}")
    raise exc


# ── Basic build ───────────────────────────────────────────────────────────────

def test_signed_data_builder_basic():
    key, cert = _make_ca()
    try:
        ci_der = (
            cms.SignedDataBuilder(b"Hello, world!")
            .add_signer(key, cert.to_der())
            .build()
        )
    except Exception as e:
        _skip_if_unavailable(e)
    assert isinstance(ci_der, bytes)
    assert ci_der[0] == 0x30


def test_signed_data_builder_is_content_info():
    key, cert = _make_ca()
    try:
        ci_der = (
            cms.SignedDataBuilder(b"Test content")
            .add_signer(key, cert.to_der(), hash_algorithm="sha256")
            .build()
        )
    except Exception as e:
        _skip_if_unavailable(e)
    ci = cms.ContentInfo.from_der(ci_der)
    assert ci.content_type_oid == cms.ID_SIGNED_DATA


def test_signed_data_builder_with_embedded_cert():
    key, cert = _make_ca()
    cert_der = cert.to_der()
    try:
        ci_der = (
            cms.SignedDataBuilder(b"data with cert")
            .add_signer(key, cert_der)
            .add_cert(cert_der)
            .build()
        )
    except Exception as e:
        _skip_if_unavailable(e)
    ci = cms.ContentInfo.from_der(ci_der)
    assert ci.content_type_oid == cms.ID_SIGNED_DATA


def test_signed_data_builder_sha384():
    key, cert = _make_ca()
    try:
        ci_der = (
            cms.SignedDataBuilder(b"SHA-384 content")
            .add_signer(key, cert.to_der(), hash_algorithm="sha384")
            .build()
        )
    except Exception as e:
        _skip_if_unavailable(e)
    assert isinstance(ci_der, bytes)


# ── Detached signature ────────────────────────────────────────────────────────

def _strip_explicit0(data: bytes) -> bytes:
    """Strip [0] EXPLICIT outer TLV and return the inner bytes."""
    inner = synta.Decoder(data, synta.Encoding.DER).decode_explicit_tag(0)
    return inner.remaining_bytes()


def test_signed_data_builder_detached():
    key, cert = _make_ca()
    try:
        ci_der = (
            cms.SignedDataBuilder(b"Detached content", detached=True)
            .add_signer(key, cert.to_der())
            .build()
        )
    except Exception as e:
        _skip_if_unavailable(e)
    assert isinstance(ci_der, bytes)
    ci = cms.ContentInfo.from_der(ci_der)
    assert ci.content_type_oid == cms.ID_SIGNED_DATA
    sd = cms.SignedData.from_der(_strip_explicit0(ci.content))
    assert sd.encap_content is None


# ── Error cases ───────────────────────────────────────────────────────────────

def test_signed_data_builder_no_signers_raises():
    with pytest.raises((ValueError, AttributeError)):
        cms.SignedDataBuilder(b"data").build()


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

def main():
    tests = [
        test_signed_data_builder_basic,
        test_signed_data_builder_is_content_info,
        test_signed_data_builder_with_embedded_cert,
        test_signed_data_builder_sha384,
        test_signed_data_builder_detached,
        test_signed_data_builder_no_signers_raises,
    ]
    passed = failed = skipped = 0
    for t in tests:
        try:
            t()
            print(f"  ok  {t.__name__}")
            passed += 1
        except pytest.skip.Exception as e:
            print(f"  skip {t.__name__}: {e}")
            skipped += 1
        except Exception as e:
            print(f"  FAIL {t.__name__}: {e}")
            traceback.print_exc()
            failed += 1
    print(f"\n{passed} passed, {skipped} skipped, {failed} failed")
    if failed:
        raise SystemExit(1)


if __name__ == "__main__":
    main()