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::{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    /// Both subject name and public key must have been set; a :exc:`ValueError`
162    /// is raised if either is missing.
163    ///
164    /// Raises :exc:`ValueError` on encoding or signing errors.
165    ///
166    /// ```python,ignore
167    /// csr = builder.sign(key, "sha256")
168    /// ```
169    fn sign<'py>(
170        &self,
171        py: Python<'py>,
172        key: &PyPrivateKey,
173        algorithm: &str,
174    ) -> PyResult<Bound<'py, PyCsr>> {
175        // Build the Rust-level builder from our stored fields.
176        let mut builder = CsrBuilder::new();
177        if let Some(ref b) = self.subject {
178            builder = builder.subject_name(b);
179        }
180        if let Some(ref b) = self.spki {
181            builder = builder.public_key_der(b);
182        }
183        for (oid, critical, value_bytes) in &self.extensions {
184            builder = builder.add_extension(oid.clone(), *critical, value_bytes);
185        }
186
187        // Sign and assemble the CSR DER.
188        let signer = key.as_signer(algorithm);
189        let csr_der = builder
190            .sign(&signer)
191            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
192
193        // Parse the assembled DER back into a PyCsr.
194        let py_bytes = PyBytes::new(py, &csr_der);
195        let csr = PyCsr::new_from_der(py, py_bytes)?;
196        Py::new(py, csr).map(|py_csr| py_csr.into_bound(py))
197    }
198
199    fn __repr__(&self) -> String {
200        let subject = self
201            .subject
202            .as_ref()
203            .map(|b| format!("{} bytes", b.len()))
204            .unwrap_or_else(|| "not set".to_string());
205        format!("CsrBuilder(subject={subject})")
206    }
207}