Skip to main content

_synta/
ext_builders.rs

1//! Python bindings for X.509 extension DER value builders.
2//!
3//! Registers the ``synta.ext`` submodule with low-level DER-encoding helpers
4//! for the most common X.509 v3 extension values.  Each function returns the
5//! raw DER bytes of the extension's ``extnValue`` — the content inside the
6//! OCTET STRING wrapper.
7//!
8//! ## Key identifier method constants
9//!
10//! Pass these to ``subject_key_identifier`` and ``authority_key_identifier``
11//! as the ``method`` argument:
12//!
13//! | Constant | Value | Specification |
14//! |----------|-------|---------------|
15//! | `KEYID_RFC5280` | 0 | SHA-1 of BIT STRING value of subjectPublicKey (RFC 5280 §4.2.1.2) |
16//! | `KEYID_RFC7093M1` | 1 | SHA-256 of BIT STRING value, truncated to 160 bits (RFC 7093 §2 m1) |
17//! | `KEYID_RFC7093M2` | 2 | SHA-384 of BIT STRING value, truncated to 160 bits (RFC 7093 §2 m2) |
18//! | `KEYID_RFC7093M3` | 3 | SHA-512 of BIT STRING value, truncated to 160 bits (RFC 7093 §2 m3) |
19//! | `KEYID_RFC7093M4` | 4 | SHA-256 of full SubjectPublicKeyInfo DER (RFC 7093 §2 m4) |
20//!
21//! ## Key usage bitmask constants
22//!
23//! ``KU_*`` constants are ready-to-OR bitmasks for ``key_usage``:
24//!
25//! | Constant | Mask | Named bit (RFC 5280 §4.2.1.3) |
26//! |----------|------|-------------------------------|
27//! | `KU_DIGITAL_SIGNATURE` | 0x001 | `digitalSignature` |
28//! | `KU_NON_REPUDIATION` | 0x002 | `contentCommitment` |
29//! | `KU_KEY_ENCIPHERMENT` | 0x004 | `keyEncipherment` |
30//! | `KU_DATA_ENCIPHERMENT` | 0x008 | `dataEncipherment` |
31//! | `KU_KEY_AGREEMENT` | 0x010 | `keyAgreement` |
32//! | `KU_KEY_CERT_SIGN` | 0x020 | `keyCertSign` |
33//! | `KU_CRL_SIGN` | 0x040 | `cRLSign` |
34//! | `KU_ENCIPHER_ONLY` | 0x080 | `encipherOnly` |
35//! | `KU_DECIPHER_ONLY` | 0x100 | `decipherOnly` |
36//!
37//! ## Fluent builder classes
38//!
39//! Four builder classes accumulate entries and produce DER on ``.build()``:
40//!
41//! | Class | Short alias | Extension | OID |
42//! |-------|-------------|-----------|-----|
43//! | ``SubjectAlternativeNameBuilder`` | ``SAN`` | Subject Alternative Name | 2.5.29.17 |
44//! | ``AuthorityInformationAccessBuilder`` | ``AIA`` | Authority Information Access | 1.3.6.1.5.5.7.1.1 |
45//! | ``ExtendedKeyUsageBuilder`` | ``EKU`` | Extended Key Usage | 2.5.29.37 |
46//! | ``NameConstraintsBuilder`` | ``NC`` | Name Constraints | 2.5.29.30 |
47
48use pyo3::exceptions::PyValueError;
49use pyo3::prelude::*;
50use pyo3::types::PyBytes;
51
52use synta_certificate::{
53    default_key_id_hasher, encode_authority_key_identifier, encode_basic_constraints,
54    encode_key_usage, encode_subject_key_identifier, AuthorityInformationAccessBuilder,
55    CRLDistributionPointsBuilder, CertificatePoliciesBuilder, ExtendedKeyUsageBuilder,
56    IssuerAlternativeNameBuilder, IssuingDistributionPointBuilder, KeyIdMethod,
57    NameConstraintsBuilder, SubjectAlternativeNameBuilder,
58};
59
60// ── Key identifier method constants ──────────────────────────────────────────
61
62/// RFC 5280 §4.2.1.2: SHA-1 of the BIT STRING value of ``subjectPublicKey``.
63pub const KEYID_RFC5280: u8 = 0;
64
65/// RFC 7093 §2 method 1: SHA-256 of BIT STRING value, truncated to 160 bits.
66pub const KEYID_RFC7093M1: u8 = 1;
67
68/// RFC 7093 §2 method 2: SHA-384 of BIT STRING value, truncated to 160 bits.
69pub const KEYID_RFC7093M2: u8 = 2;
70
71/// RFC 7093 §2 method 3: SHA-512 of BIT STRING value, truncated to 160 bits.
72pub const KEYID_RFC7093M3: u8 = 3;
73
74/// RFC 7093 §2 method 4: SHA-256 of the full ``SubjectPublicKeyInfo`` DER encoding.
75pub const KEYID_RFC7093M4: u8 = 4;
76
77// ── Key usage bitmask constants ───────────────────────────────────────────────
78
79/// ``digitalSignature`` (bit 0) — RFC 5280 §4.2.1.3.
80pub const KU_DIGITAL_SIGNATURE: u16 = 1 << 0;
81/// ``contentCommitment`` / ``nonRepudiation`` (bit 1) — RFC 5280 §4.2.1.3.
82pub const KU_NON_REPUDIATION: u16 = 1 << 1;
83/// ``keyEncipherment`` (bit 2) — RFC 5280 §4.2.1.3.
84pub const KU_KEY_ENCIPHERMENT: u16 = 1 << 2;
85/// ``dataEncipherment`` (bit 3) — RFC 5280 §4.2.1.3.
86pub const KU_DATA_ENCIPHERMENT: u16 = 1 << 3;
87/// ``keyAgreement`` (bit 4) — RFC 5280 §4.2.1.3.
88pub const KU_KEY_AGREEMENT: u16 = 1 << 4;
89/// ``keyCertSign`` (bit 5) — RFC 5280 §4.2.1.3.
90pub const KU_KEY_CERT_SIGN: u16 = 1 << 5;
91/// ``cRLSign`` (bit 6) — RFC 5280 §4.2.1.3.
92pub const KU_CRL_SIGN: u16 = 1 << 6;
93/// ``encipherOnly`` (bit 7) — RFC 5280 §4.2.1.3; only meaningful when keyAgreement is set.
94pub const KU_ENCIPHER_ONLY: u16 = 1 << 7;
95/// ``decipherOnly`` (bit 8) — RFC 5280 §4.2.1.3; only meaningful when keyAgreement is set.
96pub const KU_DECIPHER_ONLY: u16 = 1 << 8;
97
98// ── Internal helper ───────────────────────────────────────────────────────────
99
100/// Convert a Python-level ``KEYID_*`` integer constant to a [`KeyIdMethod`].
101///
102/// Accepts the five method codes exported as module-level constants:
103/// `KEYID_RFC5280` (0), `KEYID_RFC7093M1` (1), `KEYID_RFC7093M2` (2),
104/// `KEYID_RFC7093M3` (3), `KEYID_RFC7093M4` (4).
105///
106/// Returns `Err(ValueError)` with a descriptive message for any other value.
107fn method_from_int(m: u8) -> PyResult<KeyIdMethod> {
108    match m {
109        KEYID_RFC5280 => Ok(KeyIdMethod::Rfc5280Sha1),
110        KEYID_RFC7093M1 => Ok(KeyIdMethod::Rfc7093Method1Sha256),
111        KEYID_RFC7093M2 => Ok(KeyIdMethod::Rfc7093Method2Sha384),
112        KEYID_RFC7093M3 => Ok(KeyIdMethod::Rfc7093Method3Sha512),
113        KEYID_RFC7093M4 => Ok(KeyIdMethod::Rfc7093Method4 {
114            algorithm_oid: synta_certificate::ID_SHA256.to_vec(),
115        }),
116        other => Err(PyValueError::new_err(format!(
117            "unknown key identifier method {other}: \
118             use KEYID_RFC5280, KEYID_RFC7093M1, KEYID_RFC7093M2, \
119             KEYID_RFC7093M3, or KEYID_RFC7093M4"
120        ))),
121    }
122}
123
124// ── Python functions ──────────────────────────────────────────────────────────
125
126/// Encode a ``BasicConstraints`` extension value (OID 2.5.29.19).
127///
128/// Returns the DER bytes of the ``BasicConstraints`` SEQUENCE for use as the
129/// extension ``extnValue`` (inside the OCTET STRING wrapper):
130///
131/// ```text
132/// BasicConstraints ::= SEQUENCE {
133///     cA                  BOOLEAN DEFAULT FALSE,
134///     pathLenConstraint   INTEGER (0..MAX) OPTIONAL }
135/// ```
136///
137/// When ``ca`` is ``False`` the ``cA`` field is omitted (DER DEFAULT-FALSE
138/// rule).  ``path_length`` is silently ignored when ``ca`` is ``False``.
139///
140/// ```python,ignore
141/// import synta.ext as ext
142///
143/// # End-entity certificate: empty SEQUENCE (30 00)
144/// ee_bc = ext.basic_constraints()
145/// assert ee_bc == bytes([0x30, 0x00])
146///
147/// # CA with no path-length constraint (30 03 01 01 ff)
148/// ca_bc = ext.basic_constraints(ca=True)
149///
150/// # CA with pathLen = 0 (30 06 01 01 ff 02 01 00)
151/// ca_pl0 = ext.basic_constraints(ca=True, path_length=0)
152/// ```
153#[pyfunction]
154#[pyo3(signature = (ca = false, path_length = None))]
155fn basic_constraints<'py>(
156    py: Python<'py>,
157    ca: bool,
158    path_length: Option<u64>,
159) -> PyResult<Bound<'py, PyBytes>> {
160    let der = encode_basic_constraints(ca, path_length)
161        .ok_or_else(|| PyValueError::new_err("BasicConstraints DER encoding failed"))?;
162    Ok(PyBytes::new(py, &der))
163}
164
165/// Encode a ``CRLNumber`` extension value (OID 2.5.29.20, RFC 5280 §5.2.1).
166///
167/// Returns the bare DER-encoded ``INTEGER`` — the content that goes inside the
168/// OCTET STRING wrapper in the ``Extension`` SEQUENCE.  Pass the result
169/// directly to ``CertificateListBuilder.add_extension``:
170///
171/// ```python,ignore
172/// import synta.ext as ext
173///
174/// der = ext.crl_number(42)
175/// # → b'\x02\x01\x2a'
176///
177/// # Zero is encoded as a single content byte of 0x00:
178/// der = ext.crl_number(0)
179/// # → b'\x02\x01\x00'
180/// ```
181///
182/// ``n`` must be a non-negative integer that fits in a ``u64``.  Raises
183/// :exc:`ValueError` if ``n`` is negative or too large.
184#[pyfunction]
185fn crl_number<'py>(py: Python<'py>, n: u64) -> PyResult<Bound<'py, PyBytes>> {
186    // Build the big-endian two's-complement encoding for a non-negative u64.
187    // Strip leading zero bytes, but keep at least one byte.
188    // Prepend a 0x00 padding byte if the high bit of the first content byte
189    // would be set (which would make the value appear negative in two's complement).
190    let be_bytes = n.to_be_bytes(); // 8 bytes, big-endian
191    let first_nonzero = be_bytes.iter().position(|&b| b != 0).unwrap_or(7);
192    let content = &be_bytes[first_nonzero..];
193    // content is now the minimal non-zero prefix; for n=0 this is &[0x00] because
194    // first_nonzero is 7, so content = &be_bytes[7..] = &[0x00].
195    let needs_pad = content[0] & 0x80 != 0;
196    let content_len = content.len() + usize::from(needs_pad);
197
198    // Encode the length — for u64 the maximum content is 9 bytes (pad + 8), which
199    // fits in a single-byte DER length field (max 127 for short form).
200    let mut der = Vec::with_capacity(2 + content_len);
201    der.push(0x02u8); // INTEGER tag
202    der.push(content_len as u8); // length (always fits in one byte for u64)
203    if needs_pad {
204        der.push(0x00u8);
205    }
206    der.extend_from_slice(content);
207    Ok(PyBytes::new(py, &der))
208}
209
210/// Encode a ``KeyUsage`` extension value (OID 2.5.29.15).
211///
212/// ``bits`` is an integer bitmask formed by OR-ing the ``KU_*`` constants
213/// exported from this module.  Returns the DER BIT STRING value.
214///
215/// ```python,ignore
216/// import synta.ext as ext
217///
218/// # digitalSignature only → 03 02 07 80
219/// ku = ext.key_usage(ext.KU_DIGITAL_SIGNATURE)
220///
221/// # keyCertSign | cRLSign → 03 02 01 06
222/// ku = ext.key_usage(ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN)
223///
224/// # Typical CA key usage
225/// ku = ext.key_usage(
226///     ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN | ext.KU_DIGITAL_SIGNATURE
227/// )
228/// ```
229#[pyfunction]
230fn key_usage<'py>(py: Python<'py>, bits: u16) -> PyResult<Bound<'py, PyBytes>> {
231    let der = encode_key_usage(bits)
232        .ok_or_else(|| PyValueError::new_err("KeyUsage DER encoding failed"))?;
233    Ok(PyBytes::new(py, &der))
234}
235
236/// Encode a ``SubjectKeyIdentifier`` extension value (OID 2.5.29.14).
237///
238/// Decodes ``spki_der`` as a DER ``SubjectPublicKeyInfo`` and computes the
239/// key identifier according to ``method`` (one of the ``KEYID_*`` constants
240/// from this module).
241///
242/// Methods other than ``KEYID_RFC7093M4`` hash the BIT STRING value of
243/// ``subjectPublicKey`` (the key bytes, without the unused-bits octet).
244/// ``KEYID_RFC7093M4`` hashes the full ``SubjectPublicKeyInfo`` DER encoding.
245///
246/// Returns the DER OCTET STRING value, or raises :exc:`ValueError` if
247/// ``spki_der`` cannot be decoded or if hashing fails.
248///
249/// ```python,ignore
250/// import synta.ext as ext
251///
252/// # RFC 5280 default (SHA-1 of subjectPublicKey BIT STRING value)
253/// ski_der = ext.subject_key_identifier(cert.subject_public_key_info_der)
254///
255/// # RFC 7093 method 1 (SHA-256 of BIT STRING value, truncated to 160 bits)
256/// ski_der = ext.subject_key_identifier(
257///     cert.subject_public_key_info_der,
258///     method=ext.KEYID_RFC7093M1,
259/// )
260/// ```
261#[pyfunction]
262#[pyo3(signature = (spki_der, method = KEYID_RFC5280))]
263fn subject_key_identifier<'py>(
264    py: Python<'py>,
265    spki_der: &[u8],
266    method: u8,
267) -> PyResult<Bound<'py, PyBytes>> {
268    let m = method_from_int(method)?;
269    encode_subject_key_identifier(spki_der, m, &default_key_id_hasher())
270        .map(|v| PyBytes::new(py, &v))
271        .ok_or_else(|| {
272            PyValueError::new_err(
273                "failed to encode SubjectKeyIdentifier: \
274                 invalid SubjectPublicKeyInfo DER or hash error",
275            )
276        })
277}
278
279/// Encode an ``AuthorityKeyIdentifier`` extension value (OID 2.5.29.35).
280///
281/// Computes the key identifier from the issuer's ``SubjectPublicKeyInfo``
282/// using the same input selection and hash rules as
283/// :func:`subject_key_identifier`.  Sets only the ``keyIdentifier [0]``
284/// field; ``authorityCertIssuer`` and ``authorityCertSerialNumber`` are not
285/// included.
286///
287/// Returns the DER bytes of the ``AuthorityKeyIdentifier`` SEQUENCE, or
288/// raises :exc:`ValueError` on error.
289///
290/// ```python,ignore
291/// import synta.ext as ext
292///
293/// # RFC 5280 default (SHA-1 of issuer subjectPublicKey BIT STRING value)
294/// aki_der = ext.authority_key_identifier(ca_cert.subject_public_key_info_der)
295///
296/// # RFC 7093 method 1 (SHA-256 of BIT STRING value, truncated to 160 bits)
297/// aki_der = ext.authority_key_identifier(
298///     ca_cert.subject_public_key_info_der,
299///     method=ext.KEYID_RFC7093M1,
300/// )
301/// ```
302#[pyfunction]
303#[pyo3(signature = (issuer_spki_der, method = KEYID_RFC5280))]
304fn authority_key_identifier<'py>(
305    py: Python<'py>,
306    issuer_spki_der: &[u8],
307    method: u8,
308) -> PyResult<Bound<'py, PyBytes>> {
309    let m = method_from_int(method)?;
310    encode_authority_key_identifier(issuer_spki_der, m, &default_key_id_hasher())
311        .map(|v| PyBytes::new(py, &v))
312        .ok_or_else(|| {
313            PyValueError::new_err(
314                "failed to encode AuthorityKeyIdentifier: \
315                 invalid SubjectPublicKeyInfo DER or hash error",
316            )
317        })
318}
319
320// ── SubjectAlternativeNameBuilder ─────────────────────────────────────────────
321
322/// Fluent builder for a ``SubjectAltName`` extension value (OID 2.5.29.17).
323///
324/// Accumulate :class:`GeneralName` entries with the fluent methods, then call
325/// :meth:`build` to obtain the DER bytes of the ``GeneralNames`` SEQUENCE —
326/// the content that goes inside the OCTET STRING wrapper in the ``Extension``
327/// SEQUENCE.
328///
329/// The class is also available as the short alias ``synta.ext.SAN``.
330///
331/// ```python,ignore
332/// import synta.ext as ext
333///
334/// san = ext.SAN() \
335///     .dns_name("www.example.com") \
336///     .dns_name("example.com") \
337///     .rfc822_name("admin@example.com") \
338///     .ip_address(b"\xc0\xa8\x01\x01") \
339///     .build()
340/// ```
341#[pyclass(name = "SubjectAlternativeNameBuilder")]
342pub struct PySubjectAlternativeNameBuilder {
343    inner: SubjectAlternativeNameBuilder,
344    built: bool,
345}
346
347#[pymethods]
348impl PySubjectAlternativeNameBuilder {
349    #[new]
350    fn new() -> Self {
351        Self {
352            inner: SubjectAlternativeNameBuilder::new(),
353            built: false,
354        }
355    }
356
357    /// Add a ``dNSName [2]`` GeneralName.
358    fn dns_name<'py>(slf: Bound<'py, Self>, name: &str) -> Bound<'py, Self> {
359        {
360            let mut guard = slf.borrow_mut();
361            let old = std::mem::replace(&mut guard.inner, SubjectAlternativeNameBuilder::new());
362            guard.inner = old.dns_name(name);
363        }
364        slf
365    }
366
367    /// Add an ``rfc822Name [1]`` GeneralName (email address).
368    fn rfc822_name<'py>(slf: Bound<'py, Self>, email: &str) -> Bound<'py, Self> {
369        {
370            let mut guard = slf.borrow_mut();
371            let old = std::mem::replace(&mut guard.inner, SubjectAlternativeNameBuilder::new());
372            guard.inner = old.rfc822_name(email);
373        }
374        slf
375    }
376
377    /// Add a ``uniformResourceIdentifier [6]`` GeneralName (URI).
378    fn uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
379        {
380            let mut guard = slf.borrow_mut();
381            let old = std::mem::replace(&mut guard.inner, SubjectAlternativeNameBuilder::new());
382            guard.inner = old.uri(uri);
383        }
384        slf
385    }
386
387    /// Add an ``iPAddress [7]`` GeneralName.
388    ///
389    /// Pass 4 bytes for IPv4 or 16 bytes for IPv6.
390    fn ip_address<'py>(slf: Bound<'py, Self>, addr: &[u8]) -> Bound<'py, Self> {
391        {
392            let mut guard = slf.borrow_mut();
393            let old = std::mem::replace(&mut guard.inner, SubjectAlternativeNameBuilder::new());
394            guard.inner = old.ip_address(addr);
395        }
396        slf
397    }
398
399    /// Add a ``directoryName [4]`` GeneralName (EXPLICIT wrapper).
400    ///
401    /// ``name_der`` is the complete DER TLV bytes of the ``Name`` SEQUENCE
402    /// (e.g. from :meth:`synta.NameBuilder.build`).  Silently ignored if
403    /// ``name_der`` cannot be decoded.
404    fn directory_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
405        {
406            let mut guard = slf.borrow_mut();
407            let old = std::mem::replace(&mut guard.inner, SubjectAlternativeNameBuilder::new());
408            guard.inner = old.directory_name(name_der);
409        }
410        slf
411    }
412
413    /// Add a ``registeredID [8]`` GeneralName.
414    ///
415    /// ``oid_comps`` is the list of arc components of the OID (e.g.
416    /// ``[1, 3, 6, 1, 5, 5, 7, 3, 1]`` for id-kp-serverAuth).
417    fn registered_id<'py>(
418        slf: Bound<'py, Self>,
419        oid_comps: Vec<u32>,
420    ) -> PyResult<Bound<'py, Self>> {
421        let oid = synta::ObjectIdentifier::new(&oid_comps)
422            .map_err(|e| PyValueError::new_err(format!("invalid OID: {e}")))?;
423        {
424            let mut guard = slf.borrow_mut();
425            let old = std::mem::replace(&mut guard.inner, SubjectAlternativeNameBuilder::new());
426            guard.inner = old.registered_id(oid);
427        }
428        Ok(slf)
429    }
430
431    /// Add an ``otherName [0]`` GeneralName.
432    ///
433    /// ``other_name_der`` is the complete DER TLV bytes of the ``OtherName``
434    /// SEQUENCE: ``SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }``.
435    ///
436    /// Raises :exc:`ValueError` if ``other_name_der`` cannot be decoded as a
437    /// valid ``OtherName`` structure.
438    fn other_name<'py>(slf: Bound<'py, Self>, other_name_der: &[u8]) -> Bound<'py, Self> {
439        {
440            let mut guard = slf.borrow_mut();
441            let old = std::mem::replace(&mut guard.inner, SubjectAlternativeNameBuilder::new());
442            guard.inner = old.other_name(other_name_der);
443        }
444        slf
445    }
446
447    /// Build the DER-encoded ``GeneralNames`` SEQUENCE.
448    ///
449    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
450    /// entries have been added.  Raises :exc:`ValueError` if any entry failed
451    /// DER encoding or if ``build()`` is called more than once on the same
452    /// builder instance.
453    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
454        if self.built {
455            return Err(PyValueError::new_err(
456                "SubjectAlternativeNameBuilder.build() has already been called; \
457                 create a new builder instance",
458            ));
459        }
460        self.built = true;
461        let builder = std::mem::replace(&mut self.inner, SubjectAlternativeNameBuilder::new());
462        let der = builder.build().map_err(PyValueError::new_err)?;
463        Ok(PyBytes::new(py, &der))
464    }
465}
466
467// ── AuthorityInformationAccessBuilder ─────────────────────────────────────────
468
469/// Fluent builder for an ``AuthorityInfoAccessSyntax`` extension value
470/// (OID 1.3.6.1.5.5.7.1.1, Authority Information Access, RFC 5280 §4.2.2.1).
471///
472/// Add OCSP and/or CA Issuers URIs with the fluent methods, then call
473/// :meth:`build` to obtain the DER bytes of the ``AuthorityInfoAccessSyntax``
474/// SEQUENCE — the content that goes inside the OCTET STRING wrapper.
475///
476/// The class is also available as the short alias ``synta.ext.AIA``.
477///
478/// ```python,ignore
479/// import synta.ext as ext
480///
481/// aia = ext.AIA() \
482///     .ocsp("http://ocsp.example.com") \
483///     .ca_issuers("http://ca.example.com/ca.crt") \
484///     .build()
485/// ```
486#[pyclass(name = "AuthorityInformationAccessBuilder")]
487pub struct PyAuthorityInformationAccessBuilder {
488    inner: AuthorityInformationAccessBuilder,
489    built: bool,
490}
491
492#[pymethods]
493impl PyAuthorityInformationAccessBuilder {
494    #[new]
495    fn new() -> Self {
496        Self {
497            inner: AuthorityInformationAccessBuilder::new(),
498            built: false,
499        }
500    }
501
502    /// Add an OCSP responder URI (``id-ad-ocsp``, 1.3.6.1.5.5.7.48.1).
503    fn ocsp<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
504        {
505            let mut guard = slf.borrow_mut();
506            let old = std::mem::replace(&mut guard.inner, AuthorityInformationAccessBuilder::new());
507            guard.inner = old.ocsp(uri);
508        }
509        slf
510    }
511
512    /// Add a CA Issuers URI (``id-ad-caIssuers``, 1.3.6.1.5.5.7.48.2).
513    fn ca_issuers<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
514        {
515            let mut guard = slf.borrow_mut();
516            let old = std::mem::replace(&mut guard.inner, AuthorityInformationAccessBuilder::new());
517            guard.inner = old.ca_issuers(uri);
518        }
519        slf
520    }
521
522    /// Build the DER-encoded ``AuthorityInfoAccessSyntax`` SEQUENCE.
523    ///
524    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
525    /// entries have been added.  Raises :exc:`ValueError` if any entry failed
526    /// DER encoding or if ``build()`` is called more than once on the same
527    /// builder instance.
528    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
529        if self.built {
530            return Err(PyValueError::new_err(
531                "AuthorityInformationAccessBuilder.build() has already been called; \
532                 create a new builder instance",
533            ));
534        }
535        self.built = true;
536        let builder = std::mem::replace(&mut self.inner, AuthorityInformationAccessBuilder::new());
537        let der = builder.build().map_err(PyValueError::new_err)?;
538        Ok(PyBytes::new(py, &der))
539    }
540}
541
542// ── ExtendedKeyUsageBuilder ───────────────────────────────────────────────────
543
544/// Fluent builder for an ``ExtKeyUsageSyntax`` extension value
545/// (OID 2.5.29.37, Extended Key Usage, RFC 5280 §4.2.1.12).
546///
547/// Add well-known key purpose OIDs with the convenience methods or custom OIDs
548/// via :meth:`add_oid`, then call :meth:`build` to obtain the DER bytes of the
549/// ``ExtKeyUsageSyntax`` SEQUENCE.
550///
551/// The class is also available as the short alias ``synta.ext.EKU``.
552///
553/// ```python,ignore
554/// import synta.ext as ext
555///
556/// # Typical TLS server certificate EKU
557/// eku = ext.EKU().server_auth().client_auth().build()
558///
559/// # Custom OID
560/// eku = ext.EKU().add_oid([1, 3, 6, 1, 5, 5, 7, 3, 1]).build()
561/// ```
562#[pyclass(name = "ExtendedKeyUsageBuilder")]
563pub struct PyExtendedKeyUsageBuilder {
564    inner: ExtendedKeyUsageBuilder,
565    built: bool,
566}
567
568#[pymethods]
569impl PyExtendedKeyUsageBuilder {
570    #[new]
571    fn new() -> Self {
572        Self {
573            inner: ExtendedKeyUsageBuilder::new(),
574            built: false,
575        }
576    }
577
578    /// Add ``id-kp-serverAuth`` (1.3.6.1.5.5.7.3.1).
579    fn server_auth<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
580        {
581            let mut guard = slf.borrow_mut();
582            let old = std::mem::replace(&mut guard.inner, ExtendedKeyUsageBuilder::new());
583            guard.inner = old.server_auth();
584        }
585        slf
586    }
587
588    /// Add ``id-kp-clientAuth`` (1.3.6.1.5.5.7.3.2).
589    fn client_auth<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
590        {
591            let mut guard = slf.borrow_mut();
592            let old = std::mem::replace(&mut guard.inner, ExtendedKeyUsageBuilder::new());
593            guard.inner = old.client_auth();
594        }
595        slf
596    }
597
598    /// Add ``id-kp-codeSigning`` (1.3.6.1.5.5.7.3.3).
599    fn code_signing<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
600        {
601            let mut guard = slf.borrow_mut();
602            let old = std::mem::replace(&mut guard.inner, ExtendedKeyUsageBuilder::new());
603            guard.inner = old.code_signing();
604        }
605        slf
606    }
607
608    /// Add ``id-kp-emailProtection`` (1.3.6.1.5.5.7.3.4).
609    fn email_protection<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
610        {
611            let mut guard = slf.borrow_mut();
612            let old = std::mem::replace(&mut guard.inner, ExtendedKeyUsageBuilder::new());
613            guard.inner = old.email_protection();
614        }
615        slf
616    }
617
618    /// Add ``id-kp-timeStamping`` (1.3.6.1.5.5.7.3.8).
619    fn time_stamping<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
620        {
621            let mut guard = slf.borrow_mut();
622            let old = std::mem::replace(&mut guard.inner, ExtendedKeyUsageBuilder::new());
623            guard.inner = old.time_stamping();
624        }
625        slf
626    }
627
628    /// Add ``id-kp-OCSPSigning`` (1.3.6.1.5.5.7.3.9).
629    fn ocsp_signing<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
630        {
631            let mut guard = slf.borrow_mut();
632            let old = std::mem::replace(&mut guard.inner, ExtendedKeyUsageBuilder::new());
633            guard.inner = old.ocsp_signing();
634        }
635        slf
636    }
637
638    /// Add a custom key purpose OID by component list.
639    ///
640    /// ``comps`` is a list of integers forming the OID arc, e.g.
641    /// ``[1, 3, 6, 1, 5, 5, 7, 3, 1]`` for ``id-kp-serverAuth``.
642    fn add_oid<'py>(slf: Bound<'py, Self>, comps: Vec<u32>) -> Bound<'py, Self> {
643        {
644            let mut guard = slf.borrow_mut();
645            let old = std::mem::replace(&mut guard.inner, ExtendedKeyUsageBuilder::new());
646            guard.inner = old.add_oid(&comps);
647        }
648        slf
649    }
650
651    /// Build the DER-encoded ``ExtKeyUsageSyntax`` SEQUENCE.
652    ///
653    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
654    /// OIDs have been added.  Raises :exc:`ValueError` if any OID was invalid
655    /// or if ``build()`` is called more than once on the same builder instance.
656    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
657        if self.built {
658            return Err(PyValueError::new_err(
659                "ExtendedKeyUsageBuilder.build() has already been called; \
660                 create a new builder instance",
661            ));
662        }
663        self.built = true;
664        let builder = std::mem::replace(&mut self.inner, ExtendedKeyUsageBuilder::new());
665        let der = builder.build().map_err(PyValueError::new_err)?;
666        Ok(PyBytes::new(py, &der))
667    }
668}
669
670// ── NameConstraintsBuilder ────────────────────────────────────────────────────
671
672/// Fluent builder for a ``NameConstraints`` extension value (OID 2.5.29.30,
673/// RFC 5280 §4.2.1.10).
674///
675/// The ``NameConstraints`` extension restricts the namespace within which all
676/// subject names in certificates issued by a CA must fall.  Names in
677/// ``permittedSubtrees`` are allowed; names in ``excludedSubtrees`` are
678/// forbidden.  When present in a CA certificate this extension MUST be marked
679/// critical.
680///
681/// Add permitted and excluded subtrees using the typed methods, then call
682/// :meth:`build` to obtain the DER bytes of the ``NameConstraints`` SEQUENCE —
683/// the content that goes inside the OCTET STRING wrapper in the ``Extension``
684/// SEQUENCE.
685///
686/// The class is also available as the short alias ``synta.ext.NC``.
687///
688/// **DNS name conventions:** prefix the domain with a leading dot (e.g.
689/// ``".example.com"``) to constrain the entire subtree rooted at
690/// ``example.com``; omit the dot to constrain only that exact host.
691///
692/// **IP address constraints:** pass address bytes concatenated with mask bytes
693/// — 8 bytes for IPv4 (e.g. ``b"\\xc0\\xa8\\x00\\x00\\xff\\xff\\x00\\x00"``
694/// for ``192.168.0.0/16``) or 32 bytes for IPv6.
695///
696/// ```python,ignore
697/// import synta.ext as ext
698///
699/// # Permit example.com DNS subtree and a private IP range;
700/// # exclude a known-bad subdomain.
701/// nc = ext.NC() \
702///     .permit_dns(".example.com") \
703///     .permit_rfc822(".example.com") \
704///     .permit_ip(b"\x0a\x00\x00\x00\xff\x00\x00\x00") \
705///     .exclude_dns(".evil.com") \
706///     .build()
707/// ```
708#[pyclass(name = "NameConstraintsBuilder")]
709pub struct PyNameConstraintsBuilder {
710    inner: NameConstraintsBuilder,
711    built: bool,
712}
713
714#[pymethods]
715impl PyNameConstraintsBuilder {
716    #[new]
717    fn new() -> Self {
718        Self {
719            inner: NameConstraintsBuilder::new(),
720            built: false,
721        }
722    }
723
724    /// Add a DNS name constraint to the permitted subtrees.
725    ///
726    /// Use a leading dot (e.g. ``".example.com"``) to constrain an entire
727    /// domain subtree; omit the dot (e.g. ``"host.example.com"``) to
728    /// constrain only that exact host name.
729    fn permit_dns<'py>(slf: Bound<'py, Self>, dns: &str) -> Bound<'py, Self> {
730        {
731            let mut guard = slf.borrow_mut();
732            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
733            guard.inner = old.permit_dns(dns);
734        }
735        slf
736    }
737
738    /// Add an RFC 822 (email) domain constraint to the permitted subtrees.
739    ///
740    /// Pass a bare domain (e.g. ``"example.com"``) to permit all addresses
741    /// in that domain, or a full address (e.g. ``"user@example.com"``) to
742    /// permit only that specific mailbox.
743    fn permit_rfc822<'py>(slf: Bound<'py, Self>, email: &str) -> Bound<'py, Self> {
744        {
745            let mut guard = slf.borrow_mut();
746            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
747            guard.inner = old.permit_rfc822(email);
748        }
749        slf
750    }
751
752    /// Add a URI scheme constraint to the permitted subtrees.
753    ///
754    /// Per RFC 5280 §4.2.1.10, the constraint value is a host name or
755    /// dot-prefixed domain (e.g. ``".example.com"``), not a full URI.
756    fn permit_uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
757        {
758            let mut guard = slf.borrow_mut();
759            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
760            guard.inner = old.permit_uri(uri);
761        }
762        slf
763    }
764
765    /// Add an IP prefix to the permitted subtrees.
766    ///
767    /// Pass 8 bytes for IPv4 (4-byte address + 4-byte mask) or 32 bytes for
768    /// IPv6 (16-byte address + 16-byte mask), as required by RFC 5280 §4.2.1.10.
769    fn permit_ip<'py>(slf: Bound<'py, Self>, addr_and_mask: &[u8]) -> Bound<'py, Self> {
770        {
771            let mut guard = slf.borrow_mut();
772            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
773            guard.inner = old.permit_ip(addr_and_mask);
774        }
775        slf
776    }
777
778    /// Add a directory name constraint to the permitted subtrees.
779    ///
780    /// ``name_der`` is the complete DER TLV bytes of an X.509 ``Name``
781    /// SEQUENCE (e.g. from :meth:`synta.NameBuilder.build`).  The constraint
782    /// is encoded as a ``directoryName [4]`` ``GeneralName`` inside a
783    /// ``GeneralSubtree``.
784    fn permit_directory_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
785        {
786            let mut guard = slf.borrow_mut();
787            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
788            guard.inner = old.permit_directory_name(name_der);
789        }
790        slf
791    }
792
793    /// Add a DNS name constraint to the excluded subtrees.
794    ///
795    /// Use a leading dot (e.g. ``".evil.com"``) to exclude the entire
796    /// subtree rooted at ``evil.com``; omit the dot to exclude only that
797    /// exact host name.
798    fn exclude_dns<'py>(slf: Bound<'py, Self>, dns: &str) -> Bound<'py, Self> {
799        {
800            let mut guard = slf.borrow_mut();
801            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
802            guard.inner = old.exclude_dns(dns);
803        }
804        slf
805    }
806
807    /// Add an RFC 822 (email) domain constraint to the excluded subtrees.
808    ///
809    /// Pass a bare domain (e.g. ``"evil.com"``) to exclude all addresses in
810    /// that domain, or a full address (e.g. ``"user@evil.com"``) to exclude
811    /// only that specific mailbox.
812    fn exclude_rfc822<'py>(slf: Bound<'py, Self>, email: &str) -> Bound<'py, Self> {
813        {
814            let mut guard = slf.borrow_mut();
815            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
816            guard.inner = old.exclude_rfc822(email);
817        }
818        slf
819    }
820
821    /// Add a URI scheme constraint to the excluded subtrees.
822    ///
823    /// Per RFC 5280 §4.2.1.10, the constraint value is a host name or
824    /// dot-prefixed domain, not a full URI.
825    fn exclude_uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
826        {
827            let mut guard = slf.borrow_mut();
828            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
829            guard.inner = old.exclude_uri(uri);
830        }
831        slf
832    }
833
834    /// Add an IP prefix to the excluded subtrees.
835    ///
836    /// Pass 8 bytes for IPv4 (4-byte address + 4-byte mask) or 32 bytes for
837    /// IPv6 (16-byte address + 16-byte mask), as required by RFC 5280 §4.2.1.10.
838    fn exclude_ip<'py>(slf: Bound<'py, Self>, addr_and_mask: &[u8]) -> Bound<'py, Self> {
839        {
840            let mut guard = slf.borrow_mut();
841            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
842            guard.inner = old.exclude_ip(addr_and_mask);
843        }
844        slf
845    }
846
847    /// Add a directory name constraint to the excluded subtrees.
848    ///
849    /// ``name_der`` is the complete DER TLV bytes of an X.509 ``Name``
850    /// SEQUENCE (e.g. from :meth:`synta.NameBuilder.build`).  The constraint
851    /// is encoded as a ``directoryName [4]`` ``GeneralName`` inside a
852    /// ``GeneralSubtree``.
853    fn exclude_directory_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
854        {
855            let mut guard = slf.borrow_mut();
856            let old = std::mem::replace(&mut guard.inner, NameConstraintsBuilder::new());
857            guard.inner = old.exclude_directory_name(name_der);
858        }
859        slf
860    }
861
862    /// Build the DER-encoded ``NameConstraints`` SEQUENCE.
863    ///
864    /// Returns the DER bytes of the outer ``NameConstraints`` SEQUENCE (tag
865    /// ``30``), ready to be placed inside the ``extnValue`` OCTET STRING of
866    /// an ``Extension``.  If neither permitted nor excluded subtrees have been
867    /// added, the result is an empty SEQUENCE (``b'\\x30\\x00'``).
868    ///
869    /// Permitted subtrees appear as ``[0] IMPLICIT`` (tag ``A0``); excluded
870    /// subtrees as ``[1] IMPLICIT`` (tag ``A1``), as required by
871    /// RFC 5280 §4.2.1.10.
872    ///
873    /// Raises :exc:`ValueError` if any entry failed DER encoding or if
874    /// ``build()`` is called more than once on the same builder instance.
875    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
876        if self.built {
877            return Err(PyValueError::new_err(
878                "NameConstraintsBuilder.build() has already been called; \
879                 create a new builder instance",
880            ));
881        }
882        self.built = true;
883        let builder = std::mem::replace(&mut self.inner, NameConstraintsBuilder::new());
884        let der = builder.build().map_err(PyValueError::new_err)?;
885        Ok(PyBytes::new(py, &der))
886    }
887}
888
889// ── CRLDistributionPointsBuilder ─────────────────────────────────────────────
890
891/// Fluent builder for a ``CRLDistributionPoints`` extension value (OID 2.5.29.31).
892///
893/// Add distribution point entries with the fluent methods, then call
894/// :meth:`build` to obtain the DER bytes of the ``CRLDistributionPoints``
895/// SEQUENCE OF — the content that goes inside the OCTET STRING wrapper in the
896/// ``Extension`` SEQUENCE.
897///
898/// The class is also available as the short alias ``synta.ext.CDP``.
899///
900/// ```python,ignore
901/// import synta.ext as ext
902///
903/// cdp = ext.CDP() \
904///     .full_name_uri("http://crl.example.com/ca.crl") \
905///     .build()
906/// ```
907#[pyclass(name = "CRLDistributionPointsBuilder")]
908pub struct PyCRLDistributionPointsBuilder {
909    inner: CRLDistributionPointsBuilder,
910    built: bool,
911}
912
913#[pymethods]
914impl PyCRLDistributionPointsBuilder {
915    #[new]
916    fn new() -> Self {
917        Self {
918            inner: CRLDistributionPointsBuilder::new(),
919            built: false,
920        }
921    }
922
923    /// Add a distribution point with a URI as the full name.
924    ///
925    /// ``uri`` is an ASCII URI string (e.g. ``"http://crl.example.com/ca.crl"``).
926    fn full_name_uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
927        {
928            let mut guard = slf.borrow_mut();
929            let old = std::mem::replace(&mut guard.inner, CRLDistributionPointsBuilder::new());
930            guard.inner = old.full_name_uri(uri);
931        }
932        slf
933    }
934
935    /// Add a distribution point with a DNS name as the full name.
936    fn full_name_dns<'py>(slf: Bound<'py, Self>, dns: &str) -> Bound<'py, Self> {
937        {
938            let mut guard = slf.borrow_mut();
939            let old = std::mem::replace(&mut guard.inner, CRLDistributionPointsBuilder::new());
940            guard.inner = old.full_name_dns(dns);
941        }
942        slf
943    }
944
945    /// Add a distribution point with a directory name as the full name.
946    ///
947    /// ``name_der`` is the complete DER TLV bytes of an X.509 ``Name`` SEQUENCE
948    /// (e.g. from :meth:`synta.NameBuilder.build`).
949    fn full_name_directory<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
950        {
951            let mut guard = slf.borrow_mut();
952            let old = std::mem::replace(&mut guard.inner, CRLDistributionPointsBuilder::new());
953            guard.inner = old.full_name_directory(name_der);
954        }
955        slf
956    }
957
958    /// Build the DER-encoded ``CRLDistributionPoints`` SEQUENCE OF.
959    ///
960    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
961    /// entries have been added.  Raises :exc:`ValueError` if any entry failed
962    /// DER encoding or if ``build()`` is called more than once on the same
963    /// builder instance.
964    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
965        if self.built {
966            return Err(PyValueError::new_err(
967                "CRLDistributionPointsBuilder.build() has already been called; \
968                 create a new builder instance",
969            ));
970        }
971        self.built = true;
972        let builder = std::mem::replace(&mut self.inner, CRLDistributionPointsBuilder::new());
973        let der = builder.build().map_err(PyValueError::new_err)?;
974        Ok(PyBytes::new(py, &der))
975    }
976}
977
978// ── IssuerAlternativeNameBuilder ──────────────────────────────────────────────
979
980/// Fluent builder for an ``IssuerAltName`` extension value (OID 2.5.29.18).
981///
982/// Accumulate :class:`GeneralName` entries with the fluent methods, then call
983/// :meth:`build` to obtain the DER bytes of the ``GeneralNames`` SEQUENCE —
984/// the content that goes inside the OCTET STRING wrapper in the ``Extension``
985/// SEQUENCE.
986///
987/// The encoding is identical to :class:`SubjectAlternativeNameBuilder` (OID
988/// 2.5.29.17) but uses the Issuer Alternative Name OID (2.5.29.18) instead.
989/// Note: ``otherName`` is not supported by this builder.
990///
991/// The class is also available as the short alias ``synta.ext.IAN``.
992///
993/// ```python,ignore
994/// import synta.ext as ext
995///
996/// ian = ext.IAN() \
997///     .rfc822_name("ca@example.com") \
998///     .uri("http://www.example.com") \
999///     .build()
1000/// ```
1001#[pyclass(name = "IssuerAlternativeNameBuilder")]
1002pub struct PyIssuerAlternativeNameBuilder {
1003    inner: IssuerAlternativeNameBuilder,
1004    built: bool,
1005}
1006
1007#[pymethods]
1008impl PyIssuerAlternativeNameBuilder {
1009    #[new]
1010    fn new() -> Self {
1011        Self {
1012            inner: IssuerAlternativeNameBuilder::new(),
1013            built: false,
1014        }
1015    }
1016
1017    /// Add a ``dNSName [2]`` GeneralName.
1018    fn dns_name<'py>(slf: Bound<'py, Self>, name: &str) -> Bound<'py, Self> {
1019        {
1020            let mut guard = slf.borrow_mut();
1021            let old = std::mem::replace(&mut guard.inner, IssuerAlternativeNameBuilder::new());
1022            guard.inner = old.dns_name(name);
1023        }
1024        slf
1025    }
1026
1027    /// Add an ``rfc822Name [1]`` GeneralName (email address).
1028    fn rfc822_name<'py>(slf: Bound<'py, Self>, email: &str) -> Bound<'py, Self> {
1029        {
1030            let mut guard = slf.borrow_mut();
1031            let old = std::mem::replace(&mut guard.inner, IssuerAlternativeNameBuilder::new());
1032            guard.inner = old.rfc822_name(email);
1033        }
1034        slf
1035    }
1036
1037    /// Add a ``uniformResourceIdentifier [6]`` GeneralName (URI).
1038    fn uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
1039        {
1040            let mut guard = slf.borrow_mut();
1041            let old = std::mem::replace(&mut guard.inner, IssuerAlternativeNameBuilder::new());
1042            guard.inner = old.uri(uri);
1043        }
1044        slf
1045    }
1046
1047    /// Add an ``iPAddress [7]`` GeneralName.
1048    ///
1049    /// Pass 4 bytes for IPv4 or 16 bytes for IPv6.
1050    fn ip_address<'py>(slf: Bound<'py, Self>, addr: &[u8]) -> Bound<'py, Self> {
1051        {
1052            let mut guard = slf.borrow_mut();
1053            let old = std::mem::replace(&mut guard.inner, IssuerAlternativeNameBuilder::new());
1054            guard.inner = old.ip_address(addr);
1055        }
1056        slf
1057    }
1058
1059    /// Add a ``directoryName [4]`` GeneralName (EXPLICIT wrapper).
1060    ///
1061    /// ``name_der`` is the complete DER TLV bytes of the ``Name`` SEQUENCE
1062    /// (e.g. from :meth:`synta.NameBuilder.build`).  Silently ignored if
1063    /// ``name_der`` cannot be decoded.
1064    fn directory_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
1065        {
1066            let mut guard = slf.borrow_mut();
1067            let old = std::mem::replace(&mut guard.inner, IssuerAlternativeNameBuilder::new());
1068            guard.inner = old.directory_name(name_der);
1069        }
1070        slf
1071    }
1072
1073    /// Add a ``registeredID [8]`` GeneralName.
1074    ///
1075    /// ``oid_comps`` is the list of arc components of the OID (e.g.
1076    /// ``[1, 3, 6, 1, 5, 5, 7, 3, 1]`` for id-kp-serverAuth).
1077    fn registered_id<'py>(
1078        slf: Bound<'py, Self>,
1079        oid_comps: Vec<u32>,
1080    ) -> PyResult<Bound<'py, Self>> {
1081        let oid = synta::ObjectIdentifier::new(&oid_comps)
1082            .map_err(|e| PyValueError::new_err(format!("invalid OID: {e}")))?;
1083        {
1084            let mut guard = slf.borrow_mut();
1085            let old = std::mem::replace(&mut guard.inner, IssuerAlternativeNameBuilder::new());
1086            guard.inner = old.registered_id(oid);
1087        }
1088        Ok(slf)
1089    }
1090
1091    /// Build the DER-encoded ``GeneralNames`` SEQUENCE.
1092    ///
1093    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
1094    /// entries have been added.  Raises :exc:`ValueError` if any entry failed
1095    /// DER encoding or if ``build()`` is called more than once on the same
1096    /// builder instance.
1097    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1098        if self.built {
1099            return Err(PyValueError::new_err(
1100                "IssuerAlternativeNameBuilder.build() has already been called; \
1101                 create a new builder instance",
1102            ));
1103        }
1104        self.built = true;
1105        let builder = std::mem::replace(&mut self.inner, IssuerAlternativeNameBuilder::new());
1106        let der = builder.build().map_err(PyValueError::new_err)?;
1107        Ok(PyBytes::new(py, &der))
1108    }
1109}
1110
1111// ── IssuingDistributionPointBuilder ───────────────────────────────────────────
1112
1113/// Fluent builder for an ``IssuingDistributionPoint`` extension value
1114/// (OID 2.5.29.28, RFC 5280 §5.2.5).
1115///
1116/// The ``IssuingDistributionPoint`` extension identifies the CRL distribution
1117/// point and scope for a particular CRL.  Configure the distribution point and
1118/// the boolean flags with the fluent methods, then call :meth:`build` to obtain
1119/// the DER bytes — the content that goes inside the OCTET STRING wrapper in the
1120/// ``Extension`` SEQUENCE.
1121///
1122/// The class is also available as the short alias ``synta.ext.IDP``.
1123///
1124/// ```python,ignore
1125/// import synta.ext as ext
1126///
1127/// idp = ext.IDP() \
1128///     .full_name_uri("http://crl.example.com/ca.crl") \
1129///     .only_contains_user_certs(True) \
1130///     .build()
1131/// ```
1132#[pyclass(name = "IssuingDistributionPointBuilder")]
1133pub struct PyIssuingDistributionPointBuilder {
1134    inner: IssuingDistributionPointBuilder,
1135    built: bool,
1136}
1137
1138#[pymethods]
1139impl PyIssuingDistributionPointBuilder {
1140    #[new]
1141    fn new() -> Self {
1142        Self {
1143            inner: IssuingDistributionPointBuilder::new(),
1144            built: false,
1145        }
1146    }
1147
1148    /// Set the distribution point to a URI full name.
1149    ///
1150    /// ``uri`` is an ASCII URI string (e.g. ``"http://crl.example.com/ca.crl"``).
1151    fn full_name_uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
1152        {
1153            let mut guard = slf.borrow_mut();
1154            let old = std::mem::replace(&mut guard.inner, IssuingDistributionPointBuilder::new());
1155            guard.inner = old.full_name_uri(uri);
1156        }
1157        slf
1158    }
1159
1160    /// Set the distribution point to a DNS name full name.
1161    fn full_name_dns<'py>(slf: Bound<'py, Self>, dns: &str) -> Bound<'py, Self> {
1162        {
1163            let mut guard = slf.borrow_mut();
1164            let old = std::mem::replace(&mut guard.inner, IssuingDistributionPointBuilder::new());
1165            guard.inner = old.full_name_dns(dns);
1166        }
1167        slf
1168    }
1169
1170    /// Set the ``onlyContainsUserCerts`` flag.
1171    ///
1172    /// When ``True``, the CRL only contains end-entity certificate revocations.
1173    fn only_contains_user_certs<'py>(slf: Bound<'py, Self>, val: bool) -> Bound<'py, Self> {
1174        {
1175            let mut guard = slf.borrow_mut();
1176            let old = std::mem::replace(&mut guard.inner, IssuingDistributionPointBuilder::new());
1177            guard.inner = old.only_contains_user_certs(val);
1178        }
1179        slf
1180    }
1181
1182    /// Set the ``onlyContainsCACerts`` flag.
1183    ///
1184    /// When ``True``, the CRL only contains CA certificate revocations.
1185    fn only_contains_cacerts<'py>(slf: Bound<'py, Self>, val: bool) -> Bound<'py, Self> {
1186        {
1187            let mut guard = slf.borrow_mut();
1188            let old = std::mem::replace(&mut guard.inner, IssuingDistributionPointBuilder::new());
1189            guard.inner = old.only_contains_cacerts(val);
1190        }
1191        slf
1192    }
1193
1194    /// Set the ``indirectCRL`` flag.
1195    ///
1196    /// When ``True``, the CRL may contain revocations for certificates issued
1197    /// by CAs other than the CRL issuer.
1198    fn indirect_crl<'py>(slf: Bound<'py, Self>, val: bool) -> Bound<'py, Self> {
1199        {
1200            let mut guard = slf.borrow_mut();
1201            let old = std::mem::replace(&mut guard.inner, IssuingDistributionPointBuilder::new());
1202            guard.inner = old.indirect_crl(val);
1203        }
1204        slf
1205    }
1206
1207    /// Set the ``onlyContainsAttributeCerts`` flag.
1208    ///
1209    /// When ``True``, the CRL only contains attribute certificate revocations.
1210    fn only_contains_attribute_certs<'py>(slf: Bound<'py, Self>, val: bool) -> Bound<'py, Self> {
1211        {
1212            let mut guard = slf.borrow_mut();
1213            let old = std::mem::replace(&mut guard.inner, IssuingDistributionPointBuilder::new());
1214            guard.inner = old.only_contains_attribute_certs(val);
1215        }
1216        slf
1217    }
1218
1219    /// Build the DER-encoded ``IssuingDistributionPoint`` SEQUENCE.
1220    ///
1221    /// Returns ``bytes``.  Raises :exc:`ValueError` if any entry failed DER
1222    /// encoding or if ``build()`` is called more than once on the same builder
1223    /// instance.
1224    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1225        if self.built {
1226            return Err(PyValueError::new_err(
1227                "IssuingDistributionPointBuilder.build() has already been called; \
1228                 create a new builder instance",
1229            ));
1230        }
1231        self.built = true;
1232        let builder = std::mem::replace(&mut self.inner, IssuingDistributionPointBuilder::new());
1233        let der = builder.build().map_err(PyValueError::new_err)?;
1234        Ok(PyBytes::new(py, &der))
1235    }
1236}
1237
1238// ── CertificatePoliciesBuilder ────────────────────────────────────────────────
1239
1240/// Fluent builder for a ``CertificatePolicies`` extension value (OID 2.5.29.32,
1241/// RFC 5280 §4.2.1.4).
1242///
1243/// Add policy entries with optional CPS URI qualifiers using the fluent methods,
1244/// then call :meth:`build` to obtain the DER bytes of the
1245/// ``CertificatePolicies`` SEQUENCE OF — the content that goes inside the OCTET
1246/// STRING wrapper in the ``Extension`` SEQUENCE.
1247///
1248/// The class is also available as the short alias ``synta.ext.CP``.
1249///
1250/// ```python,ignore
1251/// import synta.ext as ext
1252///
1253/// cp = ext.CP() \
1254///     .add_policy([2, 23, 140, 1, 2, 1]) \
1255///     .add_policy_cps([2, 23, 140, 1, 2, 2], "https://example.com/cps") \
1256///     .build()
1257/// ```
1258#[pyclass(name = "CertificatePoliciesBuilder")]
1259pub struct PyCertificatePoliciesBuilder {
1260    inner: CertificatePoliciesBuilder,
1261    built: bool,
1262}
1263
1264#[pymethods]
1265impl PyCertificatePoliciesBuilder {
1266    #[new]
1267    fn new() -> Self {
1268        Self {
1269            inner: CertificatePoliciesBuilder::new(),
1270            built: false,
1271        }
1272    }
1273
1274    /// Add a policy identified by OID arc components, with no qualifiers.
1275    ///
1276    /// ``oid_comps`` is the list of integer arc components of the policy OID
1277    /// (e.g. ``[2, 23, 140, 1, 2, 1]`` for
1278    /// ``2.23.140.1.2.1`` / ``baseline-requirements-domain-validated``).
1279    fn add_policy<'py>(slf: Bound<'py, Self>, oid_comps: Vec<u32>) -> Bound<'py, Self> {
1280        {
1281            let mut guard = slf.borrow_mut();
1282            let old = std::mem::replace(&mut guard.inner, CertificatePoliciesBuilder::new());
1283            guard.inner = old.add_policy(&oid_comps);
1284        }
1285        slf
1286    }
1287
1288    /// Add a policy with a CPS URI qualifier (``id-qt-cps``, 1.3.6.1.5.5.7.2.1).
1289    ///
1290    /// ``oid_comps`` is the list of integer arc components of the policy OID.
1291    /// ``cps_uri`` must be an ASCII URI string pointing to the Certification
1292    /// Practice Statement for this policy.
1293    fn add_policy_cps<'py>(
1294        slf: Bound<'py, Self>,
1295        oid_comps: Vec<u32>,
1296        cps_uri: &str,
1297    ) -> Bound<'py, Self> {
1298        {
1299            let mut guard = slf.borrow_mut();
1300            let old = std::mem::replace(&mut guard.inner, CertificatePoliciesBuilder::new());
1301            guard.inner = old.add_policy_cps(&oid_comps, cps_uri);
1302        }
1303        slf
1304    }
1305
1306    /// Build the DER-encoded ``CertificatePolicies`` SEQUENCE OF.
1307    ///
1308    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
1309    /// policies have been added.  Raises :exc:`ValueError` if any policy OID
1310    /// was invalid, the CPS URI contained non-ASCII characters, any entry
1311    /// failed DER encoding, or if ``build()`` is called more than once on the
1312    /// same builder instance.
1313    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1314        if self.built {
1315            return Err(PyValueError::new_err(
1316                "CertificatePoliciesBuilder.build() has already been called; \
1317                 create a new builder instance",
1318            ));
1319        }
1320        self.built = true;
1321        let builder = std::mem::replace(&mut self.inner, CertificatePoliciesBuilder::new());
1322        let der = builder.build().map_err(PyValueError::new_err)?;
1323        Ok(PyBytes::new(py, &der))
1324    }
1325}
1326
1327// ── Module registration ───────────────────────────────────────────────────────
1328
1329/// Register the ``synta.ext`` submodule.
1330///
1331/// Exposes DER builders for common X.509 v3 extension values together with
1332/// ``KEYID_*`` key-identifier method constants (RFC 5280 / RFC 7093) and
1333/// ``KU_*`` key-usage bitmask constants (RFC 5280 §4.2.1.3).
1334pub fn register_ext_module(parent: &Bound<'_, PyModule>) -> PyResult<()> {
1335    let py = parent.py();
1336    let m = PyModule::new(py, "ext")?;
1337
1338    // Key identifier method constants (RFC 5280 / RFC 7093)
1339    m.add("KEYID_RFC5280", KEYID_RFC5280)?;
1340    m.add("KEYID_RFC7093M1", KEYID_RFC7093M1)?;
1341    m.add("KEYID_RFC7093M2", KEYID_RFC7093M2)?;
1342    m.add("KEYID_RFC7093M3", KEYID_RFC7093M3)?;
1343    m.add("KEYID_RFC7093M4", KEYID_RFC7093M4)?;
1344
1345    // Key usage bitmask constants — OR these together and pass to key_usage()
1346    m.add("KU_DIGITAL_SIGNATURE", KU_DIGITAL_SIGNATURE)?;
1347    m.add("KU_NON_REPUDIATION", KU_NON_REPUDIATION)?;
1348    m.add("KU_KEY_ENCIPHERMENT", KU_KEY_ENCIPHERMENT)?;
1349    m.add("KU_DATA_ENCIPHERMENT", KU_DATA_ENCIPHERMENT)?;
1350    m.add("KU_KEY_AGREEMENT", KU_KEY_AGREEMENT)?;
1351    m.add("KU_KEY_CERT_SIGN", KU_KEY_CERT_SIGN)?;
1352    m.add("KU_CRL_SIGN", KU_CRL_SIGN)?;
1353    m.add("KU_ENCIPHER_ONLY", KU_ENCIPHER_ONLY)?;
1354    m.add("KU_DECIPHER_ONLY", KU_DECIPHER_ONLY)?;
1355
1356    // Extension builder functions
1357    m.add_function(wrap_pyfunction!(basic_constraints, &m)?)?;
1358    m.add_function(wrap_pyfunction!(crl_number, &m)?)?;
1359    m.add_function(wrap_pyfunction!(key_usage, &m)?)?;
1360    m.add_function(wrap_pyfunction!(subject_key_identifier, &m)?)?;
1361    m.add_function(wrap_pyfunction!(authority_key_identifier, &m)?)?;
1362
1363    // Fluent builder classes
1364    m.add_class::<PySubjectAlternativeNameBuilder>()?;
1365    m.add_class::<PyAuthorityInformationAccessBuilder>()?;
1366    m.add_class::<PyExtendedKeyUsageBuilder>()?;
1367    m.add_class::<PyNameConstraintsBuilder>()?;
1368    m.add_class::<PyCRLDistributionPointsBuilder>()?;
1369    m.add_class::<PyIssuerAlternativeNameBuilder>()?;
1370    m.add_class::<PyIssuingDistributionPointBuilder>()?;
1371    m.add_class::<PyCertificatePoliciesBuilder>()?;
1372
1373    // Short aliases for the builder classes
1374    m.add("SAN", m.getattr("SubjectAlternativeNameBuilder")?)?;
1375    m.add("AIA", m.getattr("AuthorityInformationAccessBuilder")?)?;
1376    m.add("EKU", m.getattr("ExtendedKeyUsageBuilder")?)?;
1377    m.add("NC", m.getattr("NameConstraintsBuilder")?)?;
1378    m.add("CDP", m.getattr("CRLDistributionPointsBuilder")?)?;
1379    m.add("IAN", m.getattr("IssuerAlternativeNameBuilder")?)?;
1380    m.add("IDP", m.getattr("IssuingDistributionPointBuilder")?)?;
1381    m.add("CP", m.getattr("CertificatePoliciesBuilder")?)?;
1382
1383    crate::install_submodule(
1384        parent,
1385        &m,
1386        "synta.ext",
1387        Some(
1388            "X.509 v3 extension value DER builders: basic_constraints, crl_number, key_usage, \
1389             subject_key_identifier, authority_key_identifier; \
1390             SubjectAlternativeNameBuilder (alias: SAN), \
1391             AuthorityInformationAccessBuilder (alias: AIA), \
1392             ExtendedKeyUsageBuilder (alias: EKU), \
1393             NameConstraintsBuilder (alias: NC), \
1394             CRLDistributionPointsBuilder (alias: CDP), \
1395             IssuerAlternativeNameBuilder (alias: IAN), \
1396             IssuingDistributionPointBuilder (alias: IDP), \
1397             CertificatePoliciesBuilder (alias: CP); \
1398             KEYID_* method constants (RFC 5280 §4.2.1.2 / RFC 7093); \
1399             KU_* key-usage bitmask constants (RFC 5280 §4.2.1.3).",
1400        ),
1401    )
1402}