Skip to main content

_synta/certificate/
ac.rs

1//! Python bindings for RFC 5755 Attribute Certificate types.
2//!
3//! Exposes ``AttributeCertificate`` as a Python class and installs OID constants
4//! into the ``synta.ac`` submodule.
5
6use std::sync::OnceLock;
7
8use pyo3::prelude::*;
9use pyo3::types::{PyBytes, PyString};
10
11use synta::traits::Encode;
12use synta::{Decoder, Encoding};
13
14use crate::error::SyntaErr;
15use crate::types::PyObjectIdentifier;
16
17// ── helpers ───────────────────────────────────────────────────────────────────
18
19/// Encode an arbitrary `T: Encode` value to DER bytes.
20fn encode_to_der<T: Encode>(v: &T) -> Vec<u8> {
21    let mut enc = synta::Encoder::new(Encoding::Der);
22    if v.encode(&mut enc).is_err() {
23        return Vec::new();
24    }
25    enc.finish().unwrap_or_default()
26}
27
28// ── PyAttributeCertificate ────────────────────────────────────────────────────
29
30/// X.509 Attribute Certificate v2 (RFC 5755).
31///
32/// An Attribute Certificate (AC) binds a set of attributes (roles, clearances,
33/// service-authentication information) to a holder identified by reference to
34/// their Public Key Certificate (PKC), without requiring re-issuance of the PKC.
35///
36/// ```python,ignore
37/// import synta.ac as ac
38/// acer = ac.AttributeCertificate.from_der(open("attr.ac", "rb").read())
39/// print(acer.serial_number.hex())
40/// print(acer.not_before, "–", acer.not_after)
41/// print(acer.signature_algorithm_oid)
42/// ```
43#[pyclass(frozen, name = "AttributeCertificate")]
44pub struct PyAttributeCertificate {
45    _data: Py<PyBytes>,
46    raw: &'static [u8],
47    inner: OnceLock<Box<synta_certificate::attribute_cert_types::AttributeCertificate<'static>>>,
48    // Field caches
49    serial_number_cache: OnceLock<Py<PyBytes>>,
50    not_before_cache: OnceLock<Py<PyString>>,
51    not_after_cache: OnceLock<Py<PyString>>,
52    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
53    signature_cache: OnceLock<Py<PyBytes>>,
54    holder_der_cache: OnceLock<Py<PyBytes>>,
55    issuer_der_cache: OnceLock<Py<PyBytes>>,
56    attributes_der_cache: OnceLock<Py<PyBytes>>,
57}
58
59impl PyAttributeCertificate {
60    fn ac(
61        &self,
62    ) -> PyResult<&synta_certificate::attribute_cert_types::AttributeCertificate<'static>> {
63        if let Some(v) = self.inner.get() {
64            return Ok(v.as_ref());
65        }
66        let mut dec = Decoder::new(self.raw, Encoding::Der);
67        let decoded = dec
68            .decode::<synta_certificate::attribute_cert_types::AttributeCertificate<'_>>()
69            .map_err(SyntaErr)?;
70        // SAFETY: raw is pinned for the lifetime of self (kept alive by _data).
71        let decoded: synta_certificate::attribute_cert_types::AttributeCertificate<'static> =
72            unsafe { std::mem::transmute(decoded) };
73        let _ = self.inner.set(Box::new(decoded));
74        Ok(self.inner.get().unwrap().as_ref())
75    }
76}
77
78#[pymethods]
79impl PyAttributeCertificate {
80    /// Parse a DER-encoded ``AttributeCertificate`` SEQUENCE.
81    ///
82    /// :param data: DER bytes of the ``AttributeCertificate``.
83    /// :raises ValueError: if the bytes cannot be decoded.
84    #[staticmethod]
85    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
86        let py_bytes = data.unbind();
87        {
88            let raw = py_bytes.as_bytes(py);
89            Decoder::new(raw, Encoding::Der)
90                .decode::<synta_certificate::attribute_cert_types::AttributeCertificate<'_>>()
91                .map_err(SyntaErr)?;
92        }
93        let raw: &'static [u8] = unsafe { std::mem::transmute(py_bytes.as_bytes(py)) };
94        Ok(Self {
95            _data: py_bytes,
96            raw,
97            inner: OnceLock::new(),
98            serial_number_cache: OnceLock::new(),
99            not_before_cache: OnceLock::new(),
100            not_after_cache: OnceLock::new(),
101            signature_algorithm_oid_cache: OnceLock::new(),
102            signature_cache: OnceLock::new(),
103            holder_der_cache: OnceLock::new(),
104            issuer_der_cache: OnceLock::new(),
105            attributes_der_cache: OnceLock::new(),
106        })
107    }
108
109    /// Return the DER encoding of this ``AttributeCertificate``.
110    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
111        let mut enc = synta::Encoder::new(Encoding::Der);
112        self.ac()?.encode(&mut enc).map_err(SyntaErr)?;
113        Ok(PyBytes::new(py, &enc.finish().map_err(SyntaErr)?))
114    }
115
116    /// ``AttCertVersion`` integer (always ``1`` for v2 per RFC 5755).
117    #[getter]
118    fn version(&self) -> PyResult<i64> {
119        Ok(self.ac()?.acinfo.version.as_i64().unwrap_or(1))
120    }
121
122    /// Certificate serial number as big-endian bytes.
123    #[getter]
124    fn serial_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
125        if let Some(c) = self.serial_number_cache.get() {
126            return Ok(c.clone_ref(py).into_bound(py));
127        }
128        let b = PyBytes::new(py, self.ac()?.acinfo.serial_number.as_bytes());
129        let _ = self.serial_number_cache.set(b.as_unbound().clone_ref(py));
130        Ok(b)
131    }
132
133    /// Validity period start time (GeneralizedTime string, e.g. ``"20240101120000Z"``).
134    #[getter]
135    fn not_before<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
136        if let Some(c) = self.not_before_cache.get() {
137            return Ok(c.clone_ref(py).into_bound(py));
138        }
139        let s = self
140            .ac()?
141            .acinfo
142            .attr_cert_validity_period
143            .not_before_time
144            .to_string();
145        let ps = PyString::new(py, &s);
146        let _ = self.not_before_cache.set(ps.as_unbound().clone_ref(py));
147        Ok(ps)
148    }
149
150    /// Validity period end time (GeneralizedTime string).
151    #[getter]
152    fn not_after<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
153        if let Some(c) = self.not_after_cache.get() {
154            return Ok(c.clone_ref(py).into_bound(py));
155        }
156        let s = self
157            .ac()?
158            .acinfo
159            .attr_cert_validity_period
160            .not_after_time
161            .to_string();
162        let ps = PyString::new(py, &s);
163        let _ = self.not_after_cache.set(ps.as_unbound().clone_ref(py));
164        Ok(ps)
165    }
166
167    /// Signature algorithm OID.
168    #[getter]
169    fn signature_algorithm_oid(&self, py: Python<'_>) -> PyResult<Py<PyObjectIdentifier>> {
170        if let Some(c) = self.signature_algorithm_oid_cache.get() {
171            return Ok(c.clone_ref(py));
172        }
173        let oid = self.ac()?.acinfo.signature.algorithm.clone();
174        let obj = Py::new(py, PyObjectIdentifier::from_oid(oid))?;
175        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
176        Ok(obj)
177    }
178
179    /// Raw signature bytes (the bit-string value, zero-byte padding stripped).
180    #[getter]
181    fn signature<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
182        if let Some(c) = self.signature_cache.get() {
183            return Ok(c.clone_ref(py).into_bound(py));
184        }
185        let b = PyBytes::new(py, self.ac()?.signature.as_bytes());
186        let _ = self.signature_cache.set(b.as_unbound().clone_ref(py));
187        Ok(b)
188    }
189
190    /// Raw DER bytes of the ``Holder`` SEQUENCE (for re-decoding or inspection).
191    #[getter]
192    fn holder_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
193        if let Some(c) = self.holder_der_cache.get() {
194            return Ok(c.clone_ref(py).into_bound(py));
195        }
196        let der = encode_to_der(&self.ac()?.acinfo.holder);
197        let b = PyBytes::new(py, &der);
198        let _ = self.holder_der_cache.set(b.as_unbound().clone_ref(py));
199        Ok(b)
200    }
201
202    /// Raw DER bytes of the ``AttCertIssuer`` CHOICE (for re-decoding or inspection).
203    #[getter]
204    fn issuer_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
205        if let Some(c) = self.issuer_der_cache.get() {
206            return Ok(c.clone_ref(py).into_bound(py));
207        }
208        let der = encode_to_der(&self.ac()?.acinfo.issuer);
209        let b = PyBytes::new(py, &der);
210        let _ = self.issuer_der_cache.set(b.as_unbound().clone_ref(py));
211        Ok(b)
212    }
213
214    /// Raw DER bytes of the ``SEQUENCE OF Attribute`` attributes field.
215    ///
216    /// Each ``Attribute`` in the sequence can carry roles, clearances,
217    /// or service-authentication information.  Re-decode with a ``Decoder``
218    /// to inspect individual attributes.
219    #[getter]
220    fn attributes_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
221        if let Some(c) = self.attributes_der_cache.get() {
222            return Ok(c.clone_ref(py).into_bound(py));
223        }
224        let der = encode_to_der(&self.ac()?.acinfo.attributes);
225        let b = PyBytes::new(py, &der);
226        let _ = self.attributes_der_cache.set(b.as_unbound().clone_ref(py));
227        Ok(b)
228    }
229
230    fn __repr__(&self) -> PyResult<String> {
231        let ac = self.ac()?;
232        Ok(format!(
233            "AttributeCertificate(serial={})",
234            ac.acinfo
235                .serial_number
236                .as_bytes()
237                .iter()
238                .map(|b| format!("{b:02x}"))
239                .collect::<String>(),
240        ))
241    }
242}
243
244// ── register_ac_submodule ─────────────────────────────────────────────────────
245
246/// Build and register the ``synta.ac`` submodule.
247pub(super) fn register_ac_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
248    let py = parent.py();
249    let m = PyModule::new(py, "ac")?;
250
251    m.add_class::<PyAttributeCertificate>()?;
252
253    // ── RFC 5755 OIDs ─────────────────────────────────────────────────────────
254    m.add(
255        "ID_PE_AC_AUDIT_IDENTITY",
256        super::oid_const(
257            py,
258            synta_certificate::attribute_cert_types::ID_PE_AC_AUDIT_IDENTITY,
259        ),
260    )?;
261    m.add(
262        "ID_PE_AA_CONTROLS",
263        super::oid_const(
264            py,
265            synta_certificate::attribute_cert_types::ID_PE_AA_CONTROLS,
266        ),
267    )?;
268    m.add(
269        "ID_PE_AC_PROXYING",
270        super::oid_const(
271            py,
272            synta_certificate::attribute_cert_types::ID_PE_AC_PROXYING,
273        ),
274    )?;
275    m.add(
276        "ID_CE_TARGET_INFORMATION",
277        super::oid_const(
278            py,
279            synta_certificate::attribute_cert_types::ID_CE_TARGET_INFORMATION,
280        ),
281    )?;
282    m.add(
283        "ID_ACA_AUTHENTICATION_INFO",
284        super::oid_const(
285            py,
286            synta_certificate::attribute_cert_types::ID_ACA_AUTHENTICATION_INFO,
287        ),
288    )?;
289    m.add(
290        "ID_ACA_ACCESS_IDENTITY",
291        super::oid_const(
292            py,
293            synta_certificate::attribute_cert_types::ID_ACA_ACCESS_IDENTITY,
294        ),
295    )?;
296    m.add(
297        "ID_ACA_CHARGING_IDENTITY",
298        super::oid_const(
299            py,
300            synta_certificate::attribute_cert_types::ID_ACA_CHARGING_IDENTITY,
301        ),
302    )?;
303    m.add(
304        "ID_ACA_GROUP",
305        super::oid_const(py, synta_certificate::attribute_cert_types::ID_ACA_GROUP),
306    )?;
307    m.add(
308        "ID_ACA_ENC_ATTRS",
309        super::oid_const(
310            py,
311            synta_certificate::attribute_cert_types::ID_ACA_ENC_ATTRS,
312        ),
313    )?;
314    m.add(
315        "ID_AT_ROLE",
316        super::oid_const(py, synta_certificate::attribute_cert_types::ID_AT_ROLE),
317    )?;
318    m.add(
319        "ID_AT_CLEARANCE",
320        super::oid_const(py, synta_certificate::attribute_cert_types::ID_AT_CLEARANCE),
321    )?;
322
323    crate::install_submodule(
324        parent,
325        &m,
326        "synta.ac",
327        Some(concat!(
328            "synta.ac — RFC 5755 Attribute Certificate v2 types.\n\n",
329            "Provides AttributeCertificate for decoding X.509 Attribute\n",
330            "Certificates that bind roles, clearances, or service-auth\n",
331            "attributes to a holder's PKC, along with OID constants for\n",
332            "RFC 5755 extensions and attribute types.",
333        )),
334    )
335}