Skip to main content

_synta/certificate/
key_types.rs

1//! ML-DSA, RSA public-key, and RSASSA-PSS parameter types for the Python binding.
2
3use pyo3::prelude::*;
4use pyo3::types::PyBytes;
5
6use synta::{Decoder, Encoding};
7
8use crate::types::PyObjectIdentifier;
9
10// ── ML-DSA private key ────────────────────────────────────────────────────────
11
12/// A parsed ML-DSA private key (RFC 9881 / FIPS 204).
13///
14/// ML-DSA private keys are DER-encoded ``CHOICE`` structures with three
15/// possible representations:
16///
17/// - ``"seed"`` — only the 32-octet seed; the expanded key is rederived on use.
18/// - ``"expanded_key"`` — only the pre-expanded key material; faster signing.
19/// - ``"both"`` — seed and expanded key stored together.
20///
21/// The :attr:`parameter_set` property exposes the algorithm
22/// :class:`~synta.ObjectIdentifier` (one of ``synta.oids.ML_DSA_44``,
23/// ``synta.oids.ML_DSA_65``, or ``synta.oids.ML_DSA_87``).
24///
25/// Use :meth:`from_der` to decode the ``privateKey`` value bytes from a
26/// ``OneAsymmetricKey`` / PKCS\#8 structure.  Pass the algorithm OID from the
27/// ``OneAsymmetricKey.algorithm`` field so the correct parameter set is used
28/// automatically.
29#[pyclass(frozen, name = "MlDsaPrivateKey")]
30pub struct PyMlDsaPrivateKey {
31    parameter_set: Py<PyObjectIdentifier>,
32    kind: &'static str,
33    seed: Option<Py<PyBytes>>,
34    expanded_key: Option<Py<PyBytes>>,
35}
36
37#[pymethods]
38impl PyMlDsaPrivateKey {
39    /// Decode an ML-DSA private key CHOICE from DER bytes.
40    ///
41    /// ``algorithm_oid`` selects the ML-DSA parameter set and must be one of
42    /// ``synta.oids.ML_DSA_44``, ``synta.oids.ML_DSA_65``, or
43    /// ``synta.oids.ML_DSA_87`` (or equivalent dotted-decimal strings).
44    /// Pass the OID directly from the ``OneAsymmetricKey.algorithm`` field so
45    /// the parameter set is discovered automatically without hard-coding numbers.
46    ///
47    /// ``data`` must be the raw ``privateKey`` value bytes from a
48    /// ``OneAsymmetricKey`` / PKCS\#8 structure.
49    ///
50    /// :param algorithm_oid: :class:`~synta.ObjectIdentifier` or dotted-decimal
51    ///     ``str``; must be one of the ``synta.oids.ML_DSA_*`` constants.
52    /// :param data: DER bytes of the ``ML-DSA-{44,65,87}-PrivateKey`` CHOICE.
53    /// :returns: :class:`MlDsaPrivateKey`
54    /// :raises ValueError: on unknown OID or malformed DER.
55    ///
56    /// ```python,ignore
57    /// # oid comes from parsing the OneAsymmetricKey structure:
58    /// key = synta.MlDsaPrivateKey.from_der(oid, private_key_bytes)
59    /// if key.kind == "both":
60    ///     seed = key.seed
61    ///     expanded = key.expanded_key
62    /// ```
63    #[staticmethod]
64    fn from_der(
65        py: Python<'_>,
66        algorithm_oid: &Bound<'_, PyAny>,
67        data: &Bound<'_, PyBytes>,
68    ) -> PyResult<Self> {
69        use synta_certificate::oids;
70        use synta_certificate::{
71            MlDsa44PrivateKey, MlDsa44PrivateKeyBoth, MlDsa65PrivateKey, MlDsa65PrivateKeyBoth,
72            MlDsa87PrivateKey, MlDsa87PrivateKeyBoth,
73        };
74
75        let oid = super::oid_from_pyany(algorithm_oid)?;
76        let comps = oid.components();
77        let raw = data.as_bytes();
78        let mut decoder = Decoder::new(raw, Encoding::Der);
79
80        macro_rules! to_py {
81            ($slice:expr) => {
82                PyBytes::new(py, $slice).unbind()
83            };
84        }
85
86        let (kind, seed, expanded_key) = if comps == oids::ML_DSA_44 {
87            let key: MlDsa44PrivateKey<'_> = decoder
88                .decode()
89                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
90            match key {
91                MlDsa44PrivateKey::Seed(s) => ("seed", Some(to_py!(s.as_bytes())), None),
92                MlDsa44PrivateKey::ExpandedKey(ek) => {
93                    ("expanded_key", None, Some(to_py!(ek.as_bytes())))
94                }
95                MlDsa44PrivateKey::Both(MlDsa44PrivateKeyBoth {
96                    seed: s,
97                    expanded_key: ek,
98                }) => (
99                    "both",
100                    Some(to_py!(s.as_bytes())),
101                    Some(to_py!(ek.as_bytes())),
102                ),
103            }
104        } else if comps == oids::ML_DSA_65 {
105            let key: MlDsa65PrivateKey<'_> = decoder
106                .decode()
107                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
108            match key {
109                MlDsa65PrivateKey::Seed(s) => ("seed", Some(to_py!(s.as_bytes())), None),
110                MlDsa65PrivateKey::ExpandedKey(ek) => {
111                    ("expanded_key", None, Some(to_py!(ek.as_bytes())))
112                }
113                MlDsa65PrivateKey::Both(MlDsa65PrivateKeyBoth {
114                    seed: s,
115                    expanded_key: ek,
116                }) => (
117                    "both",
118                    Some(to_py!(s.as_bytes())),
119                    Some(to_py!(ek.as_bytes())),
120                ),
121            }
122        } else if comps == oids::ML_DSA_87 {
123            let key: MlDsa87PrivateKey<'_> = decoder
124                .decode()
125                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
126            match key {
127                MlDsa87PrivateKey::Seed(s) => ("seed", Some(to_py!(s.as_bytes())), None),
128                MlDsa87PrivateKey::ExpandedKey(ek) => {
129                    ("expanded_key", None, Some(to_py!(ek.as_bytes())))
130                }
131                MlDsa87PrivateKey::Both(MlDsa87PrivateKeyBoth {
132                    seed: s,
133                    expanded_key: ek,
134                }) => (
135                    "both",
136                    Some(to_py!(s.as_bytes())),
137                    Some(to_py!(ek.as_bytes())),
138                ),
139            }
140        } else {
141            return Err(pyo3::exceptions::PyValueError::new_err(format!(
142                "unknown ML-DSA algorithm OID: {oid}; expected one of ML_DSA_44, ML_DSA_65, ML_DSA_87",
143            )));
144        };
145
146        let ps = Py::new(py, PyObjectIdentifier::from_oid(oid))
147            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
148        Ok(Self {
149            parameter_set: ps,
150            kind,
151            seed,
152            expanded_key,
153        })
154    }
155
156    /// The algorithm :class:`~synta.ObjectIdentifier` identifying the ML-DSA
157    /// parameter set (one of ``synta.oids.ML_DSA_44``, ``synta.oids.ML_DSA_65``,
158    /// or ``synta.oids.ML_DSA_87``).
159    #[getter]
160    fn parameter_set<'py>(&self, py: Python<'py>) -> Bound<'py, PyObjectIdentifier> {
161        self.parameter_set.bind(py).clone()
162    }
163
164    /// Which representation is stored: ``"seed"``, ``"expanded_key"``, or
165    /// ``"both"``.
166    #[getter]
167    fn kind(&self) -> &str {
168        self.kind
169    }
170
171    /// The 32-octet seed bytes, or ``None`` when :attr:`kind` is
172    /// ``"expanded_key"``.
173    #[getter]
174    fn seed<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
175        self.seed.as_ref().map(|b| b.bind(py).clone())
176    }
177
178    /// The expanded key bytes, or ``None`` when :attr:`kind` is ``"seed"``.
179    #[getter]
180    fn expanded_key<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
181        self.expanded_key.as_ref().map(|b| b.bind(py).clone())
182    }
183
184    fn __repr__(&self, py: Python<'_>) -> String {
185        let oid_str = self.parameter_set.bind(py).borrow().inner.to_string();
186        format!(
187            "MlDsaPrivateKey(parameter_set={oid_str:?}, kind={:?})",
188            self.kind
189        )
190    }
191}
192
193// ── ML-DSA public key ─────────────────────────────────────────────────────────
194
195/// A validated ML-DSA public key (RFC 9881 / FIPS 204).
196///
197/// ML-DSA public keys are raw byte strings carried in
198/// ``SubjectPublicKeyInfo.subjectPublicKey`` (as BIT STRING value bytes, with
199/// the unused-bits prefix stripped).  The expected sizes are:
200///
201/// - ``ML_DSA_44`` — 1312 bytes
202/// - ``ML_DSA_65`` — 1952 bytes
203/// - ``ML_DSA_87`` — 2592 bytes
204///
205/// Use :meth:`from_bytes` with the algorithm OID from
206/// ``SubjectPublicKeyInfo.algorithm`` to validate and wrap the raw key bytes:
207///
208/// ```python,ignore
209/// oid = cert.subject_public_key_algorithm
210/// raw = cert.subject_public_key
211/// pub_key = synta.MlDsaPublicKey.from_bytes(oid, raw)
212/// ```
213#[pyclass(frozen, name = "MlDsaPublicKey")]
214pub struct PyMlDsaPublicKey {
215    parameter_set: Py<PyObjectIdentifier>,
216    key: Py<PyBytes>,
217}
218
219#[pymethods]
220impl PyMlDsaPublicKey {
221    /// Validate and wrap raw ML-DSA public key bytes.
222    ///
223    /// ``algorithm_oid`` must be one of ``synta.oids.ML_DSA_44``,
224    /// ``synta.oids.ML_DSA_65``, or ``synta.oids.ML_DSA_87`` (or an
225    /// equivalent dotted-decimal string).  The byte length of ``data`` is
226    /// checked against the size mandated by the OID (1312, 1952, or 2592).
227    ///
228    /// ``data`` must be the raw key bytes — **not** a DER-encoded OCTET STRING.
229    /// When working with a ``SubjectPublicKeyInfo``, pass the value bytes of
230    /// the ``subjectPublicKey`` BIT STRING directly (unused-bits byte already
231    /// stripped), which :attr:`~synta.Certificate.subject_public_key` provides.
232    ///
233    /// :param algorithm_oid: :class:`~synta.ObjectIdentifier` or dotted-decimal
234    ///     ``str``; must be one of the ``synta.oids.ML_DSA_*`` constants.
235    /// :param data: raw public key bytes.
236    /// :returns: :class:`MlDsaPublicKey`
237    /// :raises ValueError: on unknown OID or wrong byte length.
238    ///
239    /// ```python,ignore
240    /// pub_key = synta.MlDsaPublicKey.from_bytes(synta.oids.ML_DSA_65, raw)
241    /// assert len(pub_key) == 1952
242    /// ```
243    #[staticmethod]
244    fn from_bytes(
245        py: Python<'_>,
246        algorithm_oid: &Bound<'_, PyAny>,
247        data: &Bound<'_, PyBytes>,
248    ) -> PyResult<Self> {
249        use synta_certificate::oids;
250
251        let oid = super::oid_from_pyany(algorithm_oid)?;
252        let comps = oid.components();
253        let raw = data.as_bytes();
254
255        // Expected sizes per RFC 9881 §3 and FIPS 204.
256        let expected_len = if comps == oids::ML_DSA_44 {
257            1312usize
258        } else if comps == oids::ML_DSA_65 {
259            1952
260        } else if comps == oids::ML_DSA_87 {
261            2592
262        } else {
263            return Err(pyo3::exceptions::PyValueError::new_err(format!(
264                "unknown ML-DSA algorithm OID: {oid}; \
265                 expected one of ML_DSA_44, ML_DSA_65, ML_DSA_87",
266            )));
267        };
268
269        if raw.len() != expected_len {
270            return Err(pyo3::exceptions::PyValueError::new_err(format!(
271                "ML-DSA public key has wrong length: expected {expected_len} bytes, got {}",
272                raw.len()
273            )));
274        }
275
276        let ps = Py::new(py, PyObjectIdentifier::from_oid(oid))
277            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
278        Ok(Self {
279            parameter_set: ps,
280            key: PyBytes::new(py, raw).unbind(),
281        })
282    }
283
284    /// The algorithm :class:`~synta.ObjectIdentifier` identifying the ML-DSA
285    /// parameter set (one of ``synta.oids.ML_DSA_44``, ``synta.oids.ML_DSA_65``,
286    /// or ``synta.oids.ML_DSA_87``).
287    #[getter]
288    fn parameter_set<'py>(&self, py: Python<'py>) -> Bound<'py, PyObjectIdentifier> {
289        self.parameter_set.bind(py).clone()
290    }
291
292    /// The raw public key bytes (1312, 1952, or 2592 octets depending on the
293    /// parameter set).
294    #[getter]
295    fn key<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
296        self.key.bind(py).clone()
297    }
298
299    fn __len__(&self, py: Python<'_>) -> usize {
300        self.key.bind(py).as_bytes().len()
301    }
302
303    fn __repr__(&self, py: Python<'_>) -> String {
304        let oid_str = self.parameter_set.bind(py).borrow().inner.to_string();
305        let key_len = self.key.bind(py).as_bytes().len();
306        format!("MlDsaPublicKey(parameter_set={oid_str:?}, key=<{key_len} bytes>)")
307    }
308}
309
310// ── RSA public key ────────────────────────────────────────────────────────────
311
312/// A parsed RSA public key (RFC 8017 / PKCS \#1).
313///
314/// Holds the two components of an RSA public key:
315///
316/// - :attr:`modulus` — the RSA modulus ``n`` as big-endian bytes
317/// - :attr:`public_exponent` — the public exponent ``e`` as big-endian bytes
318///   (commonly ``b'\x01\x00\x01'`` for 65537)
319///
320/// Use :meth:`from_der` to decode the ``BIT STRING`` payload from a
321/// ``SubjectPublicKeyInfo`` structure (after stripping the leading zero byte).
322#[pyclass(frozen, name = "RsaPublicKey")]
323pub struct PyRsaPublicKey {
324    modulus: Py<PyBytes>,
325    public_exponent: Py<PyBytes>,
326}
327
328#[pymethods]
329impl PyRsaPublicKey {
330    /// Decode an RSA public key from DER bytes.
331    ///
332    /// ``data`` must be the DER encoding of ``SEQUENCE { modulus INTEGER,
333    /// publicExponent INTEGER }``.  This is the inner payload of the
334    /// ``BIT STRING`` in a ``SubjectPublicKeyInfo`` (strip the leading ``0x00``
335    /// padding byte before passing here).
336    ///
337    /// :param data: DER bytes of the ``RSAPublicKey`` SEQUENCE.
338    /// :returns: :class:`RsaPublicKey`
339    /// :raises ValueError: on malformed DER.
340    ///
341    /// ```python,ignore
342    /// pub_key = synta.RsaPublicKey.from_der(spki_bitstring_value[1:])
343    /// print(len(pub_key.modulus))   # e.g. 256 for RSA-2048
344    /// ```
345    #[staticmethod]
346    fn from_der(py: Python<'_>, data: &Bound<'_, PyBytes>) -> PyResult<Self> {
347        use synta_certificate::pkcs1_types::RsaPublicKey;
348        let raw = data.as_bytes();
349        let mut decoder = Decoder::new(raw, Encoding::Der);
350        let key: RsaPublicKey = decoder
351            .decode()
352            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
353        Ok(Self {
354            modulus: PyBytes::new(py, key.modulus.as_bytes()).unbind(),
355            public_exponent: PyBytes::new(py, key.public_exponent.as_bytes()).unbind(),
356        })
357    }
358
359    /// The RSA modulus ``n`` as big-endian bytes.
360    #[getter]
361    fn modulus<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
362        self.modulus.bind(py).clone()
363    }
364
365    /// The public exponent ``e`` as big-endian bytes (commonly ``b'\x01\x00\x01'``).
366    #[getter]
367    fn public_exponent<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
368        self.public_exponent.bind(py).clone()
369    }
370
371    fn __repr__(&self, py: Python<'_>) -> String {
372        let mod_len = self.modulus.bind(py).as_bytes().len();
373        format!("RsaPublicKey(modulus=<{mod_len} bytes>)")
374    }
375}
376
377// ── RSASSA-PSS algorithm parameters ──────────────────────────────────────────
378
379/// Parsed RSASSA-PSS algorithm parameters (RFC 8017 §A.2.3).
380///
381/// These appear in the ``parameters`` field of an ``AlgorithmIdentifier``
382/// whose ``algorithm`` OID is ``id-RSASSA-PSS`` (``synta.oids.RSASSA_PSS``).
383///
384/// All fields are ``None`` when the corresponding ASN.1 field is absent (they
385/// all have DEFAULT values in the RFC; absent means the RFC default applies).
386///
387/// - :attr:`hash_algorithm` — OID identifying the hash function (e.g.
388///   ``id-sha256``); ``None`` → default SHA-1.
389/// - :attr:`mask_gen_algorithm` — OID identifying the mask-generation function
390///   (almost always MGF1, ``synta.oids.MGF1``); ``None`` → default MGF1.
391/// - :attr:`mask_gen_hash_algorithm` — OID of the hash function *inside* MGF1;
392///   decoded from the ``parameters`` of the mask-gen ``AlgorithmIdentifier``.
393/// - :attr:`salt_length` — salt length in bytes; ``None`` → default 20.
394/// - :attr:`trailer_field` — trailer field integer; ``None`` → default 1 (BC).
395#[pyclass(frozen, name = "RsassaPssParams")]
396pub struct PyRsassaPssParams {
397    hash_algorithm: Option<Py<PyObjectIdentifier>>,
398    mask_gen_algorithm: Option<Py<PyObjectIdentifier>>,
399    mask_gen_hash_algorithm: Option<Py<PyObjectIdentifier>>,
400    salt_length: Option<i64>,
401    trailer_field: Option<i64>,
402}
403
404#[pymethods]
405impl PyRsassaPssParams {
406    /// Decode RSASSA-PSS parameters from DER bytes.
407    ///
408    /// ``data`` must be the DER encoding of the ``RSASSA-PSS-params`` SEQUENCE
409    /// from the ``parameters`` field of an ``AlgorithmIdentifier`` whose OID is
410    /// ``synta.oids.RSASSA_PSS``.
411    ///
412    /// :param data: DER bytes of the ``RSASSA-PSS-params`` SEQUENCE.
413    /// :returns: :class:`RsassaPssParams`
414    /// :raises ValueError: on malformed DER.
415    ///
416    /// ```python,ignore
417    /// # alg_params_der comes from cert.signature_algorithm_parameters_der()
418    /// pss = synta.RsassaPssParams.from_der(alg_params_der)
419    /// print(pss.hash_algorithm)      # e.g. ObjectIdentifier("2.16.840.1.101.3.4.2.1")
420    /// print(pss.salt_length)         # e.g. 32
421    /// ```
422    #[staticmethod]
423    fn from_der(py: Python<'_>, data: &Bound<'_, PyBytes>) -> PyResult<Self> {
424        use synta::{Encode, Encoder as SyntaEncoder};
425        use synta_certificate::pkcs1_types::RsassaPssParams;
426        let raw = data.as_bytes();
427        let mut decoder = Decoder::new(raw, Encoding::Der);
428        let params: RsassaPssParams<'_> = decoder
429            .decode()
430            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
431
432        let mk_oid = |oid: synta::ObjectIdentifier| -> PyResult<Py<PyObjectIdentifier>> {
433            Py::new(py, PyObjectIdentifier::from_oid(oid))
434                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
435        };
436
437        let hash_algorithm = params
438            .hash_algorithm
439            .map(|a| mk_oid(a.algorithm))
440            .transpose()?;
441
442        let (mask_gen_algorithm, mask_gen_hash_algorithm) = match params.mask_gen_algorithm {
443            None => (None, None),
444            Some(mga) => {
445                let mga_oid = mk_oid(mga.algorithm)?;
446                // Decode the inner hash AlgorithmIdentifier from mga.parameters.
447                let inner_hash_oid = mga.parameters.and_then(|elem| {
448                    let mut enc = SyntaEncoder::new(Encoding::Der);
449                    elem.encode(&mut enc).ok()?;
450                    let bytes = enc.finish().ok()?;
451                    let mut dec = Decoder::new(bytes.as_slice(), Encoding::Der);
452                    // SAFETY: we decode AlgorithmIdentifier<'_> but bytes is dropped after
453                    // the OID is cloned below.  The decode is sound because we clone the OID.
454                    let alg_id: synta_certificate::AlgorithmIdentifier<'_> = dec.decode().ok()?;
455                    Some(alg_id.algorithm)
456                });
457                let inner = inner_hash_oid.map(mk_oid).transpose()?;
458                (Some(mga_oid), inner)
459            }
460        };
461
462        let salt_length = params
463            .salt_length
464            .map(|i| {
465                i.as_i64()
466                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
467            })
468            .transpose()?;
469
470        let trailer_field = params
471            .trailer_field
472            .map(|i| {
473                i.as_i64()
474                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
475            })
476            .transpose()?;
477
478        Ok(Self {
479            hash_algorithm,
480            mask_gen_algorithm,
481            mask_gen_hash_algorithm,
482            salt_length,
483            trailer_field,
484        })
485    }
486
487    /// OID of the hash function (``None`` → RFC default SHA-1).
488    #[getter]
489    fn hash_algorithm<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyObjectIdentifier>> {
490        self.hash_algorithm.as_ref().map(|o| o.bind(py).clone())
491    }
492
493    /// OID of the mask-generation function (``None`` → RFC default MGF1).
494    #[getter]
495    fn mask_gen_algorithm<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyObjectIdentifier>> {
496        self.mask_gen_algorithm.as_ref().map(|o| o.bind(py).clone())
497    }
498
499    /// OID of the hash function *inside* MGF1 (``None`` if not decoded).
500    #[getter]
501    fn mask_gen_hash_algorithm<'py>(
502        &self,
503        py: Python<'py>,
504    ) -> Option<Bound<'py, PyObjectIdentifier>> {
505        self.mask_gen_hash_algorithm
506            .as_ref()
507            .map(|o| o.bind(py).clone())
508    }
509
510    /// Salt length in bytes (``None`` → RFC default 20).
511    #[getter]
512    fn salt_length(&self) -> Option<i64> {
513        self.salt_length
514    }
515
516    /// Trailer field integer (``None`` → RFC default 1).
517    #[getter]
518    fn trailer_field(&self) -> Option<i64> {
519        self.trailer_field
520    }
521
522    fn __repr__(&self, py: Python<'_>) -> String {
523        let hash = self
524            .hash_algorithm
525            .as_ref()
526            .map(|o| o.bind(py).borrow().inner.to_string())
527            .unwrap_or_else(|| "SHA-1 (default)".to_string());
528        let salt = self.salt_length.unwrap_or(20);
529        format!("RsassaPssParams(hash_algorithm={hash:?}, salt_length={salt})")
530    }
531}