synta 0.2.6

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Example: CMS SignedData builder (RFC 5652).

Demonstrates:
  - cms.SignedDataBuilder: create a CMS ContentInfo wrapping a SignedData
  - Attached vs detached signatures
  - Verifying the result by parsing back with ContentInfo / SignedData

Requires the 'openssl' or 'nss' Cargo feature (key generation and signing).
"""

import datetime

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


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


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


def _make_ca():
    """Return (key, cert) for a minimal self-signed EC CA."""
    key = synta.PrivateKey.generate_ec("P-256")
    name = synta.NameBuilder().common_name("CMS Example 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 demo_attached_signature():
    section("Attached (encapsulating) SignedData")

    key, cert = _make_ca()
    content = b"The quick brown fox jumps over the lazy dog."

    try:
        ci_der = (
            cms.SignedDataBuilder(content)
            .add_signer(key, cert.to_der(), hash_algorithm="sha256")
            .add_cert(cert.to_der())
            .build()
        )
    except (NotImplementedError, AttributeError) as e:
        print(f"  Skipped: {e}")
        return

    print(f"  ContentInfo DER: {len(ci_der)} bytes")

    ci = cms.ContentInfo.from_der(ci_der)
    print(f"  content_type_oid: {ci.content_type_oid}")
    assert ci.content_type_oid == cms.ID_SIGNED_DATA

    sd = cms.SignedData.from_der(_strip_explicit0(ci.content))
    print(f"  SignedData version : {sd.version}")
    print(f"  encapContentType   : {sd.encap_content_type}")
    print(f"  encapContent len   : {len(sd.encap_content or b'')} bytes")
    print(f"  signer count       : {len(sd.signer_infos)}")
    for si in sd.signer_infos:
        print(f"    SignerInfo digest alg: {si.digest_algorithm_oid}")
        print(f"    SignerInfo sig alg   : {si.signature_algorithm_oid}")


def demo_detached_signature():
    section("Detached SignedData")

    key, cert = _make_ca()
    content = b"Content whose hash is signed but not embedded."

    try:
        ci_der = (
            cms.SignedDataBuilder(content, detached=True)
            .add_signer(key, cert.to_der(), hash_algorithm="sha256")
            .build()
        )
    except (NotImplementedError, AttributeError) as e:
        print(f"  Skipped: {e}")
        return

    print(f"  Detached ContentInfo DER: {len(ci_der)} bytes")

    ci = cms.ContentInfo.from_der(ci_der)
    sd = cms.SignedData.from_der(_strip_explicit0(ci.content))
    # In detached mode eContent is absent (None)
    print(f"  encap_content (should be None): {sd.encap_content!r}")
    assert sd.encap_content is None


def demo_multiple_hash_algorithms():
    section("SHA-256 vs SHA-384 digest")

    key, cert = _make_ca()
    content = b"Compare digest sizes."

    for hash_alg in ("sha256", "sha384"):
        try:
            ci_der = (
                cms.SignedDataBuilder(content)
                .add_signer(key, cert.to_der(), hash_algorithm=hash_alg)
                .build()
            )
            ci = cms.ContentInfo.from_der(ci_der)
            sd = cms.SignedData.from_der(_strip_explicit0(ci.content))
            si = sd.signer_infos[0]
            print(f"  hash_alg={hash_alg:6s}  digest_oid={si.digest_algorithm_oid}")
        except (NotImplementedError, AttributeError) as e:
            print(f"  {hash_alg}: skipped ({e})")


def main():
    print("=" * 60)
    print("CMS SignedData builder")
    print("=" * 60)
    demo_attached_signature()
    demo_detached_signature()
    demo_multiple_hash_algorithms()
    print("\nAll CMS SignedData examples completed.")


if __name__ == "__main__":
    main()