Skip to main content

_synta/certificate/
pkcs8.rs

1//! Python bindings for PKCS #8 / RFC 5958 private key structures.
2//!
3//! Exposes ``OneAsymmetricKey`` (aka ``PrivateKeyInfo``) into the
4//! ``synta.pkcs8`` submodule for parsing DER-encoded private key files.
5
6use std::sync::OnceLock;
7
8use pyo3::prelude::*;
9use pyo3::types::PyBytes;
10
11use synta::{Decoder, Encoding};
12
13use crate::error::SyntaErr;
14use crate::types::PyObjectIdentifier;
15
16// ── PyOneAsymmetricKey ────────────────────────────────────────────────────────
17
18/// A PKCS #8 / RFC 5958 ``OneAsymmetricKey`` (also known as
19/// ``PrivateKeyInfo``) structure.
20///
21/// Parses a DER-encoded private key envelope that contains:
22///
23/// - ``version`` — 0 for v1, 1 for v2 (RFC 5958)
24/// - ``private_key_algorithm`` — the algorithm OID (e.g. RSA, EC, Ed25519)
25/// - ``private_key`` — the raw key material as an OCTET STRING
26/// - ``attributes`` (optional) — ``[0] IMPLICIT`` bag of attributes
27/// - ``public_key`` (optional) — ``[1] IMPLICIT`` bit string (v2 only)
28///
29/// Example:
30///
31/// ```python,ignore
32/// import synta.pkcs8 as pkcs8
33///
34/// with open("key.p8", "rb") as f:
35///     key = pkcs8.OneAsymmetricKey.from_der(f.read())
36/// print(key.version, key.private_key_algorithm)
37/// print(key.private_key.hex())
38/// ```
39#[pyclass(frozen, name = "OneAsymmetricKey")]
40pub struct PyOneAsymmetricKey {
41    _data: Py<PyBytes>,
42    raw: &'static [u8],
43    inner: OnceLock<Box<synta_certificate::pkcs8_types::OneAsymmetricKey<'static>>>,
44    // per-field caches
45    alg_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
46}
47
48impl PyOneAsymmetricKey {
49    fn key(&self) -> PyResult<&synta_certificate::pkcs8_types::OneAsymmetricKey<'static>> {
50        if let Some(v) = self.inner.get() {
51            return Ok(v.as_ref());
52        }
53        let mut dec = Decoder::new(self.raw, Encoding::Der);
54        let decoded = dec
55            .decode::<synta_certificate::pkcs8_types::OneAsymmetricKey<'static>>()
56            .map_err(SyntaErr)?;
57        let _ = self.inner.set(Box::new(decoded));
58        Ok(self.inner.get().unwrap().as_ref())
59    }
60}
61
62#[pymethods]
63impl PyOneAsymmetricKey {
64    /// Parse a DER-encoded ``OneAsymmetricKey`` SEQUENCE.
65    ///
66    /// :param data: DER bytes (the content of a PKCS #8 ``.p8`` or ``.key``
67    ///     file).
68    /// :raises ValueError: if the bytes cannot be decoded.
69    #[staticmethod]
70    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
71        let py_bytes = data.unbind();
72        {
73            let raw = py_bytes.as_bytes(py);
74            Decoder::new(raw, Encoding::Der)
75                .decode::<synta_certificate::pkcs8_types::OneAsymmetricKey<'_>>()
76                .map_err(SyntaErr)?;
77        }
78        let raw: &'static [u8] = unsafe {
79            let s = py_bytes.bind(py).as_bytes();
80            std::slice::from_raw_parts(s.as_ptr(), s.len())
81        };
82        Ok(Self {
83            _data: py_bytes,
84            raw,
85            inner: OnceLock::new(),
86            alg_oid_cache: OnceLock::new(),
87        })
88    }
89
90    /// Re-encode the ``OneAsymmetricKey`` to DER bytes.
91    ///
92    /// For a round-tripped object this returns the original bytes verbatim.
93    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
94        Ok(PyBytes::new(py, &self.key()?.to_der().map_err(SyntaErr)?))
95    }
96
97    /// Integer version: 0 for v1 (``PrivateKeyInfo``), 1 for v2 (RFC 5958).
98    #[getter]
99    fn version(&self) -> PyResult<i64> {
100        Ok(self.key()?.version.as_i64().map_err(SyntaErr)?)
101    }
102
103    /// The private-key algorithm :class:`~synta.ObjectIdentifier`.
104    ///
105    /// For RSA keys this is ``1.2.840.113549.1.1.1``; for EC keys it is
106    /// ``1.2.840.10045.2.1``; for Ed25519 it is ``1.3.101.112``.
107    #[getter]
108    fn private_key_algorithm<'py>(
109        &self,
110        py: Python<'py>,
111    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
112        if let Some(c) = self.alg_oid_cache.get() {
113            return Ok(c.clone_ref(py).into_bound(py));
114        }
115        let k = self.key()?;
116        let oid_obj = Py::new(
117            py,
118            PyObjectIdentifier::from_oid(k.private_key_algorithm.algorithm.clone()),
119        )?;
120        let _ = self.alg_oid_cache.set(oid_obj.clone_ref(py));
121        Ok(oid_obj.into_bound(py))
122    }
123
124    /// Raw key material bytes (the OCTET STRING value of ``privateKey``).
125    ///
126    /// The internal encoding of these bytes depends on the algorithm:
127    /// RFC 8410 (Ed25519/Ed448) wraps an OCTET STRING, while SEC 1 (EC)
128    /// uses an ``ECPrivateKey`` SEQUENCE.
129    #[getter]
130    fn private_key<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
131        Ok(PyBytes::new(py, self.key()?.private_key.as_bytes()))
132    }
133
134    /// Raw DER bytes of the ``[0] IMPLICIT`` attributes bag, or ``None``.
135    ///
136    /// The bytes include the implicit tag; feed them to a ``synta.Decoder``
137    /// for further inspection.
138    #[getter]
139    fn attributes_der<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
140        Ok(self
141            .key()?
142            .attributes
143            .as_ref()
144            .map(|a| PyBytes::new(py, a.as_bytes())))
145    }
146
147    /// Raw DER bytes of the ``[1] IMPLICIT`` public key bit string, or ``None``.
148    ///
149    /// Present only for v2 (RFC 5958) keys that include the public key.
150    #[getter]
151    fn public_key_der<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
152        Ok(self
153            .key()?
154            .public_key
155            .as_ref()
156            .map(|pk| PyBytes::new(py, pk.as_bytes())))
157    }
158
159    /// Algorithm parameters DER bytes, or ``None`` if absent.
160    ///
161    /// For RSA keys the parameters field is absent (``None``).  For EC keys
162    /// it contains a ``namedCurve`` OID.
163    #[getter]
164    fn alg_parameters_der<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
165        let k = self.key()?;
166        Ok(k.private_key_algorithm.parameters.as_ref().map(|p| {
167            let mut enc = synta::Encoder::new(Encoding::Der);
168            if enc.encode(p).is_err() {
169                return PyBytes::new(py, &[]);
170            }
171            PyBytes::new(py, &enc.finish().unwrap_or_default())
172        }))
173    }
174
175    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
176        let alg = self.private_key_algorithm(py)?;
177        Ok(format!(
178            "OneAsymmetricKey(version={}, algorithm={})",
179            self.version()?,
180            alg.borrow().inner,
181        ))
182    }
183}
184
185// ── register_pkcs8_submodule ──────────────────────────────────────────────────
186
187/// Build and register the ``synta.pkcs8`` submodule.
188pub(super) fn register_pkcs8_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
189    let py = parent.py();
190    let m = PyModule::new(py, "pkcs8")?;
191
192    m.add_class::<PyOneAsymmetricKey>()?;
193
194    // ``PrivateKeyInfo`` is the RFC 5958 alias for ``OneAsymmetricKey``.
195    // Expose it as a module-level alias so both names work.
196    m.add("PrivateKeyInfo", m.getattr("OneAsymmetricKey")?)?;
197
198    crate::install_submodule(
199        parent,
200        &m,
201        "synta.pkcs8",
202        Some(concat!(
203            "synta.pkcs8 — PKCS #8 / RFC 5958 private key structures.\n\n",
204            "Provides OneAsymmetricKey (PrivateKeyInfo) for parsing DER-encoded\n",
205            "private key envelopes produced by OpenSSL and other PKI tools.",
206        )),
207    )
208}