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