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