_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}