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