synta 0.1.11

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# Certificate, CSR, and Name Builders


`CertificateBuilder`, `CsrBuilder`, and `NameBuilder` are top-level `synta` exports for
constructing DER-encoded X.509 certificates, PKCS#10 CSRs, and X.509 distinguished names.
All three follow a fluent API — each setter returns the same builder object so calls can be
chained.

---

## NameBuilder

Accumulates `AttributeTypeAndValue` entries and serialises them into a DER-encoded
`Name` SEQUENCE. The output bytes are suitable for use with `CertificateBuilder` and
`CsrBuilder`.

```python
class NameBuilder:
    def __init__(self) -> None: ...

    def common_name(self, value: str) -> NameBuilder: ...
    # commonName (2.5.4.3)

    def organization(self, value: str) -> NameBuilder: ...
    # organizationName (2.5.4.10)

    def organizational_unit(self, value: str) -> NameBuilder: ...
    # organizationalUnitName (2.5.4.11)

    def country(self, value: str) -> NameBuilder: ...
    # countryName (2.5.4.6) — two-letter ISO 3166-1 code

    def state(self, value: str) -> NameBuilder: ...
    # stateOrProvinceName (2.5.4.8)

    def locality(self, value: str) -> NameBuilder: ...
    # localityName (2.5.4.7)

    def street(self, value: str) -> NameBuilder: ...
    # streetAddress (2.5.4.9)

    def email_address(self, value: str) -> NameBuilder: ...
    # emailAddress (1.2.840.113549.1.9.1)

    def add_attr(self, oid: str, value: str) -> NameBuilder: ...
    # Add an attribute by dotted-decimal OID string with a UTF-8 string value.
    # Use synta.oids.attr constants for well-known attribute types.

    def build(self) -> bytes: ...
    # Return the DER-encoded Name SEQUENCE (complete TLV).
    # An empty builder returns b'\x30\x00'.
```

### Example

```python,ignore
import synta

# Simple CN-only name
cn_der = synta.NameBuilder().common_name("My Root CA").build()

# Full distinguished name
dn_der = (
    synta.NameBuilder()
    .country("US")
    .state("California")
    .locality("San Francisco")
    .organization("Example Corp")
    .organizational_unit("Engineering")
    .common_name("example.com")
    .build()
)

# Custom attribute by OID
import synta.oids as oids
dn_der = (
    synta.NameBuilder()
    .add_attr(str(oids.attr.ORGANIZATION), "Example Corp")
    .common_name("example.com")
    .build()
)
```

---

## CertificateBuilder

Builder for X.509 v3 certificates. All Name, SubjectPublicKeyInfo, and extension-value bytes
are stored verbatim — no re-parse or re-encode. The only per-field cost is copying the input
slice into the builder's owned storage.

```python
class CertificateBuilder:
    def __init__(self) -> None: ...

    def issuer_name(self, name_der: bytes) -> CertificateBuilder: ...
    # Set the issuer Name from pre-encoded DER bytes.
    # Certificate.subject_raw_der / Certificate.issuer_raw_der are suitable directly.

    def subject_name(self, name_der: bytes) -> CertificateBuilder: ...
    # Set the subject Name from pre-encoded DER bytes.

    def public_key(self, key: PublicKey) -> CertificateBuilder: ...
    # Set the SubjectPublicKeyInfo from a PublicKey object.

    def public_key_der(self, spki_der: bytes) -> CertificateBuilder: ...
    # Set the SubjectPublicKeyInfo from pre-encoded SPKI DER bytes (verbatim).

    def serial_number(self, n: int | bytes) -> CertificateBuilder: ...
    # Set the certificate serial number.
    # Accepts a Python int (any size) or big-endian bytes.

    def not_valid_before_utc(self, dt: datetime) -> CertificateBuilder: ...
    # Set notBefore.  dt must be a timezone-aware datetime (tzinfo must not be None).

    def not_valid_after_utc(self, dt: datetime) -> CertificateBuilder: ...
    # Set notAfter.  dt must be a timezone-aware datetime.

    def add_extension(self, oid: str, critical: bool, value_der: bytes) -> CertificateBuilder: ...
    # Add an X.509 v3 extension.
    # oid: dotted-decimal OID string.
    # value_der: the extnValue content bytes (what get_extension_value_der() returns).

    def sign(self, key: PrivateKey, algorithm: str, context: bytes | None = None) -> Certificate: ...
    # Sign and return a Certificate.
    # algorithm: hash name ("sha256", "sha384", "sha512") for RSA/ECDSA;
    # ignored for Ed25519/Ed448/ML-DSA.
    # context: ML-DSA domain-separation string (FIPS 204 §5.2); None = empty
    # (equivalent to no context).  Ignored for non-ML-DSA keys.
    # Raises ValueError if any required field is missing or signing fails.

    def sign_unsigned(self) -> Certificate: ...
    # Sign with RFC 9925 id-alg-unsigned (no private key required).
    # Produces a zero-length BIT STRING signature value.
    # Raises ValueError if any required field is missing.
```

### `context=` and ML-DSA signing

