Skip to main content

_synta/certificate/
csr_builder.rs

1//! Python bindings for building PKCS #10 Certificate Signing Requests.
2//!
3//! Delegates all DER construction and signing to
4//! [`synta_certificate::csr_builder::CsrBuilder`].  OpenSSL is only used for
5//! the signing step; all ASN.1 encoding is done inside the Rust builder.
6//!
7//! # Chaining
8//!
9//! Each setter returns the same `CsrBuilder` object, enabling a
10//! single-expression build:
11//!
12//! ```python,ignore
13//! import synta
14//!
15//! csr = (
16//!     synta.CsrBuilder()
17//!     .subject_name(name_der)
18//!     .public_key(key.public_key())
19//!     .add_extension("2.5.29.17", False, san_der)
20//!     .sign(key, "sha256")
21//! )
22//! ```
23
24use pyo3::exceptions::PyValueError;
25use pyo3::prelude::*;
26use pyo3::types::PyBytes;
27use pyo3::Py;
28use synta::ObjectIdentifier;
29use synta_certificate::{CertificateSigner as _, CsrBuilder, PrivateKey as _};
30
31use crate::certificate::pkix::PyCsr;
32use crate::crypto_keys::PyPrivateKey;
33
34// ── CsrBuilder ────────────────────────────────────────────────────────────────
35
36/// Builder for PKCS #10 Certificate Signing Requests.
37///
38/// The subject Name and SubjectPublicKeyInfo bytes are stored as-is and spliced
39/// verbatim into the ``CertificationRequestInfo`` DER at signing time — no
40/// re-parse or re-encode.
41///
42/// Each setter method returns the **same** builder object, so calls can be
43/// chained with ``.``:
44///
45/// ```python,ignore
46/// import synta
47///
48/// csr = (
49///     synta.CsrBuilder()
50///     .subject_name(cert.subject_raw_der)
51///     .public_key(key.public_key())
52///     .add_extension("2.5.29.17", False, san_der)
53///     .sign(key, "sha256")
54/// )
55/// ```
56#[pyclass(name = "CsrBuilder")]
57pub struct PyCsrBuilder {
58    /// Subject Name TLV (pre-encoded DER).
59    subject: Option<Vec<u8>>,
60    /// SubjectPublicKeyInfo TLV (pre-encoded DER).
61    spki: Option<Vec<u8>>,
62    /// Extensions: (OID, critical, extension-value DER bytes).
63    extensions: Vec<(ObjectIdentifier, bool, Vec<u8>)>,
64}
65
66#[pymethods]
67impl PyCsrBuilder {
68    /// Create a new, empty ``CsrBuilder``.
69    #[new]
70    fn new() -> Self {
71        Self {
72            subject: None,
73            spki: None,
74            extensions: Vec::new(),
75        }
76    }
77
78    /// Set the subject Name from pre-encoded DER bytes.
79    ///
80    /// Accepts any bytes object that is a valid DER-encoded ``Name`` SEQUENCE
81    /// TLV.  The bytes from ``Certificate.subject_raw_der`` or
82    /// ``CertificationRequest.subject_raw_der`` are suitable directly:
83    ///
84    /// ```python,ignore
85    /// builder.subject_name(cert.subject_raw_der)
86    /// ```
87    fn subject_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
88        slf.borrow_mut().subject = Some(name_der.to_vec());
89        slf
90    }
91
92    /// Set the SubjectPublicKeyInfo from a :class:`PublicKey` object.
93    ///
94    /// Serializes the key to SPKI DER once and stores the bytes.
95    ///
96    /// ```python,ignore
97    /// builder.public_key(key.public_key())
98    /// ```
99    fn public_key<'py>(
100        slf: Bound<'py, Self>,
101        key: &crate::crypto_keys::PyPublicKey,
102    ) -> PyResult<Bound<'py, Self>> {
103        let der = key
104            .inner
105            .to_der()
106            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
107        slf.borrow_mut().spki = Some(der);
108        Ok(slf)
109    }
110
111    /// Set the SubjectPublicKeyInfo from pre-encoded SPKI DER bytes.
112    ///
113    /// Stores the bytes verbatim — no re-parsing.  Suitable for passing
114    /// ``Certificate.subject_public_key_info_der`` or
115    /// ``CertificationRequest.subject_public_key_info_der`` directly:
116    ///
117    /// ```python,ignore
118    /// builder.public_key_der(csr.subject_public_key_info_der)
119    /// ```
120    fn public_key_der<'py>(slf: Bound<'py, Self>, spki_der: &[u8]) -> Bound<'py, Self> {
121        slf.borrow_mut().spki = Some(spki_der.to_vec());
122        slf
123    }
124
125    /// Add an X.509 v3 extension to the ``extensionRequest`` attribute.
126    ///
127    /// ``oid`` is the extension OID in dotted-decimal notation.
128    /// ``critical`` is ``True`` if the extension is critical.
129    /// ``value_der`` is the DER-encoded extension value — the raw bytes that
130    /// ``Certificate.get_extension_value_der(oid)`` returns (i.e., the
131    /// content of the OCTET STRING, *not* the OCTET STRING TLV itself).
132    ///
133    /// All extensions added with this method are collected into a single
134    /// ``extensionRequest`` attribute in the ``CertificationRequestInfo``.
135    ///
136    /// ```python,ignore
137    /// builder.add_extension("2.5.29.17", False, san_der)
138    /// ```
139    fn add_extension<'py>(
140        slf: Bound<'py, Self>,
141        oid: &str,
142        critical: bool,
143        value_der: &[u8],
144    ) -> PyResult<Bound<'py, Self>> {
145        use std::str::FromStr;
146        let oid = ObjectIdentifier::from_str(oid)
147            .map_err(|_| PyValueError::new_err(format!("invalid OID: {oid}")))?;
148        slf.borrow_mut()
149            .extensions
150            .push((oid, critical, value_der.to_vec()));
151        Ok(slf)
152    }
153
154    /// Sign the CSR and return a :class:`CertificationRequest`.
155    ///
156    /// ``key`` is the subject's :class:`PrivateKey`.  ``algorithm`` is the
157    /// hash algorithm name — one of ``"sha1"``, ``"sha256"``, ``"sha384"``,
158    /// ``"sha512"`` for RSA and ECDSA keys.  For Ed25519 / Ed448 keys the
159    /// argument is ignored (no pre-hash is used).
160    ///
161    /// ``context`` is the ML-DSA context string (FIPS 204 domain separator).
162    /// It defaults to ``None`` (equivalent to an empty context).  Ignored for
163    /// non-ML-DSA keys.  When ``context`` is a non-empty ``bytes`` object and
164    /// the key is an ML-DSA key, the manual signing path is used:
165    /// ``build_cri`` → ``sign_ml_dsa_with_context`` → ``assemble``.
166    ///
167    /// Both subject name and public key must have been set; a :exc:`ValueError`
168    /// is raised if either is missing.
169    ///
170    /// Raises :exc:`ValueError` on encoding or signing errors.
171    ///
172    /// ```python,ignore
173    /// csr = builder.sign(key, "sha256")
174    /// ml_csr = builder.sign(ml_dsa_key, "sha256", context=b"my-app")
175    /// ```
176    #[pyo3(signature = (key, algorithm, context = None))]
177    fn sign<'py>(
178        &self,
179        py: Python<'py>,
180        key: &PyPrivateKey,
181        algorithm: &str,
182        context: Option<&[u8]>,
183    ) -> PyResult<Bound<'py, PyCsr>> {
184        // Build the Rust-level builder from our stored fields.
185        let mut builder = CsrBuilder::new();
186        if let Some(ref b) = self.subject {
187            builder = builder.subject_name(b);
188        }
189        if let Some(ref b) = self.spki {
190            builder = builder.public_key_der(b);
191        }
192        for (oid, critical, value_bytes) in &self.extensions {
193            builder = builder.add_extension(oid.clone(), *critical, value_bytes);
194        }
195
196        // If a non-empty context is provided and this is an ML-DSA key, take the
197        // manual path: build_cri → sign_ml_dsa_with_context → assemble.
198        let ctx = context.unwrap_or(b"");
199        let is_ml_dsa = matches!(
200            key.inner.key_type(),
201            "ml-dsa-44" | "ml-dsa-65" | "ml-dsa-87"
202        );
203        let csr_der = if !ctx.is_empty() && is_ml_dsa {
204            let signer = key.as_signer(algorithm);
205            let sig_alg_der = signer
206                .signature_algorithm_der()
207                .map_err(|e| PyValueError::new_err(format!("{e}")))?;
208            let cri_der = builder
209                .build_cri(&sig_alg_der)
210                .map_err(|e| PyValueError::new_err(format!("{e}")))?;
211            let signature = key
212                .inner
213                .sign_ml_dsa_with_context(&cri_der, ctx)
214                .map_err(|e| PyValueError::new_err(format!("{e}")))?;
215            CsrBuilder::assemble(&cri_der, &sig_alg_der, &signature)
216                .map_err(|e| PyValueError::new_err(format!("{e}")))?
217        } else {
218            // Standard path: delegate to the signer trait object.
219            let signer = key.as_signer(algorithm);
220            builder
221                .sign(&signer)
222                .map_err(|e| PyValueError::new_err(format!("{e}")))?
223        };
224
225        // Parse the assembled DER back into a PyCsr.
226        let py_bytes = PyBytes::new(py, &csr_der);
227        let csr = PyCsr::new_from_der(py, py_bytes)?;
228        Py::new(py, csr).map(|py_csr| py_csr.into_bound(py))
229    }
230
231    fn __repr__(&self) -> String {
232        let subject = self
233            .subject
234            .as_ref()
235            .map(|b| format!("{} bytes", b.len()))
236            .unwrap_or_else(|| "not set".to_string());
237        format!("CsrBuilder(subject={subject})")
238    }
239}