synta 0.3.2

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Example: Composite ML-DSA X.509 certificate generation and verification.

Demonstrates composite ML-DSA (draft-ietf-lamps-pq-composite-sigs-19):
- PrivateKey.generate_composite_ml_dsa(sub_arc) for each of the 18 variants
- CertificateBuilder.sign() with composite keys
- PKCS#8 serialisation round-trip (to_der / from_der)
- Public key extraction from a composite SPKI
- synta.oids composite ML-DSA OID constants

Composite ML-DSA combines an ML-DSA component with a traditional algorithm
(RSA, ECDSA, EdDSA). Both components sign the same M' message; both
signatures appear in a single concatenated BIT STRING value.

OID arc: 1.3.6.1.5.5.7.6.37 through .54  (18 variants).

Run:
    PYTHONPATH=python python3 examples/example_composite_mldsa_certificate.py
"""

import datetime
import synta
import synta.oids as oids
import synta.ext as ext


_UTC = datetime.timezone.utc
_NOW = datetime.datetime(2026, 1, 1, tzinfo=_UTC)
_ONE_YEAR = datetime.datetime(2027, 1, 1, tzinfo=_UTC)


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


# ── Mapping: sub_arc → (OID constant, display name) ──────────────────────────

COMPOSITE_VARIANTS = [
    (37, oids.MLDSA44_RSA2048_PSS_SHA256,             "MLDSA44-RSA2048-PSS-SHA256"),
    (38, oids.MLDSA44_RSA2048_PKCS15_SHA256,          "MLDSA44-RSA2048-PKCS15-SHA256"),
    (39, oids.MLDSA44_ED25519_SHA512,                 "MLDSA44-Ed25519-SHA512"),
    (40, oids.MLDSA44_ECDSA_P256_SHA256,              "MLDSA44-ECDSA-P256-SHA256"),
    (41, oids.MLDSA65_RSA3072_PSS_SHA512,             "MLDSA65-RSA3072-PSS-SHA512"),
    (42, oids.MLDSA65_RSA3072_PKCS15_SHA512,          "MLDSA65-RSA3072-PKCS15-SHA512"),
    (43, oids.MLDSA65_RSA4096_PSS_SHA512,             "MLDSA65-RSA4096-PSS-SHA512"),
    (44, oids.MLDSA65_RSA4096_PKCS15_SHA512,          "MLDSA65-RSA4096-PKCS15-SHA512"),
    (45, oids.MLDSA65_ECDSA_P256_SHA512,              "MLDSA65-ECDSA-P256-SHA512"),
    (46, oids.MLDSA65_ECDSA_P384_SHA512,              "MLDSA65-ECDSA-P384-SHA512"),
    (47, oids.MLDSA65_ECDSA_BRAINPOOL_P256R1_SHA512,  "MLDSA65-ECDSA-brainpoolP256r1-SHA512"),
    (48, oids.MLDSA65_ED25519_SHA512,                 "MLDSA65-Ed25519-SHA512"),
    (49, oids.MLDSA87_ECDSA_P384_SHA512,              "MLDSA87-ECDSA-P384-SHA512"),
    (50, oids.MLDSA87_ECDSA_BRAINPOOL_P384R1_SHA512,  "MLDSA87-ECDSA-brainpoolP384r1-SHA512"),
    (51, oids.MLDSA87_ED448_SHAKE256,                 "MLDSA87-Ed448-SHAKE256"),
    (52, oids.MLDSA87_RSA3072_PSS_SHA512,             "MLDSA87-RSA3072-PSS-SHA512"),
    (53, oids.MLDSA87_RSA4096_PSS_SHA512,             "MLDSA87-RSA4096-PSS-SHA512"),
    (54, oids.MLDSA87_ECDSA_P521_SHA512,              "MLDSA87-ECDSA-P521-SHA512"),
]


def demo_oid_constants():
    section("synta.oids composite ML-DSA OID constants")
    arc = oids.COMPOSITE_MLDSA_ARC
    print(f"  COMPOSITE_MLDSA_ARC = {arc}")
    assert str(arc) == "1.3.6.1.5.5.7.6", f"unexpected arc: {arc}"

    for sub_arc, oid, name in COMPOSITE_VARIANTS:
        expected = f"1.3.6.1.5.5.7.6.{sub_arc}"
        assert str(oid) == expected, f"{name}: expected {expected}, got {oid}"
        print(f"  sub_arc {sub_arc:2d}: {oid}  ({name})")

    print("\n  All 18 composite OID constants verified.")


def demo_key_generation_and_certificate(sub_arc: int, name: str):
    """Generate a composite key, build a self-signed cert, round-trip verify."""
    print(f"\n  [{sub_arc}] {name}")

    # Generate composite ML-DSA key.
    try:
        priv = synta.PrivateKey.generate_composite_ml_dsa(sub_arc)
    except ValueError as e:
        print(f"      SKIP (not supported in this build: {e})")
        return

    # PKCS#8 DER round-trip.
    pkcs8_der = priv.to_der()
    priv2 = synta.PrivateKey.from_der(pkcs8_der)
    assert priv2.to_der() == pkcs8_der, "PKCS#8 round-trip mismatch"

    # Build subject/issuer name.
    subject = synta.NameBuilder().common_name(f"Composite {name}").build()

    # Standard X.509 extensions for a self-signed CA.
    bc_der = ext.basic_constraints(ca=True, path_length=0)
    ku_der = ext.key_usage(ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN | ext.KU_DIGITAL_SIGNATURE)
    spki_der = priv.public_key.to_der()
    ski_der = ext.subject_key_identifier(spki_der)

    # Build and sign the self-signed certificate.
    cert = (
        synta.CertificateBuilder()
        .issuer_name(subject)
        .subject_name(subject)
        .public_key(priv.public_key)
        .serial_number(sub_arc)
        .not_valid_before_utc(_NOW)
        .not_valid_after_utc(_ONE_YEAR)
        .add_extension("2.5.29.19", True, bc_der)
        .add_extension("2.5.29.15", True, ku_der)
        .add_extension("2.5.29.14", False, ski_der)
        .sign(priv, "sha256")
    )

    assert isinstance(cert, synta.Certificate)
    assert cert.serial_number == sub_arc

    # Verify the signature algorithm OID matches the expected composite OID.
    sig_oid = str(cert.signature_algorithm_oid)
    expected_oid = f"1.3.6.1.5.5.7.6.{sub_arc}"
    assert sig_oid == expected_oid, (
        f"signature OID mismatch: got {sig_oid}, expected {expected_oid}"
    )

    # DER round-trip of the certificate.
    cert_der = cert.to_der()
    cert2 = synta.Certificate.from_der(cert_der)
    assert cert2.serial_number == cert.serial_number
    assert cert2.subject == cert.subject

    print(f"      signature_algorithm: {cert.signature_algorithm}")
    print(f"      cert DER:            {len(cert_der):6d} bytes  PKCS#8: {len(pkcs8_der):6d} bytes")
    print(f"      DER round-trip:      OK")


def demo_all_variants():
    section("Composite ML-DSA certificate generation — all 18 variants")
    for sub_arc, _oid, name in COMPOSITE_VARIANTS:
        demo_key_generation_and_certificate(sub_arc, name)


def demo_focused_round_trip():
    """Detailed walk-through of one composite variant (MLDSA65-ECDSA-P256-SHA512)."""
    section("Detailed round-trip: MLDSA65-ECDSA-P256-SHA512 (sub_arc=45)")

    sub_arc = 45
    try:
        ca_key = synta.PrivateKey.generate_composite_ml_dsa(sub_arc)
    except ValueError as e:
        print(f"  SKIP (not supported in this build: {e})")
        return

    # ── Key properties ────────────────────────────────────────────────────────
    print(f"  Key generated.  PKCS#8 DER length: {len(ca_key.to_der())} bytes")
    print(f"  Public key SPKI length:             {len(ca_key.public_key.to_der())} bytes")

    # ── PKCS#8 serialisation round-trip ───────────────────────────────────────
    pkcs8_der = ca_key.to_der()
    ca_key2 = synta.PrivateKey.from_der(pkcs8_der)
    assert ca_key2.to_der() == pkcs8_der
    print("  PKCS#8 DER round-trip: OK")

    # ── Build self-signed CA certificate ──────────────────────────────────────
    ca_name = synta.NameBuilder().country("DE").organization("Synta PQC").common_name("Composite CA").build()
    bc_der = ext.basic_constraints(ca=True, path_length=1)
    ku_der = ext.key_usage(ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN | ext.KU_DIGITAL_SIGNATURE)
    ski_der = ext.subject_key_identifier(ca_key.public_key.to_der())

    ca_cert = (
        synta.CertificateBuilder()
        .issuer_name(ca_name)
        .subject_name(ca_name)
        .public_key(ca_key.public_key)
        .serial_number(1)
        .not_valid_before_utc(_NOW)
        .not_valid_after_utc(_ONE_YEAR)
        .add_extension("2.5.29.19", True, bc_der)
        .add_extension("2.5.29.15", True, ku_der)
        .add_extension("2.5.29.14", False, ski_der)
        .sign(ca_key, "sha256")
    )

    print(f"  CA cert issuer:           {ca_cert.issuer}")
    print(f"  CA cert subject:          {ca_cert.subject}")
    print(f"  CA cert serial:           {ca_cert.serial_number}")
    print(f"  CA cert signature alg:    {ca_cert.signature_algorithm}")
    print(f"  CA cert signature OID:    {ca_cert.signature_algorithm_oid}")
    print(f"  CA cert DER:              {len(ca_cert.to_der())} bytes")

    # Verify the composite signature OID.
    assert str(ca_cert.signature_algorithm_oid) == "1.3.6.1.5.5.7.6.45"
    print("  Composite OID matches MLDSA65-ECDSA-P256-SHA512: OK")

    # ── Certificate DER round-trip ────────────────────────────────────────────
    ca_cert2 = synta.Certificate.from_der(ca_cert.to_der())
    assert ca_cert2.issuer == ca_cert.issuer
    assert ca_cert2.subject == ca_cert.subject
    assert ca_cert2.serial_number == 1
    print("  Certificate DER round-trip: OK")

    # ── Sign a leaf certificate with the composite CA key ─────────────────────
    leaf_key = synta.PrivateKey.generate_ec("P-256")
    leaf_name = synta.NameBuilder().common_name("leaf.example.com").build()
    san_der = ext.SubjectAlternativeNameBuilder().dns_name("leaf.example.com").build()
    bc_ee = ext.basic_constraints(ca=False)
    ku_ee = ext.key_usage(ext.KU_DIGITAL_SIGNATURE)
    aki_der = ext.authority_key_identifier(ca_key.public_key.to_der())

    leaf_cert = (
        synta.CertificateBuilder()
        .issuer_name(ca_cert.subject_raw_der)
        .subject_name(leaf_name)
        .public_key(leaf_key.public_key)
        .serial_number(2)
        .not_valid_before_utc(_NOW)
        .not_valid_after_utc(_ONE_YEAR)
        .add_extension("2.5.29.17", False, san_der)
        .add_extension("2.5.29.19", False, bc_ee)
        .add_extension("2.5.29.15", True, ku_ee)
        .add_extension("2.5.29.35", False, aki_der)
        .sign(ca_key, "sha256")
    )

    assert isinstance(leaf_cert, synta.Certificate)
    assert str(leaf_cert.signature_algorithm_oid) == "1.3.6.1.5.5.7.6.45"
    print(f"  Leaf cert subject:        {leaf_cert.subject}")
    print(f"  Leaf cert signature alg:  {leaf_cert.signature_algorithm}")
    print(f"  Leaf cert DER:            {len(leaf_cert.to_der())} bytes")
    print("  Leaf certificate signed by composite CA key: OK")


def main():
    print("=" * 70)
    print("Composite ML-DSA X.509 certificate example")
    print("draft-ietf-lamps-pq-composite-sigs-19")
    print("=" * 70)

    demo_oid_constants()
    demo_focused_round_trip()
    demo_all_variants()

    print("\nAll composite ML-DSA examples completed.")


if __name__ == "__main__":
    main()