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//! Three 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
47use pyo3::exceptions::PyValueError;
48use pyo3::prelude::*;
49use pyo3::types::PyBytes;
50
51use synta_certificate::{
52    default_key_id_hasher, encode_authority_key_identifier, encode_basic_constraints,
53    encode_key_usage, encode_subject_key_identifier, AuthorityInformationAccessBuilder,
54    ExtendedKeyUsageBuilder, KeyIdMethod, SubjectAlternativeNameBuilder,
55};
56
57// ── Key identifier method constants ──────────────────────────────────────────
58
59/// RFC 5280 §4.2.1.2: SHA-1 of the BIT STRING value of ``subjectPublicKey``.
60pub const KEYID_RFC5280: u8 = 0;
61
62/// RFC 7093 §2 method 1: SHA-256 of BIT STRING value, truncated to 160 bits.
63pub const KEYID_RFC7093M1: u8 = 1;
64
65/// RFC 7093 §2 method 2: SHA-384 of BIT STRING value, truncated to 160 bits.
66pub const KEYID_RFC7093M2: u8 = 2;
67
68/// RFC 7093 §2 method 3: SHA-512 of BIT STRING value, truncated to 160 bits.
69pub const KEYID_RFC7093M3: u8 = 3;
70
71/// RFC 7093 §2 method 4: SHA-256 of the full ``SubjectPublicKeyInfo`` DER encoding.
72pub const KEYID_RFC7093M4: u8 = 4;
73
74// ── Key usage bitmask constants ───────────────────────────────────────────────
75
76/// ``digitalSignature`` (bit 0) — RFC 5280 §4.2.1.3.
77pub const KU_DIGITAL_SIGNATURE: u16 = 1 << 0;
78/// ``contentCommitment`` / ``nonRepudiation`` (bit 1) — RFC 5280 §4.2.1.3.
79pub const KU_NON_REPUDIATION: u16 = 1 << 1;
80/// ``keyEncipherment`` (bit 2) — RFC 5280 §4.2.1.3.
81pub const KU_KEY_ENCIPHERMENT: u16 = 1 << 2;
82/// ``dataEncipherment`` (bit 3) — RFC 5280 §4.2.1.3.
83pub const KU_DATA_ENCIPHERMENT: u16 = 1 << 3;
84/// ``keyAgreement`` (bit 4) — RFC 5280 §4.2.1.3.
85pub const KU_KEY_AGREEMENT: u16 = 1 << 4;
86/// ``keyCertSign`` (bit 5) — RFC 5280 §4.2.1.3.
87pub const KU_KEY_CERT_SIGN: u16 = 1 << 5;
88/// ``cRLSign`` (bit 6) — RFC 5280 §4.2.1.3.
89pub const KU_CRL_SIGN: u16 = 1 << 6;
90/// ``encipherOnly`` (bit 7) — RFC 5280 §4.2.1.3; only meaningful when keyAgreement is set.
91pub const KU_ENCIPHER_ONLY: u16 = 1 << 7;
92/// ``decipherOnly`` (bit 8) — RFC 5280 §4.2.1.3; only meaningful when keyAgreement is set.
93pub const KU_DECIPHER_ONLY: u16 = 1 << 8;
94
95// ── Internal helper ───────────────────────────────────────────────────────────
96
97fn method_from_int(m: u8) -> PyResult<KeyIdMethod> {
98    match m {
99        KEYID_RFC5280 => Ok(KeyIdMethod::Rfc5280Sha1),
100        KEYID_RFC7093M1 => Ok(KeyIdMethod::Rfc7093Method1Sha256),
101        KEYID_RFC7093M2 => Ok(KeyIdMethod::Rfc7093Method2Sha384),
102        KEYID_RFC7093M3 => Ok(KeyIdMethod::Rfc7093Method3Sha512),
103        KEYID_RFC7093M4 => Ok(KeyIdMethod::Rfc7093Method4 {
104            algorithm_oid: synta_certificate::ID_SHA256.to_vec(),
105        }),
106        other => Err(PyValueError::new_err(format!(
107            "unknown key identifier method {other}: \
108             use KEYID_RFC5280, KEYID_RFC7093M1, KEYID_RFC7093M2, \
109             KEYID_RFC7093M3, or KEYID_RFC7093M4"
110        ))),
111    }
112}
113
114// ── Python functions ──────────────────────────────────────────────────────────
115
116/// Encode a ``BasicConstraints`` extension value (OID 2.5.29.19).
117///
118/// Returns the DER bytes of the ``BasicConstraints`` SEQUENCE for use as the
119/// extension ``extnValue`` (inside the OCTET STRING wrapper):
120///
121/// ```text
122/// BasicConstraints ::= SEQUENCE {
123///     cA                  BOOLEAN DEFAULT FALSE,
124///     pathLenConstraint   INTEGER (0..MAX) OPTIONAL }
125/// ```
126///
127/// When ``ca`` is ``False`` the ``cA`` field is omitted (DER DEFAULT-FALSE
128/// rule).  ``path_length`` is silently ignored when ``ca`` is ``False``.
129///
130/// ```python,ignore
131/// import synta.ext as ext
132///
133/// # End-entity certificate: empty SEQUENCE (30 00)
134/// ee_bc = ext.basic_constraints()
135/// assert ee_bc == bytes([0x30, 0x00])
136///
137/// # CA with no path-length constraint (30 03 01 01 ff)
138/// ca_bc = ext.basic_constraints(ca=True)
139///
140/// # CA with pathLen = 0 (30 06 01 01 ff 02 01 00)
141/// ca_pl0 = ext.basic_constraints(ca=True, path_length=0)
142/// ```
143#[pyfunction]
144#[pyo3(signature = (ca = false, path_length = None))]
145fn basic_constraints<'py>(
146    py: Python<'py>,
147    ca: bool,
148    path_length: Option<u64>,
149) -> PyResult<Bound<'py, PyBytes>> {
150    let der = encode_basic_constraints(ca, path_length)
151        .ok_or_else(|| PyValueError::new_err("BasicConstraints DER encoding failed"))?;
152    Ok(PyBytes::new(py, &der))
153}
154
155/// Encode a ``KeyUsage`` extension value (OID 2.5.29.15).
156///
157/// ``bits`` is an integer bitmask formed by OR-ing the ``KU_*`` constants
158/// exported from this module.  Returns the DER BIT STRING value.
159///
160/// ```python,ignore
161/// import synta.ext as ext
162///
163/// # digitalSignature only → 03 02 07 80
164/// ku = ext.key_usage(ext.KU_DIGITAL_SIGNATURE)
165///
166/// # keyCertSign | cRLSign → 03 02 01 06
167/// ku = ext.key_usage(ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN)
168///
169/// # Typical CA key usage
170/// ku = ext.key_usage(
171///     ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN | ext.KU_DIGITAL_SIGNATURE
172/// )
173/// ```
174#[pyfunction]
175fn key_usage<'py>(py: Python<'py>, bits: u16) -> PyResult<Bound<'py, PyBytes>> {
176    let der = encode_key_usage(bits)
177        .ok_or_else(|| PyValueError::new_err("KeyUsage DER encoding failed"))?;
178    Ok(PyBytes::new(py, &der))
179}
180
181/// Encode a ``SubjectKeyIdentifier`` extension value (OID 2.5.29.14).
182///
183/// Decodes ``spki_der`` as a DER ``SubjectPublicKeyInfo`` and computes the
184/// key identifier according to ``method`` (one of the ``KEYID_*`` constants
185/// from this module).
186///
187/// Methods other than ``KEYID_RFC7093M4`` hash the BIT STRING value of
188/// ``subjectPublicKey`` (the key bytes, without the unused-bits octet).
189/// ``KEYID_RFC7093M4`` hashes the full ``SubjectPublicKeyInfo`` DER encoding.
190///
191/// Returns the DER OCTET STRING value, or raises :exc:`ValueError` if
192/// ``spki_der`` cannot be decoded or if hashing fails.
193///
194/// ```python,ignore
195/// import synta.ext as ext
196///
197/// # RFC 5280 default (SHA-1 of subjectPublicKey BIT STRING value)
198/// ski_der = ext.subject_key_identifier(cert.subject_public_key_info_raw_der)
199///
200/// # RFC 7093 method 1 (SHA-256 of BIT STRING value, truncated to 160 bits)
201/// ski_der = ext.subject_key_identifier(
202///     cert.subject_public_key_info_raw_der,
203///     method=ext.KEYID_RFC7093M1,
204/// )
205/// ```
206#[pyfunction]
207#[pyo3(signature = (spki_der, method = KEYID_RFC5280))]
208fn subject_key_identifier<'py>(
209    py: Python<'py>,
210    spki_der: &[u8],
211    method: u8,
212) -> PyResult<Bound<'py, PyBytes>> {
213    let m = method_from_int(method)?;
214    encode_subject_key_identifier(spki_der, m, &default_key_id_hasher())
215        .map(|v| PyBytes::new(py, &v))
216        .ok_or_else(|| {
217            PyValueError::new_err(
218                "failed to encode SubjectKeyIdentifier: \
219                 invalid SubjectPublicKeyInfo DER or hash error",
220            )
221        })
222}
223
224/// Encode an ``AuthorityKeyIdentifier`` extension value (OID 2.5.29.35).
225///
226/// Computes the key identifier from the issuer's ``SubjectPublicKeyInfo``
227/// using the same input selection and hash rules as
228/// :func:`subject_key_identifier`.  Sets only the ``keyIdentifier [0]``
229/// field; ``authorityCertIssuer`` and ``authorityCertSerialNumber`` are not
230/// included.
231///
232/// Returns the DER bytes of the ``AuthorityKeyIdentifier`` SEQUENCE, or
233/// raises :exc:`ValueError` on error.
234///
235/// ```python,ignore
236/// import synta.ext as ext
237///
238/// # RFC 5280 default (SHA-1 of issuer subjectPublicKey BIT STRING value)
239/// aki_der = ext.authority_key_identifier(ca_cert.subject_public_key_info_raw_der)
240///
241/// # RFC 7093 method 1 (SHA-256 of BIT STRING value, truncated to 160 bits)
242/// aki_der = ext.authority_key_identifier(
243///     ca_cert.subject_public_key_info_raw_der,
244///     method=ext.KEYID_RFC7093M1,
245/// )
246/// ```
247#[pyfunction]
248#[pyo3(signature = (issuer_spki_der, method = KEYID_RFC5280))]
249fn authority_key_identifier<'py>(
250    py: Python<'py>,
251    issuer_spki_der: &[u8],
252    method: u8,
253) -> PyResult<Bound<'py, PyBytes>> {
254    let m = method_from_int(method)?;
255    encode_authority_key_identifier(issuer_spki_der, m, &default_key_id_hasher())
256        .map(|v| PyBytes::new(py, &v))
257        .ok_or_else(|| {
258            PyValueError::new_err(
259                "failed to encode AuthorityKeyIdentifier: \
260                 invalid SubjectPublicKeyInfo DER or hash error",
261            )
262        })
263}
264
265// ── SubjectAlternativeNameBuilder ─────────────────────────────────────────────
266
267/// Fluent builder for a ``SubjectAltName`` extension value (OID 2.5.29.17).
268///
269/// Accumulate :class:`GeneralName` entries with the fluent methods, then call
270/// :meth:`build` to obtain the DER bytes of the ``GeneralNames`` SEQUENCE —
271/// the content that goes inside the OCTET STRING wrapper in the ``Extension``
272/// SEQUENCE.
273///
274/// The class is also available as the short alias ``synta.ext.SAN``.
275///
276/// ```python,ignore
277/// import synta.ext as ext
278///
279/// san = ext.SAN() \
280///     .dns_name("www.example.com") \
281///     .dns_name("example.com") \
282///     .rfc822_name("admin@example.com") \
283///     .ip_address(b"\xc0\xa8\x01\x01") \
284///     .build()
285/// ```
286#[pyclass(name = "SubjectAlternativeNameBuilder")]
287pub struct PySubjectAlternativeNameBuilder {
288    inner: SubjectAlternativeNameBuilder,
289    built: bool,
290}
291
292#[pymethods]
293impl PySubjectAlternativeNameBuilder {
294    #[new]
295    fn new() -> Self {
296        Self {
297            inner: SubjectAlternativeNameBuilder::new(),
298            built: false,
299        }
300    }
301
302    /// Add a ``dNSName [2]`` GeneralName.
303    fn dns_name<'py>(slf: Bound<'py, Self>, name: &str) -> Bound<'py, Self> {
304        let old = std::mem::replace(
305            &mut slf.borrow_mut().inner,
306            SubjectAlternativeNameBuilder::new(),
307        );
308        slf.borrow_mut().inner = old.dns_name(name);
309        slf
310    }
311
312    /// Add an ``rfc822Name [1]`` GeneralName (email address).
313    fn rfc822_name<'py>(slf: Bound<'py, Self>, email: &str) -> Bound<'py, Self> {
314        let old = std::mem::replace(
315            &mut slf.borrow_mut().inner,
316            SubjectAlternativeNameBuilder::new(),
317        );
318        slf.borrow_mut().inner = old.rfc822_name(email);
319        slf
320    }
321
322    /// Add a ``uniformResourceIdentifier [6]`` GeneralName (URI).
323    fn uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
324        let old = std::mem::replace(
325            &mut slf.borrow_mut().inner,
326            SubjectAlternativeNameBuilder::new(),
327        );
328        slf.borrow_mut().inner = old.uri(uri);
329        slf
330    }
331
332    /// Add an ``iPAddress [7]`` GeneralName.
333    ///
334    /// Pass 4 bytes for IPv4 or 16 bytes for IPv6.
335    fn ip_address<'py>(slf: Bound<'py, Self>, addr: &[u8]) -> Bound<'py, Self> {
336        let old = std::mem::replace(
337            &mut slf.borrow_mut().inner,
338            SubjectAlternativeNameBuilder::new(),
339        );
340        slf.borrow_mut().inner = old.ip_address(addr);
341        slf
342    }
343
344    /// Add a ``directoryName [4]`` GeneralName (EXPLICIT wrapper).
345    ///
346    /// ``name_der`` is the complete DER TLV bytes of the ``Name`` SEQUENCE
347    /// (e.g. from :meth:`synta.NameBuilder.build`).  Silently ignored if
348    /// ``name_der`` cannot be decoded.
349    fn directory_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
350        let old = std::mem::replace(
351            &mut slf.borrow_mut().inner,
352            SubjectAlternativeNameBuilder::new(),
353        );
354        slf.borrow_mut().inner = old.directory_name(name_der);
355        slf
356    }
357
358    /// Build the DER-encoded ``GeneralNames`` SEQUENCE.
359    ///
360    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
361    /// entries have been added.  Raises :exc:`ValueError` if any entry failed
362    /// DER encoding or if ``build()`` is called more than once on the same
363    /// builder instance.
364    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
365        if self.built {
366            return Err(PyValueError::new_err(
367                "SubjectAlternativeNameBuilder.build() has already been called; \
368                 create a new builder instance",
369            ));
370        }
371        self.built = true;
372        let builder = std::mem::replace(&mut self.inner, SubjectAlternativeNameBuilder::new());
373        let der = builder.build().map_err(PyValueError::new_err)?;
374        Ok(PyBytes::new(py, &der))
375    }
376}
377
378// ── AuthorityInformationAccessBuilder ─────────────────────────────────────────
379
380/// Fluent builder for an ``AuthorityInfoAccessSyntax`` extension value
381/// (OID 1.3.6.1.5.5.7.1.1, Authority Information Access, RFC 5280 §4.2.2.1).
382///
383/// Add OCSP and/or CA Issuers URIs with the fluent methods, then call
384/// :meth:`build` to obtain the DER bytes of the ``AuthorityInfoAccessSyntax``
385/// SEQUENCE — the content that goes inside the OCTET STRING wrapper.
386///
387/// The class is also available as the short alias ``synta.ext.AIA``.
388///
389/// ```python,ignore
390/// import synta.ext as ext
391///
392/// aia = ext.AIA() \
393///     .ocsp("http://ocsp.example.com") \
394///     .ca_issuers("http://ca.example.com/ca.crt") \
395///     .build()
396/// ```
397#[pyclass(name = "AuthorityInformationAccessBuilder")]
398pub struct PyAuthorityInformationAccessBuilder {
399    inner: AuthorityInformationAccessBuilder,
400    built: bool,
401}
402
403#[pymethods]
404impl PyAuthorityInformationAccessBuilder {
405    #[new]
406    fn new() -> Self {
407        Self {
408            inner: AuthorityInformationAccessBuilder::new(),
409            built: false,
410        }
411    }
412
413    /// Add an OCSP responder URI (``id-ad-ocsp``, 1.3.6.1.5.5.7.48.1).
414    fn ocsp<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
415        let old = std::mem::replace(
416            &mut slf.borrow_mut().inner,
417            AuthorityInformationAccessBuilder::new(),
418        );
419        slf.borrow_mut().inner = old.ocsp(uri);
420        slf
421    }
422
423    /// Add a CA Issuers URI (``id-ad-caIssuers``, 1.3.6.1.5.5.7.48.2).
424    fn ca_issuers<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
425        let old = std::mem::replace(
426            &mut slf.borrow_mut().inner,
427            AuthorityInformationAccessBuilder::new(),
428        );
429        slf.borrow_mut().inner = old.ca_issuers(uri);
430        slf
431    }
432
433    /// Build the DER-encoded ``AuthorityInfoAccessSyntax`` SEQUENCE.
434    ///
435    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
436    /// entries have been added.  Raises :exc:`ValueError` if any entry failed
437    /// DER encoding or if ``build()`` is called more than once on the same
438    /// builder instance.
439    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
440        if self.built {
441            return Err(PyValueError::new_err(
442                "AuthorityInformationAccessBuilder.build() has already been called; \
443                 create a new builder instance",
444            ));
445        }
446        self.built = true;
447        let builder = std::mem::replace(&mut self.inner, AuthorityInformationAccessBuilder::new());
448        let der = builder.build().map_err(PyValueError::new_err)?;
449        Ok(PyBytes::new(py, &der))
450    }
451}
452
453// ── ExtendedKeyUsageBuilder ───────────────────────────────────────────────────
454
455/// Fluent builder for an ``ExtKeyUsageSyntax`` extension value
456/// (OID 2.5.29.37, Extended Key Usage, RFC 5280 §4.2.1.12).
457///
458/// Add well-known key purpose OIDs with the convenience methods or custom OIDs
459/// via :meth:`add_oid`, then call :meth:`build` to obtain the DER bytes of the
460/// ``ExtKeyUsageSyntax`` SEQUENCE.
461///
462/// The class is also available as the short alias ``synta.ext.EKU``.
463///
464/// ```python,ignore
465/// import synta.ext as ext
466///
467/// # Typical TLS server certificate EKU
468/// eku = ext.EKU().server_auth().client_auth().build()
469///
470/// # Custom OID
471/// eku = ext.EKU().add_oid([1, 3, 6, 1, 5, 5, 7, 3, 1]).build()
472/// ```
473#[pyclass(name = "ExtendedKeyUsageBuilder")]
474pub struct PyExtendedKeyUsageBuilder {
475    inner: ExtendedKeyUsageBuilder,
476    built: bool,
477}
478
479#[pymethods]
480impl PyExtendedKeyUsageBuilder {
481    #[new]
482    fn new() -> Self {
483        Self {
484            inner: ExtendedKeyUsageBuilder::new(),
485            built: false,
486        }
487    }
488
489    /// Add ``id-kp-serverAuth`` (1.3.6.1.5.5.7.3.1).
490    fn server_auth<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
491        let old = std::mem::replace(&mut slf.borrow_mut().inner, ExtendedKeyUsageBuilder::new());
492        slf.borrow_mut().inner = old.server_auth();
493        slf
494    }
495
496    /// Add ``id-kp-clientAuth`` (1.3.6.1.5.5.7.3.2).
497    fn client_auth<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
498        let old = std::mem::replace(&mut slf.borrow_mut().inner, ExtendedKeyUsageBuilder::new());
499        slf.borrow_mut().inner = old.client_auth();
500        slf
501    }
502
503    /// Add ``id-kp-codeSigning`` (1.3.6.1.5.5.7.3.3).
504    fn code_signing<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
505        let old = std::mem::replace(&mut slf.borrow_mut().inner, ExtendedKeyUsageBuilder::new());
506        slf.borrow_mut().inner = old.code_signing();
507        slf
508    }
509
510    /// Add ``id-kp-emailProtection`` (1.3.6.1.5.5.7.3.4).
511    fn email_protection<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
512        let old = std::mem::replace(&mut slf.borrow_mut().inner, ExtendedKeyUsageBuilder::new());
513        slf.borrow_mut().inner = old.email_protection();
514        slf
515    }
516
517    /// Add ``id-kp-timeStamping`` (1.3.6.1.5.5.7.3.8).
518    fn time_stamping<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
519        let old = std::mem::replace(&mut slf.borrow_mut().inner, ExtendedKeyUsageBuilder::new());
520        slf.borrow_mut().inner = old.time_stamping();
521        slf
522    }
523
524    /// Add ``id-kp-OCSPSigning`` (1.3.6.1.5.5.7.3.9).
525    fn ocsp_signing<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
526        let old = std::mem::replace(&mut slf.borrow_mut().inner, ExtendedKeyUsageBuilder::new());
527        slf.borrow_mut().inner = old.ocsp_signing();
528        slf
529    }
530
531    /// Add a custom key purpose OID by component list.
532    ///
533    /// ``comps`` is a list of integers forming the OID arc, e.g.
534    /// ``[1, 3, 6, 1, 5, 5, 7, 3, 1]`` for ``id-kp-serverAuth``.
535    fn add_oid<'py>(slf: Bound<'py, Self>, comps: Vec<u32>) -> Bound<'py, Self> {
536        let old = std::mem::replace(&mut slf.borrow_mut().inner, ExtendedKeyUsageBuilder::new());
537        slf.borrow_mut().inner = old.add_oid(&comps);
538        slf
539    }
540
541    /// Build the DER-encoded ``ExtKeyUsageSyntax`` SEQUENCE.
542    ///
543    /// Returns ``bytes``.  Returns ``b'\\x30\\x00'`` (empty SEQUENCE) if no
544    /// OIDs have been added.  Raises :exc:`ValueError` if any OID was invalid
545    /// or if ``build()`` is called more than once on the same builder instance.
546    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
547        if self.built {
548            return Err(PyValueError::new_err(
549                "ExtendedKeyUsageBuilder.build() has already been called; \
550                 create a new builder instance",
551            ));
552        }
553        self.built = true;
554        let builder = std::mem::replace(&mut self.inner, ExtendedKeyUsageBuilder::new());
555        let der = builder.build().map_err(PyValueError::new_err)?;
556        Ok(PyBytes::new(py, &der))
557    }
558}
559
560// ── Module registration ───────────────────────────────────────────────────────
561
562/// Register the ``synta.ext`` submodule.
563///
564/// Exposes DER builders for common X.509 v3 extension values together with
565/// ``KEYID_*`` key-identifier method constants (RFC 5280 / RFC 7093) and
566/// ``KU_*`` key-usage bitmask constants (RFC 5280 §4.2.1.3).
567pub fn register_ext_module(parent: &Bound<'_, PyModule>) -> PyResult<()> {
568    let py = parent.py();
569    let m = PyModule::new(py, "ext")?;
570
571    // Key identifier method constants (RFC 5280 / RFC 7093)
572    m.add("KEYID_RFC5280", KEYID_RFC5280)?;
573    m.add("KEYID_RFC7093M1", KEYID_RFC7093M1)?;
574    m.add("KEYID_RFC7093M2", KEYID_RFC7093M2)?;
575    m.add("KEYID_RFC7093M3", KEYID_RFC7093M3)?;
576    m.add("KEYID_RFC7093M4", KEYID_RFC7093M4)?;
577
578    // Key usage bitmask constants — OR these together and pass to key_usage()
579    m.add("KU_DIGITAL_SIGNATURE", KU_DIGITAL_SIGNATURE)?;
580    m.add("KU_NON_REPUDIATION", KU_NON_REPUDIATION)?;
581    m.add("KU_KEY_ENCIPHERMENT", KU_KEY_ENCIPHERMENT)?;
582    m.add("KU_DATA_ENCIPHERMENT", KU_DATA_ENCIPHERMENT)?;
583    m.add("KU_KEY_AGREEMENT", KU_KEY_AGREEMENT)?;
584    m.add("KU_KEY_CERT_SIGN", KU_KEY_CERT_SIGN)?;
585    m.add("KU_CRL_SIGN", KU_CRL_SIGN)?;
586    m.add("KU_ENCIPHER_ONLY", KU_ENCIPHER_ONLY)?;
587    m.add("KU_DECIPHER_ONLY", KU_DECIPHER_ONLY)?;
588
589    // Extension builder functions
590    m.add_function(wrap_pyfunction!(basic_constraints, &m)?)?;
591    m.add_function(wrap_pyfunction!(key_usage, &m)?)?;
592    m.add_function(wrap_pyfunction!(subject_key_identifier, &m)?)?;
593    m.add_function(wrap_pyfunction!(authority_key_identifier, &m)?)?;
594
595    // Fluent builder classes
596    m.add_class::<PySubjectAlternativeNameBuilder>()?;
597    m.add_class::<PyAuthorityInformationAccessBuilder>()?;
598    m.add_class::<PyExtendedKeyUsageBuilder>()?;
599
600    // Short aliases for the builder classes
601    m.add("SAN", m.getattr("SubjectAlternativeNameBuilder")?)?;
602    m.add("AIA", m.getattr("AuthorityInformationAccessBuilder")?)?;
603    m.add("EKU", m.getattr("ExtendedKeyUsageBuilder")?)?;
604
605    crate::install_submodule(
606        parent,
607        &m,
608        "synta.ext",
609        Some(
610            "X.509 v3 extension value DER builders: basic_constraints, key_usage, \
611             subject_key_identifier, authority_key_identifier; \
612             SubjectAlternativeNameBuilder (alias: SAN), \
613             AuthorityInformationAccessBuilder (alias: AIA), \
614             ExtendedKeyUsageBuilder (alias: EKU); \
615             KEYID_* method constants (RFC 5280 §4.2.1.2 / RFC 7093); \
616             KU_* key-usage bitmask constants (RFC 5280 §4.2.1.3).",
617        ),
618    )
619}