The `context` keyword argument on `sign()` is the FIPS 204 §5.2 domain-separation
string for ML-DSA keys. It is ignored for all other key types (RSA, ECDSA, Ed25519,
Ed448). When `context` is a non-empty `bytes` object and the key is an ML-DSA key,
the builder follows the manual path (`build_tbs` → `sign_ml_dsa_with_context` →
`assemble`) so the context is incorporated into the signature computation.

Pass `context=None` (the default) or `context=b""` for standard ML-DSA signing
without a context string.

### Example

```python,ignore
import synta, synta.ext as ext, datetime

# Prepare name and key
ca_key  = synta.PrivateKey.generate_ec("P-256")
ca_pub  = ca_key.public_key()
name_der = synta.NameBuilder().common_name("Test CA").build()

now    = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
expire = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)

bc_der = ext.basic_constraints(ca=True)
ku_der = ext.key_usage(ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN)

ca_cert = (
    synta.CertificateBuilder()
    .issuer_name(name_der)
    .subject_name(name_der)
    .public_key(ca_pub)
    .serial_number(1)
    .not_valid_before_utc(now)
    .not_valid_after_utc(expire)
    .add_extension("2.5.29.19", True, bc_der)
    .add_extension("2.5.29.15", True, ku_der)
    .sign(ca_key, "sha256")
)

# Issue a leaf certificate from a CSR
leaf_key = synta.PrivateKey.generate_ec("P-256")
csr = (
    synta.CsrBuilder()
    .subject_name(synta.NameBuilder().common_name("example.com").build())
    .public_key(leaf_key.public_key())
    .sign(leaf_key, "sha256")
)

san_der = ext.SAN().dns_name("example.com").dns_name("www.example.com").build()
leaf_cert = (
    synta.CertificateBuilder()
    .issuer_name(ca_cert.subject_raw_der)
    .subject_name(csr.subject_raw_der)
    .public_key_der(csr.subject_public_key_info_der)
    .serial_number(2)
    .not_valid_before_utc(now)
    .not_valid_after_utc(expire)
    .add_extension("2.5.29.17", False, san_der)
    .sign(ca_key, "sha256")
)

# ML-DSA certificate with a domain-separation context string
ml_key = synta.PrivateKey.generate_ml_dsa("ML-DSA-65")   # requires OpenSSL 3.5+
ml_cert = (
    synta.CertificateBuilder()
    .issuer_name(name_der)
    .subject_name(name_der)
    .public_key(ml_key.public_key())
    .serial_number(3)
    .not_valid_before_utc(now)
    .not_valid_after_utc(expire)
    .sign(ml_key, "sha256", context=b"my-app")
)
```

---

## CsrBuilder

Builder for PKCS#10 Certificate Signing Requests (RFC 2986). Produces a self-signed
`CertificationRequest` DER blob.

```python
class CsrBuilder:
    def __init__(self) -> None: ...

    def subject_name(self, name_der: bytes) -> CsrBuilder: ...
    # Set the subject Name from pre-encoded DER bytes.

    def public_key(self, key: PublicKey) -> CsrBuilder: ...
    # Set the SubjectPublicKeyInfo from a PublicKey object.

    def public_key_der(self, spki_der: bytes) -> CsrBuilder: ...
    # Set the SubjectPublicKeyInfo from pre-encoded SPKI DER bytes.

    def add_extension(self, oid: str, critical: bool, value_der: bytes) -> CsrBuilder: ...
    # Add an extension to the extensionRequest attribute.

    def sign(self, key: PrivateKey, algorithm: str, context: bytes | None = None) -> CertificationRequest: ...
    # Sign and return a CertificationRequest.
    # algorithm: same conventions as CertificateBuilder.sign().
    # context: ML-DSA domain-separation string (FIPS 204 §5.2); None = empty.
    # Ignored for non-ML-DSA keys.
    # Raises ValueError if subject name or public key is missing.
```

### `context=` and ML-DSA signing

Same semantics as `CertificateBuilder.sign()`. For ML-DSA keys a non-empty `context`
follows the manual path (`build_cri` → `sign_ml_dsa_with_context` → `assemble`).
Ignored for RSA, ECDSA, Ed25519, and Ed448 keys.

### Example

```python,ignore
import synta, synta.ext as ext

key = synta.PrivateKey.generate_ec("P-384")

san_der = ext.SAN().dns_name("example.com").build()
csr = (
    synta.CsrBuilder()
    .subject_name(synta.NameBuilder().common_name("example.com").build())
    .public_key(key.public_key())
    .add_extension("2.5.29.17", False, san_der)
    .sign(key, "sha384")
)

# Verify the self-signature
csr.verify_self_signature()

# Inspect
print(csr.subject)
print(csr.public_key_algorithm)

# ML-DSA CSR with context
ml_key = synta.PrivateKey.generate_ml_dsa("ML-DSA-44")   # requires OpenSSL 3.5+
ml_csr = (
    synta.CsrBuilder()
    .subject_name(synta.NameBuilder().common_name("ml-dsa-subject").build())
    .public_key(ml_key.public_key())
    .sign(ml_key, "sha256", context=b"my-app")
)
```

See also [CRL and OCSP Response Builders](crl-ocsp-builders.md) for building signed CRL
and OCSP response structures, and [X.509 Extension Value Builders](ext-builders.md) for
building extension values to pass to `add_extension`.