Skip to main content

_synta/certificate/
general_name.rs

1//! Typed Python classes for each `GeneralName` variant (RFC 5280 §4.2.1.6).
2//!
3//! Each variant of the ASN.1 `GeneralName` CHOICE is represented as a
4//! separate frozen pyclass in the `synta.general_name` submodule.  These
5//! types are constructed by [`py_from_general_name`], which is called from
6//! the Python binding whenever typed `GeneralName` objects are needed.
7
8use pyo3::prelude::*;
9use pyo3::types::{PyBytes, PyList, PyModule};
10
11use crate::error::SyntaErr;
12use crate::types::PyObjectIdentifier;
13
14// ── OtherName ─────────────────────────────────────────────────────────────────
15
16/// An `otherName [0]` GeneralName.
17///
18/// Carries an arbitrary typed value identified by ``type_id`` (the OID) and
19/// ``value`` (the full DER TLV of the ``[0] EXPLICIT ANY`` field).
20#[pyclass(frozen, name = "OtherName", module = "synta.general_name")]
21pub struct PyOtherName {
22    pub type_id: synta::ObjectIdentifier,
23    /// DER bytes of the value Element (tag + length + contents).
24    pub value: Vec<u8>,
25}
26
27#[pymethods]
28impl PyOtherName {
29    /// OID identifying the OtherName type.
30    #[getter]
31    fn type_id<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
32        Py::new(py, PyObjectIdentifier::from_oid(self.type_id.clone())).map(|p| p.into_bound(py))
33    }
34
35    /// Raw DER bytes of the value field (tag + length + value).
36    #[getter]
37    fn value<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
38        PyBytes::new(py, &self.value)
39    }
40
41    fn __repr__(&self) -> String {
42        format!("OtherName(type_id={})", self.type_id)
43    }
44}
45
46// ── RFC822Name ────────────────────────────────────────────────────────────────
47
48/// An `rfc822Name [1]` GeneralName (e-mail address).
49#[pyclass(frozen, name = "RFC822Name", module = "synta.general_name")]
50pub struct PyRfc822Name {
51    pub value: String,
52}
53
54#[pymethods]
55impl PyRfc822Name {
56    /// The e-mail address string.
57    #[getter]
58    fn value(&self) -> &str {
59        &self.value
60    }
61
62    fn __repr__(&self) -> String {
63        format!("RFC822Name(value={:?})", self.value)
64    }
65}
66
67// ── DNSName ───────────────────────────────────────────────────────────────────
68
69/// A `dNSName [2]` GeneralName (DNS host name).
70#[pyclass(frozen, name = "DNSName", module = "synta.general_name")]
71pub struct PyDnsName {
72    pub value: String,
73}
74
75#[pymethods]
76impl PyDnsName {
77    /// The DNS name string.
78    #[getter]
79    fn value(&self) -> &str {
80        &self.value
81    }
82
83    fn __repr__(&self) -> String {
84        format!("DNSName(value={:?})", self.value)
85    }
86}
87
88// ── X400Address ───────────────────────────────────────────────────────────────
89
90/// An `x400Address [3]` GeneralName (raw DER, unusual in practice).
91#[pyclass(frozen, name = "X400Address", module = "synta.general_name")]
92pub struct PyX400Address {
93    pub raw_der: Vec<u8>,
94}
95
96#[pymethods]
97impl PyX400Address {
98    /// Raw DER bytes of the ORAddress value.
99    #[getter]
100    fn raw_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
101        PyBytes::new(py, &self.raw_der)
102    }
103
104    fn __repr__(&self) -> String {
105        format!("X400Address(raw_der=<{} bytes>)", self.raw_der.len())
106    }
107}
108
109// ── DirectoryName ─────────────────────────────────────────────────────────────
110
111/// A `directoryName [4]` GeneralName (X.500 distinguished name).
112///
113/// The ``name_der`` field contains the raw DER encoding of the `Name`
114/// SEQUENCE.  Pass it to :func:`~synta.parse_name_attrs` to decode the
115/// attribute list.
116#[pyclass(frozen, name = "DirectoryName", module = "synta.general_name")]
117pub struct PyDirectoryName {
118    /// Raw DER bytes of the Name SEQUENCE.
119    pub name_der: Vec<u8>,
120}
121
122#[pymethods]
123impl PyDirectoryName {
124    /// Raw DER bytes of the Name SEQUENCE (starts with ``0x30``).
125    #[getter]
126    fn name_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
127        PyBytes::new(py, &self.name_der)
128    }
129
130    fn __repr__(&self) -> String {
131        let dn = synta_certificate::name::format_dn(&self.name_der);
132        format!("DirectoryName(name={dn:?})")
133    }
134}
135
136// ── EDIPartyName ──────────────────────────────────────────────────────────────
137
138/// An `ediPartyName [5]` GeneralName (raw DER, unusual in practice).
139#[pyclass(frozen, name = "EDIPartyName", module = "synta.general_name")]
140pub struct PyEdiPartyName {
141    pub raw_der: Vec<u8>,
142}
143
144#[pymethods]
145impl PyEdiPartyName {
146    /// Raw DER bytes of the EDIPartyName value.
147    #[getter]
148    fn raw_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
149        PyBytes::new(py, &self.raw_der)
150    }
151
152    fn __repr__(&self) -> String {
153        format!("EDIPartyName(raw_der=<{} bytes>)", self.raw_der.len())
154    }
155}
156
157// ── UniformResourceIdentifier ─────────────────────────────────────────────────
158
159/// A `uniformResourceIdentifier [6]` GeneralName (URI).
160#[pyclass(
161    frozen,
162    name = "UniformResourceIdentifier",
163    module = "synta.general_name"
164)]
165pub struct PyUri {
166    pub value: String,
167}
168
169#[pymethods]
170impl PyUri {
171    /// The URI string.
172    #[getter]
173    fn value(&self) -> &str {
174        &self.value
175    }
176
177    fn __repr__(&self) -> String {
178        format!("UniformResourceIdentifier(value={:?})", self.value)
179    }
180}
181
182// ── IPAddress ─────────────────────────────────────────────────────────────────
183
184/// An `iPAddress [7]` GeneralName.
185///
186/// ``address`` is 4 raw bytes for IPv4 or 16 raw bytes for IPv6.
187#[pyclass(frozen, name = "IPAddress", module = "synta.general_name")]
188pub struct PyIPAddress {
189    pub address: Vec<u8>,
190}
191
192#[pymethods]
193impl PyIPAddress {
194    /// Raw address bytes (4 bytes for IPv4, 16 bytes for IPv6).
195    #[getter]
196    fn address<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
197        PyBytes::new(py, &self.address)
198    }
199
200    /// The IP address as a Python :class:`ipaddress.IPv4Address` or
201    /// :class:`ipaddress.IPv6Address`, matching the ``cryptography`` library API.
202    ///
203    /// ```python,ignore
204    /// import ipaddress
205    /// gn = ...  # IPAddress general name
206    /// assert isinstance(gn.value, (ipaddress.IPv4Address, ipaddress.IPv6Address))
207    /// ```
208    #[getter]
209    fn value<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
210        let ipaddress = PyModule::import(py, "ipaddress")?;
211        let cls = match self.address.len() {
212            4 => ipaddress.getattr("IPv4Address")?,
213            16 => ipaddress.getattr("IPv6Address")?,
214            n => {
215                return Err(pyo3::exceptions::PyValueError::new_err(format!(
216                    "invalid IP address length: {n} (expected 4 or 16)"
217                )))
218            }
219        };
220        cls.call1((PyBytes::new(py, &self.address),))
221    }
222
223    fn __repr__(&self) -> String {
224        match self.address.len() {
225            4 => format!(
226                "IPAddress(address={}.{}.{}.{})",
227                self.address[0], self.address[1], self.address[2], self.address[3]
228            ),
229            16 => {
230                let parts: Vec<String> = self
231                    .address
232                    .chunks(2)
233                    .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
234                    .collect();
235                format!("IPAddress(address={})", parts.join(":"))
236            }
237            n => format!("IPAddress(address=<{n} bytes>)"),
238        }
239    }
240}
241
242// ── RegisteredID ──────────────────────────────────────────────────────────────
243
244/// A `registeredID [8]` GeneralName (OID).
245#[pyclass(frozen, name = "RegisteredID", module = "synta.general_name")]
246pub struct PyRegisteredId {
247    pub oid: synta::ObjectIdentifier,
248}
249
250#[pymethods]
251impl PyRegisteredId {
252    /// The registered OID.
253    #[getter]
254    fn oid<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
255        Py::new(py, PyObjectIdentifier::from_oid(self.oid.clone())).map(|p| p.into_bound(py))
256    }
257
258    fn __repr__(&self) -> String {
259        format!("RegisteredID(oid={})", self.oid)
260    }
261}
262
263// ── Converter helper ──────────────────────────────────────────────────────────
264
265/// Encode an ASN.1 `Element` to DER bytes (no generated `to_der` for Element).
266///
267/// Returns `None` if encoding fails.
268fn encode_element(elem: &synta::Element<'_>) -> Option<Vec<u8>> {
269    use synta::traits::Encode;
270    let mut enc = synta::Encoder::new(synta::Encoding::Der);
271    elem.encode(&mut enc).ok()?;
272    enc.finish().ok()
273}
274
275/// Convert a Rust `GeneralName<'_>` to the appropriate Python general-name object.
276///
277/// Returns a `Bound<'py, PyAny>` pointing to the concrete typed class
278/// (`PyOtherName`, `PyDnsName`, etc.) registered in `synta.general_name`.
279pub fn py_from_general_name<'py>(
280    py: Python<'py>,
281    gn: &synta_certificate::GeneralName<'_>,
282) -> PyResult<Bound<'py, PyAny>> {
283    use synta_certificate::GeneralName;
284
285    match gn {
286        GeneralName::OtherName(on) => {
287            let value = encode_element(&on.value).ok_or_else(|| {
288                pyo3::exceptions::PyValueError::new_err(
289                    "failed to encode OtherName value element to DER",
290                )
291            })?;
292            let obj = Py::new(
293                py,
294                PyOtherName {
295                    type_id: on.type_id.clone(),
296                    value,
297                },
298            )?;
299            Ok(obj.into_bound(py).into_any())
300        }
301        GeneralName::Rfc822Name(ia5) => {
302            let obj = Py::new(
303                py,
304                PyRfc822Name {
305                    value: ia5.as_str().to_string(),
306                },
307            )?;
308            Ok(obj.into_bound(py).into_any())
309        }
310        GeneralName::DNSName(ia5) => {
311            let obj = Py::new(
312                py,
313                PyDnsName {
314                    value: ia5.as_str().to_string(),
315                },
316            )?;
317            Ok(obj.into_bound(py).into_any())
318        }
319        GeneralName::X400Address(elem) => {
320            let raw_der = encode_element(elem).ok_or_else(|| {
321                pyo3::exceptions::PyValueError::new_err(
322                    "failed to encode X400Address element to DER",
323                )
324            })?;
325            let obj = Py::new(py, PyX400Address { raw_der })?;
326            Ok(obj.into_bound(py).into_any())
327        }
328        GeneralName::DirectoryName(name) => {
329            let name_der = name.to_der().map_err(SyntaErr)?;
330            let obj = Py::new(py, PyDirectoryName { name_der })?;
331            Ok(obj.into_bound(py).into_any())
332        }
333        GeneralName::EdiPartyName(epn) => {
334            let raw_der = epn.to_der().map_err(SyntaErr)?;
335            let obj = Py::new(py, PyEdiPartyName { raw_der })?;
336            Ok(obj.into_bound(py).into_any())
337        }
338        GeneralName::UniformResourceIdentifier(ia5) => {
339            let obj = Py::new(
340                py,
341                PyUri {
342                    value: ia5.as_str().to_string(),
343                },
344            )?;
345            Ok(obj.into_bound(py).into_any())
346        }
347        GeneralName::IPAddress(oct) => {
348            let obj = Py::new(
349                py,
350                PyIPAddress {
351                    address: oct.as_bytes().to_vec(),
352                },
353            )?;
354            Ok(obj.into_bound(py).into_any())
355        }
356        GeneralName::RegisteredID(oid) => {
357            let obj = Py::new(py, PyRegisteredId { oid: oid.clone() })?;
358            Ok(obj.into_bound(py).into_any())
359        }
360    }
361}
362
363/// Decode a DER-encoded `SEQUENCE OF GeneralName` into a Python list of
364/// typed general-name objects.
365///
366/// `raw` must be the complete DER bytes of the `SEQUENCE OF GeneralName`
367/// value (i.e. the `extn_value` OCTET STRING content for SAN or IAN).
368///
369/// Returns a Python `list` of typed objects: :class:`DNSName`,
370/// :class:`RFC822Name`, :class:`OtherName`, etc.
371pub fn decode_general_names_to_py<'py>(
372    py: Python<'py>,
373    raw: &[u8],
374) -> PyResult<Bound<'py, PyList>> {
375    use synta_certificate::GeneralName;
376
377    let mut decoder = synta::Decoder::new(raw, synta::Encoding::Der);
378    let gns: Vec<GeneralName<'_>> = decoder.decode().map_err(|e| {
379        pyo3::exceptions::PyValueError::new_err(format!(
380            "failed to decode SEQUENCE OF GeneralName: {e}"
381        ))
382    })?;
383
384    let list = PyList::empty(py);
385    for gn in &gns {
386        list.append(py_from_general_name(py, gn)?)?;
387    }
388    Ok(list)
389}