_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}