# 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`.