Skip to main content

_synta/certificate/
name_builder.rs

1//! Python bindings for building X.509 Distinguished Names.
2//!
3//! Wraps [`synta_certificate::NameBuilder`], a fluent builder that accumulates
4//! ``AttributeTypeAndValue`` entries and serialises them into a DER-encoded
5//! ``Name`` SEQUENCE on demand.
6//!
7//! # Chaining
8//!
9//! Each setter returns the same ``NameBuilder`` object, enabling a
10//! single-expression build:
11//!
12//! ```python,ignore
13//! import synta
14//!
15//! name_der = (
16//!     synta.NameBuilder()
17//!     .country("US")
18//!     .organization("Example Corp")
19//!     .common_name("example.com")
20//!     .build()
21//! )
22//! ```
23
24use pyo3::exceptions::PyValueError;
25use pyo3::prelude::*;
26use pyo3::types::PyBytes;
27use synta_certificate::NameBuilder;
28
29// ── NameBuilder ───────────────────────────────────────────────────────────────
30
31/// Builder for DER-encoded X.509 ``Name`` SEQUENCE structures.
32///
33/// Constructs a ``Name`` by accumulating ``RelativeDistinguishedName`` (RDN)
34/// entries, each containing one ``AttributeTypeAndValue`` (ATV) with a
35/// ``UTF8String`` value.  Call :meth:`build` at the end to obtain the complete
36/// DER bytes.
37///
38/// The output is a complete DER ``Name`` TLV suitable for use with
39/// :class:`CertificateBuilder` and :class:`CsrBuilder` methods
40/// ``issuer_name``, ``subject_name``.
41///
42/// Each setter method returns the **same** ``NameBuilder`` object, so calls
43/// can be chained with ``.``:
44///
45/// ```python,ignore
46/// import synta
47///
48/// # CN only
49/// name_der = synta.NameBuilder().common_name("My Root CA").build()
50///
51/// # Full distinguished name
52/// name_der = (
53///     synta.NameBuilder()
54///     .country("US")
55///     .organization("Example Corp")
56///     .organizational_unit("Engineering")
57///     .common_name("example.com")
58///     .build()
59/// )
60///
61/// # Use with CertificateBuilder:
62/// cert = (
63///     synta.CertificateBuilder()
64///     .issuer_name(
65///         synta.NameBuilder().common_name("Test CA").build()
66///     )
67///     .subject_name(
68///         synta.NameBuilder().common_name("Test Leaf").build()
69///     )
70///     ...
71/// )
72/// ```
73#[pyclass(name = "NameBuilder")]
74pub struct PyNameBuilder {
75    inner: NameBuilder,
76}
77
78#[pymethods]
79impl PyNameBuilder {
80    /// Create a new, empty ``NameBuilder``.
81    #[new]
82    fn new() -> Self {
83        Self {
84            inner: NameBuilder::new(),
85        }
86    }
87
88    /// Add an attribute with a dotted-decimal OID string and a UTF-8 string value.
89    ///
90    /// Use the constants in ``synta.oids.attr`` for well-known DN attribute types,
91    /// or pass any OID string for custom attributes.
92    ///
93    /// ```python,ignore
94    /// import synta
95    /// import synta.oids as oids
96    ///
97    /// name_der = (
98    ///     synta.NameBuilder()
99    ///     .add_attr(str(oids.attr.ORGANIZATION), "Example Corp")
100    ///     .add_attr(str(oids.attr.COMMON_NAME), "example.com")
101    ///     .build()
102    /// )
103    /// ```
104    fn add_attr<'py>(slf: Bound<'py, Self>, oid: &str, value: &str) -> PyResult<Bound<'py, Self>> {
105        use std::str::FromStr;
106        let parsed = synta::ObjectIdentifier::from_str(oid)
107            .map_err(|_| PyValueError::new_err(format!("invalid OID: {oid}")))?;
108        let comps = parsed.components().to_vec();
109        // Replace inner via a consuming take-and-rebuild pattern.
110        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
111        slf.borrow_mut().inner = old.add_attr(&comps, value);
112        Ok(slf)
113    }
114
115    /// Add a ``commonName`` (2.5.4.3) attribute.
116    ///
117    /// ```python,ignore
118    /// name_der = synta.NameBuilder().common_name("example.com").build()
119    /// ```
120    fn common_name<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
121        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
122        slf.borrow_mut().inner = old.common_name(value);
123        slf
124    }
125
126    /// Add an ``organizationName`` (2.5.4.10) attribute.
127    ///
128    /// ```python,ignore
129    /// name_der = synta.NameBuilder().organization("Example Corp").build()
130    /// ```
131    fn organization<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
132        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
133        slf.borrow_mut().inner = old.organization(value);
134        slf
135    }
136
137    /// Add an ``organizationalUnitName`` (2.5.4.11) attribute.
138    ///
139    /// ```python,ignore
140    /// name_der = synta.NameBuilder().organizational_unit("Engineering").build()
141    /// ```
142    fn organizational_unit<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
143        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
144        slf.borrow_mut().inner = old.organizational_unit(value);
145        slf
146    }
147
148    /// Add a ``countryName`` (2.5.4.6) attribute.
149    ///
150    /// The value should be a two-letter ISO 3166-1 country code (e.g. ``"US"``).
151    ///
152    /// ```python,ignore
153    /// name_der = synta.NameBuilder().country("US").build()
154    /// ```
155    fn country<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
156        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
157        slf.borrow_mut().inner = old.country(value);
158        slf
159    }
160
161    /// Add a ``stateOrProvinceName`` (2.5.4.8) attribute.
162    ///
163    /// ```python,ignore
164    /// name_der = synta.NameBuilder().state("California").build()
165    /// ```
166    fn state<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
167        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
168        slf.borrow_mut().inner = old.state(value);
169        slf
170    }
171
172    /// Add a ``localityName`` (2.5.4.7) attribute.
173    ///
174    /// ```python,ignore
175    /// name_der = synta.NameBuilder().locality("San Francisco").build()
176    /// ```
177    fn locality<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
178        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
179        slf.borrow_mut().inner = old.locality(value);
180        slf
181    }
182
183    /// Add a ``streetAddress`` (2.5.4.9) attribute.
184    ///
185    /// ```python,ignore
186    /// name_der = synta.NameBuilder().street("123 Main St").build()
187    /// ```
188    fn street<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
189        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
190        slf.borrow_mut().inner = old.street(value);
191        slf
192    }
193
194    /// Add an ``emailAddress`` (1.2.840.113549.1.9.1) attribute.
195    ///
196    /// ```python,ignore
197    /// name_der = synta.NameBuilder().email_address("admin@example.com").build()
198    /// ```
199    fn email_address<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
200        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
201        slf.borrow_mut().inner = old.email_address(value);
202        slf
203    }
204
205    /// Build and return the DER-encoded X.509 ``Name`` SEQUENCE as ``bytes``.
206    ///
207    /// Returns the complete TLV bytes including the outer ``SEQUENCE`` tag and
208    /// length.  An empty ``NameBuilder`` returns a DER-encoded empty
209    /// ``SEQUENCE`` (``b'\\x30\\x00'``).
210    ///
211    /// ```python,ignore
212    /// import synta
213    ///
214    /// name_der = synta.NameBuilder().common_name("Root CA").build()
215    ///
216    /// # Use directly with CertificateBuilder:
217    /// cert = (
218    ///     synta.CertificateBuilder()
219    ///     .issuer_name(name_der)
220    ///     .subject_name(name_der)
221    ///     ...
222    /// )
223    /// ```
224    fn build<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
225        let der = self
226            .inner
227            .build()
228            .map_err(pyo3::exceptions::PyValueError::new_err)?;
229        Ok(PyBytes::new(py, &der))
230    }
231
232    fn __repr__(&self) -> String {
233        "NameBuilder()".to_string()
234    }
235}