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