Skip to main content

_synta/certificate/
ms_pki.rs

1//! Python bindings for Microsoft PKI (AD CS) ASN.1 types.
2//!
3//! Exposes ``MSCSTemplateV2`` (certificate template extension) and
4//! ``RequestClientInfo`` (enrollment client info) into the ``synta.ms_pki``
5//! submodule, along with OID constants for Microsoft-specific extensions.
6
7use pyo3::prelude::*;
8use pyo3::types::PyBytes;
9use synta::traits::{Decode, Encode};
10use synta::{Decoder, Encoding};
11
12use crate::error::SyntaErr;
13use crate::types::PyObjectIdentifier;
14
15fn encode_to_der<T: Encode>(v: &T) -> Vec<u8> {
16    let mut enc = synta::Encoder::new(Encoding::Der);
17    if v.encode(&mut enc).is_err() {
18        return Vec::new();
19    }
20    enc.finish().unwrap_or_default()
21}
22
23// ── MSCSTemplateV2 ────────────────────────────────────────────────────────────
24
25/// Microsoft Certificate Services template extension v2 (``id-ms-certificate-template``).
26///
27/// Parsed from the extension value DER bytes of OID ``1.3.6.1.4.1.311.21.7``
28/// (``synta.oids.MS_CERTIFICATE_TEMPLATE``).
29///
30/// ```python,ignore
31/// import synta
32/// import synta.ms_pki as ms_pki
33///
34/// ext_der = cert.get_extension_value_der("1.3.6.1.4.1.311.21.7")
35/// if ext_der:
36///     tmpl = ms_pki.MSCSTemplateV2.from_der(ext_der)
37///     print(tmpl.template_id, tmpl.template_major_version)
38/// ```
39#[pyclass(frozen, name = "MSCSTemplateV2")]
40pub struct PyMSCSTemplateV2 {
41    template_id: Py<PyObjectIdentifier>,
42    template_major_version: i64,
43    template_minor_version: Option<i64>,
44    raw_der: Vec<u8>,
45}
46
47#[pymethods]
48impl PyMSCSTemplateV2 {
49    /// Parse a DER-encoded ``MSCSTemplateV2`` SEQUENCE.
50    #[staticmethod]
51    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
52        let mut dec = Decoder::new(data, Encoding::Der);
53        let tmpl =
54            synta_certificate::ms_pki_types::MSCSTemplateV2::decode(&mut dec).map_err(SyntaErr)?;
55        let template_id = Py::new(py, PyObjectIdentifier::from_oid(tmpl.template_id.clone()))?;
56        let template_major_version = tmpl.template_major_version.as_i64().map_err(SyntaErr)?;
57        let template_minor_version = tmpl
58            .template_minor_version
59            .as_ref()
60            .map(|v| v.as_i64().map_err(SyntaErr))
61            .transpose()?;
62        let raw_der = encode_to_der(&tmpl);
63        Ok(PyMSCSTemplateV2 {
64            template_id,
65            template_major_version,
66            template_minor_version,
67            raw_der,
68        })
69    }
70
71    /// Re-encode to DER bytes.
72    pub fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
73        PyBytes::new(py, &self.raw_der)
74    }
75
76    /// Template OID (e.g. ``"1.3.6.1.4.1.311.21.8.<enterprise>.<template-id>"``).
77    #[getter]
78    fn template_id(&self, py: Python<'_>) -> Py<PyObjectIdentifier> {
79        self.template_id.clone_ref(py)
80    }
81
82    /// Template major version number.
83    #[getter]
84    fn template_major_version(&self) -> i64 {
85        self.template_major_version
86    }
87
88    /// Template minor version number, or ``None`` if absent.
89    #[getter]
90    fn template_minor_version(&self) -> Option<i64> {
91        self.template_minor_version
92    }
93
94    fn __repr__(&self, py: Python<'_>) -> String {
95        let oid = self.template_id.bind(py).get();
96        format!(
97            "MSCSTemplateV2(template_id='{}', major={}, minor={:?})",
98            oid.inner, self.template_major_version, self.template_minor_version,
99        )
100    }
101}
102
103// ── RequestClientInfo ─────────────────────────────────────────────────────────
104
105/// Windows enrollment client info (OID ``1.3.6.1.4.1.311.21.20``).
106///
107/// Extension carried in certificate requests to identify the enrolling client.
108///
109/// ```python,ignore
110/// import synta
111/// import synta.ms_pki as ms_pki
112///
113/// ext_der = cert.get_extension_value_der("1.3.6.1.4.1.311.21.20")
114/// if ext_der:
115///     info = ms_pki.RequestClientInfo.from_der(ext_der)
116///     print(info.machine_name, info.user_name, info.process_name)
117/// ```
118#[pyclass(frozen, name = "RequestClientInfo")]
119pub struct PyRequestClientInfo {
120    client_id: Option<i64>,
121    machine_name: Option<String>,
122    user_name: Option<String>,
123    process_name: Option<String>,
124}
125
126#[pymethods]
127impl PyRequestClientInfo {
128    /// Parse a DER-encoded ``RequestClientInfo`` SEQUENCE.
129    #[staticmethod]
130    pub fn from_der(data: &[u8]) -> PyResult<Self> {
131        let mut dec = Decoder::new(data, Encoding::Der);
132        let info = synta_certificate::ms_pki_types::RequestClientInfo::decode(&mut dec)
133            .map_err(SyntaErr)?;
134        let client_id = info
135            .client_id
136            .as_ref()
137            .map(|v| v.as_i64().map_err(SyntaErr))
138            .transpose()?;
139        let machine_name = info.machine_name.as_ref().map(|s| s.as_str().to_owned());
140        let user_name = info.user_name.as_ref().map(|s| s.as_str().to_owned());
141        let process_name = info.process_name.as_ref().map(|s| s.as_str().to_owned());
142        Ok(PyRequestClientInfo {
143            client_id,
144            machine_name,
145            user_name,
146            process_name,
147        })
148    }
149
150    /// Numeric client ID (enrollment type), or ``None``.
151    #[getter]
152    fn client_id(&self) -> Option<i64> {
153        self.client_id
154    }
155
156    /// NetBIOS or DNS machine name, or ``None``.
157    #[getter]
158    fn machine_name(&self) -> Option<&str> {
159        self.machine_name.as_deref()
160    }
161
162    /// Windows user name (``DOMAIN\user``), or ``None``.
163    #[getter]
164    fn user_name(&self) -> Option<&str> {
165        self.user_name.as_deref()
166    }
167
168    /// Enrolling process name (e.g. ``certreq``), or ``None``.
169    #[getter]
170    fn process_name(&self) -> Option<&str> {
171        self.process_name.as_deref()
172    }
173
174    fn __repr__(&self) -> String {
175        format!(
176            "RequestClientInfo(machine={:?}, user={:?}, process={:?})",
177            self.machine_name, self.user_name, self.process_name,
178        )
179    }
180}
181
182// ── Module registration ───────────────────────────────────────────────────────
183
184pub(super) fn register_ms_pki_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
185    let py = parent.py();
186    let m = PyModule::new(py, "synta.ms_pki")?;
187
188    m.add_class::<PyMSCSTemplateV2>()?;
189    m.add_class::<PyRequestClientInfo>()?;
190
191    // OID constants not already in synta.oids
192    let oc = |components: &[u32]| super::oid_const(py, components);
193
194    m.add(
195        "ID_MS_CERTSRV_CA_VERSION",
196        oc(synta_certificate::ms_pki_types::ID_MS_CERTSRV_CA_VERSION),
197    )?;
198    m.add(
199        "ID_MS_CERTSRV_PREVIOUS_CERT_HASH",
200        oc(synta_certificate::ms_pki_types::ID_MS_CERTSRV_PREVIOUS_CERT_HASH),
201    )?;
202    m.add(
203        "ID_MS_CRL_VIRTUAL_BASE",
204        oc(synta_certificate::ms_pki_types::ID_MS_CRL_VIRTUAL_BASE),
205    )?;
206    m.add(
207        "ID_MS_CRL_NEXT_PUBLISH",
208        oc(synta_certificate::ms_pki_types::ID_MS_CRL_NEXT_PUBLISH),
209    )?;
210    m.add(
211        "ID_MS_KP_CA_EXCHANGE",
212        oc(synta_certificate::ms_pki_types::ID_MS_KP_CA_EXCHANGE),
213    )?;
214    m.add(
215        "ID_MS_KP_KEY_RECOVERY_AGENT",
216        oc(synta_certificate::ms_pki_types::ID_MS_KP_KEY_RECOVERY_AGENT),
217    )?;
218    m.add(
219        "ID_MS_ENTERPRISE_OID_ROOT",
220        oc(synta_certificate::ms_pki_types::ID_MS_ENTERPRISE_OID_ROOT),
221    )?;
222    m.add(
223        "ID_MS_APPLICATION_CERT_POLICIES",
224        oc(synta_certificate::ms_pki_types::ID_MS_APPLICATION_CERT_POLICIES),
225    )?;
226    m.add(
227        "ID_MS_REQUEST_CLIENT_INFO",
228        oc(synta_certificate::ms_pki_types::ID_MS_REQUEST_CLIENT_INFO),
229    )?;
230    m.add(
231        "ID_MS_ENCRYPTED_KEY_HASH",
232        oc(synta_certificate::ms_pki_types::ID_MS_ENCRYPTED_KEY_HASH),
233    )?;
234    m.add(
235        "ID_MS_CERTSRV_CROSSCA_VERSION",
236        oc(synta_certificate::ms_pki_types::ID_MS_CERTSRV_CROSSCA_VERSION),
237    )?;
238    m.add(
239        "ID_MS_KP_CTL_USAGE_SIGNING",
240        oc(synta_certificate::ms_pki_types::ID_MS_KP_CTL_USAGE_SIGNING),
241    )?;
242    m.add(
243        "ID_MS_KP_TIME_STAMP_SIGNING",
244        oc(synta_certificate::ms_pki_types::ID_MS_KP_TIME_STAMP_SIGNING),
245    )?;
246    m.add(
247        "ID_MS_KP_EFS_CRYPTO",
248        oc(synta_certificate::ms_pki_types::ID_MS_KP_EFS_CRYPTO),
249    )?;
250    m.add(
251        "ID_MS_KP_EFS_RECOVERY",
252        oc(synta_certificate::ms_pki_types::ID_MS_KP_EFS_RECOVERY),
253    )?;
254    m.add(
255        "ID_MS_KP_KEY_RECOVERY",
256        oc(synta_certificate::ms_pki_types::ID_MS_KP_KEY_RECOVERY),
257    )?;
258    m.add(
259        "ID_MS_KP_DOCUMENT_SIGNING",
260        oc(synta_certificate::ms_pki_types::ID_MS_KP_DOCUMENT_SIGNING),
261    )?;
262    m.add(
263        "ID_MS_KP_LIFETIME_SIGNING",
264        oc(synta_certificate::ms_pki_types::ID_MS_KP_LIFETIME_SIGNING),
265    )?;
266    m.add(
267        "ID_MS_AUTO_ENROLL_CTL_USAGE",
268        oc(synta_certificate::ms_pki_types::ID_MS_AUTO_ENROLL_CTL_USAGE),
269    )?;
270
271    crate::install_submodule(
272        parent,
273        &m,
274        "synta.ms_pki",
275        Some(concat!(
276            "synta.ms_pki — Microsoft PKI (AD CS) certificate extension types.\n\n",
277            "Provides ``MSCSTemplateV2`` for parsing certificate template extensions\n",
278            "and ``RequestClientInfo`` for parsing enrollment client info extensions.\n",
279        )),
280    )?;
281    Ok(())
282}