Skip to main content

_synta/
pkinit.rs

1//! Python bindings for PKINIT types (RFC 4556 + RFC 6112 + RFC 8636).
2//!
3//! Exposes the ASN.1 structures used in PKINIT AS exchanges as frozen
4//! Python classes under the ``synta.krb5`` namespace.
5//!
6//! # Classes
7//!
8//! | Rust type                       | Python name                   | RFC / section   |
9//! |---------------------------------|-------------------------------|-----------------|
10//! | [`PyEncryptionKey`]             | `EncryptionKey`               | RFC 3961 §2     |
11//! | [`PyChecksum`]                  | `Checksum`                    | RFC 3961 §4     |
12//! | [`PyKDFAlgorithmId`]            | `KDFAlgorithmId`              | RFC 8636 §3.1   |
13//! | [`PyIssuerAndSerialNumber`]     | `IssuerAndSerialNumber`       | RFC 4556 §3.2.2 |
14//! | [`PyExternalPrincipalIdentifier`]| `ExternalPrincipalIdentifier`| RFC 4556 §3.2.2 |
15//! | [`PyPKAuthenticator`]           | `PKAuthenticator`             | RFC 4556 §3.2.1 |
16//! | [`PyAuthPack`]                  | `AuthPack`                    | RFC 4556 §3.2.1 |
17//! | [`PyPaPkAsReq`]                 | `PaPkAsReq`                   | RFC 4556 §3.2.2 |
18//! | [`PyDHRepInfo`]                 | `DHRepInfo`                   | RFC 4556 §3.2.4 |
19//! | [`PyKDCDHKeyInfo`]              | `KDCDHKeyInfo`                | RFC 4556 §3.2.4 |
20//! | [`PyReplyKeyPack`]              | `ReplyKeyPack`                | RFC 4556 §3.2.3 |
21//! | [`PyPaPkAsRep`]                 | `PaPkAsRep`                   | RFC 4556 §3.2.4 |
22//!
23//! Call [`register_pkinit_classes`] from
24//! ``synta_python::krb5::register_krb5_module`` to install the classes.
25
26use pyo3::exceptions::PyOverflowError;
27use pyo3::prelude::*;
28use pyo3::types::{PyBytes, PyList};
29
30use crate::error::SyntaErr;
31use crate::types::PyObjectIdentifier;
32use synta::traits::Encode;
33use synta::{Decoder, Encoding, ObjectIdentifier};
34
35// ── helpers ───────────────────────────────────────────────────────────────────
36
37/// Re-encode a synta `Element<'_>` to owned DER bytes.
38fn encode_element_to_vec(elem: &synta::Element<'_>) -> PyResult<Vec<u8>> {
39    let mut encoder = synta::Encoder::new(Encoding::Der);
40    elem.encode(&mut encoder).map_err(SyntaErr)?;
41    Ok(encoder.finish().map_err(SyntaErr)?)
42}
43
44// ── Leaf classes (no nested pyclass fields) ───────────────────────────────────
45
46/// Kerberos EncryptionKey — RFC 3961 §2 / RFC 4556
47///
48/// Contains the encryption algorithm type identifier and the raw key material.
49///
50/// ```python
51/// key = synta.krb5.EncryptionKey.from_der(der_bytes)
52/// print(key.keytype, key.keyvalue.hex())
53/// ```
54#[pyclass(frozen, name = "EncryptionKey")]
55pub struct PyEncryptionKey {
56    keytype: i64,
57    keyvalue: Vec<u8>,
58}
59
60#[pymethods]
61impl PyEncryptionKey {
62    /// Parse a DER-encoded EncryptionKey SEQUENCE.
63    #[staticmethod]
64    fn from_der(data: &[u8]) -> PyResult<Self> {
65        let mut dec = Decoder::new(data, Encoding::Der);
66        let parsed: synta_krb5::pkinit::EncryptionKey<'_> = dec.decode().map_err(SyntaErr)?;
67        Ok(Self {
68            keytype: parsed
69                .keytype
70                .as_i64()
71                .map_err(|_| PyOverflowError::new_err("keytype out of i64 range"))?,
72            keyvalue: parsed.keyvalue.as_bytes().to_vec(),
73        })
74    }
75
76    /// Kerberos encryption type number (RFC 3961 Table 1).
77    #[getter]
78    fn keytype(&self) -> i64 {
79        self.keytype
80    }
81
82    /// Raw key material bytes.
83    #[getter]
84    fn keyvalue<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
85        PyBytes::new(py, &self.keyvalue)
86    }
87
88    fn __repr__(&self) -> String {
89        format!(
90            "EncryptionKey(keytype={}, keyvalue=<{} bytes>)",
91            self.keytype,
92            self.keyvalue.len()
93        )
94    }
95}
96
97/// Kerberos Checksum — RFC 3961 §4 / RFC 4556
98///
99/// Contains the checksum algorithm type identifier and the raw checksum value.
100///
101/// ```python
102/// ck = synta.krb5.Checksum.from_der(der_bytes)
103/// print(ck.cksumtype, ck.checksum.hex())
104/// ```
105#[pyclass(frozen, name = "Checksum")]
106pub struct PyChecksum {
107    cksumtype: i64,
108    checksum: Vec<u8>,
109}
110
111#[pymethods]
112impl PyChecksum {
113    /// Parse a DER-encoded Checksum SEQUENCE.
114    #[staticmethod]
115    fn from_der(data: &[u8]) -> PyResult<Self> {
116        let mut dec = Decoder::new(data, Encoding::Der);
117        let parsed: synta_krb5::pkinit::Checksum<'_> = dec.decode().map_err(SyntaErr)?;
118        Ok(Self {
119            cksumtype: parsed
120                .cksumtype
121                .as_i64()
122                .map_err(|_| PyOverflowError::new_err("cksumtype out of i64 range"))?,
123            checksum: parsed.checksum.as_bytes().to_vec(),
124        })
125    }
126
127    /// Kerberos checksum type number.
128    #[getter]
129    fn cksumtype(&self) -> i64 {
130        self.cksumtype
131    }
132
133    /// Raw checksum bytes.
134    #[getter]
135    fn checksum<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
136        PyBytes::new(py, &self.checksum)
137    }
138
139    fn __repr__(&self) -> String {
140        format!(
141            "Checksum(cksumtype={}, checksum=<{} bytes>)",
142            self.cksumtype,
143            self.checksum.len()
144        )
145    }
146}
147
148/// PKINIT KDF algorithm identifier — RFC 8636 §3.1
149///
150/// Identifies the key-derivation function used in a PKINIT exchange.
151///
152/// ```python
153/// kdf = synta.krb5.KDFAlgorithmId.from_der(der_bytes)
154/// print(kdf.kdf_id)   # ObjectIdentifier("1.3.6.1.5.2.3.6.2")
155/// ```
156#[pyclass(frozen, name = "KDFAlgorithmId")]
157pub struct PyKDFAlgorithmId {
158    kdf_id: ObjectIdentifier,
159}
160
161#[pymethods]
162impl PyKDFAlgorithmId {
163    /// Parse a DER-encoded KDFAlgorithmId SEQUENCE.
164    #[staticmethod]
165    fn from_der(data: &[u8]) -> PyResult<Self> {
166        let mut dec = Decoder::new(data, Encoding::Der);
167        let parsed: synta_krb5::pkinit::KDFAlgorithmId = dec.decode().map_err(SyntaErr)?;
168        Ok(Self {
169            kdf_id: parsed.kdf_id,
170        })
171    }
172
173    /// Key-derivation function OID.
174    #[getter]
175    fn kdf_id(&self, py: Python<'_>) -> PyResult<Py<PyObjectIdentifier>> {
176        Py::new(py, PyObjectIdentifier::from_oid(self.kdf_id.clone()))
177    }
178
179    fn __repr__(&self) -> String {
180        format!("KDFAlgorithmId(kdf_id='{}')", self.kdf_id)
181    }
182}
183
184/// PKINIT IssuerAndSerialNumber — RFC 4556 §3.2.2
185///
186/// Identifies a certificate by its issuer Name and serial number.
187///
188/// ```python
189/// isn = synta.krb5.IssuerAndSerialNumber.from_der(der_bytes)
190/// print(isn.serial_number)
191/// ```
192#[pyclass(frozen, name = "IssuerAndSerialNumber")]
193pub struct PyIssuerAndSerialNumber {
194    issuer: Vec<u8>,
195    serial_number_bytes: Vec<u8>,
196}
197
198#[pymethods]
199impl PyIssuerAndSerialNumber {
200    /// Parse a DER-encoded IssuerAndSerialNumber SEQUENCE.
201    #[staticmethod]
202    fn from_der(data: &[u8]) -> PyResult<Self> {
203        let mut dec = Decoder::new(data, Encoding::Der);
204        let parsed: synta_krb5::pkinit::IssuerAndSerialNumber<'_> =
205            dec.decode().map_err(SyntaErr)?;
206        Ok(Self {
207            issuer: parsed.issuer.as_bytes().to_vec(),
208            serial_number_bytes: parsed.serial_number.as_bytes().to_vec(),
209        })
210    }
211
212    /// DER-encoded issuer Name bytes (raw Name SEQUENCE).
213    #[getter]
214    fn issuer<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
215        PyBytes::new(py, &self.issuer)
216    }
217
218    /// Certificate serial number as a Python :class:`int`.
219    #[getter]
220    fn serial_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
221        let bytes_obj = PyBytes::new(py, &self.serial_number_bytes);
222        let kwargs = pyo3::types::PyDict::new(py);
223        kwargs.set_item(pyo3::intern!(py, "signed"), true)?;
224        py.get_type::<pyo3::types::PyInt>().call_method(
225            "from_bytes",
226            (bytes_obj, "big"),
227            Some(&kwargs),
228        )
229    }
230
231    fn __repr__(&self) -> String {
232        format!(
233            "IssuerAndSerialNumber(issuer=<{} bytes>, serial=<{} bytes>)",
234            self.issuer.len(),
235            self.serial_number_bytes.len(),
236        )
237    }
238}
239
240/// PKINIT PKAuthenticator — RFC 4556 §3.2.1
241///
242/// The client's proof of liveness in a PKINIT AS-REQ.
243///
244/// ```python
245/// auth = synta.krb5.PKAuthenticator.from_der(der_bytes)
246/// print(auth.ctime, auth.nonce)
247/// ```
248#[pyclass(frozen, name = "PKAuthenticator")]
249pub struct PyPKAuthenticator {
250    cusec: i64,
251    ctime: String,
252    nonce: i64,
253    pa_checksum: Option<Vec<u8>>,
254    freshness_token: Option<Vec<u8>>,
255}
256
257#[pymethods]
258impl PyPKAuthenticator {
259    /// Parse a DER-encoded PKAuthenticator SEQUENCE.
260    #[staticmethod]
261    fn from_der(data: &[u8]) -> PyResult<Self> {
262        let mut dec = Decoder::new(data, Encoding::Der);
263        let parsed: synta_krb5::pkinit::PKAuthenticator<'_> = dec.decode().map_err(SyntaErr)?;
264        Ok(Self {
265            cusec: parsed
266                .cusec
267                .as_i64()
268                .map_err(|_| PyOverflowError::new_err("cusec out of i64 range"))?,
269            ctime: parsed.ctime.to_string(),
270            nonce: parsed
271                .nonce
272                .as_i64()
273                .map_err(|_| PyOverflowError::new_err("nonce out of i64 range"))?,
274            pa_checksum: parsed.pa_checksum.map(|s| s.as_bytes().to_vec()),
275            freshness_token: parsed.freshness_token.map(|s| s.as_bytes().to_vec()),
276        })
277    }
278
279    /// Microsecond component of the client's current time (0–999 999).
280    #[getter]
281    fn cusec(&self) -> i64 {
282        self.cusec
283    }
284
285    /// Client's current time as a string (``YYYYMMDDHHMMSSz``).
286    #[getter]
287    fn ctime(&self) -> &str {
288        &self.ctime
289    }
290
291    /// Nonce preventing replay attacks.
292    #[getter]
293    fn nonce(&self) -> i64 {
294        self.nonce
295    }
296
297    /// Optional SHA-1 checksum of the DER-encoded AS-REQ body, or ``None``.
298    #[getter]
299    fn pa_checksum<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
300        self.pa_checksum.as_deref().map(|b| PyBytes::new(py, b))
301    }
302
303    /// Optional freshness token from the KDC (RFC 8070), or ``None``.
304    #[getter]
305    fn freshness_token<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
306        self.freshness_token.as_deref().map(|b| PyBytes::new(py, b))
307    }
308
309    fn __repr__(&self) -> String {
310        format!(
311            "PKAuthenticator(ctime='{}', nonce={})",
312            self.ctime, self.nonce
313        )
314    }
315}
316
317/// PKINIT DHRepInfo — RFC 4556 §3.2.4
318///
319/// Carries the KDC's Diffie-Hellman reply inside a PA-PK-AS-REP.
320///
321/// ```python
322/// rep = synta.krb5.DHRepInfo.from_der(der_bytes)
323/// print(len(rep.dh_signed_data))
324/// ```
325#[pyclass(frozen, name = "DHRepInfo")]
326pub struct PyDHRepInfo {
327    dh_signed_data: Vec<u8>,
328    server_dhnonce: Option<Vec<u8>>,
329}
330
331#[pymethods]
332impl PyDHRepInfo {
333    /// Parse a DER-encoded DHRepInfo SEQUENCE.
334    #[staticmethod]
335    fn from_der(data: &[u8]) -> PyResult<Self> {
336        let mut dec = Decoder::new(data, Encoding::Der);
337        let parsed: synta_krb5::pkinit::DHRepInfo<'_> = dec.decode().map_err(SyntaErr)?;
338        Ok(Self {
339            dh_signed_data: parsed.dh_signed_data.as_bytes().to_vec(),
340            server_dhnonce: parsed.server_dhnonce.map(|s| s.as_bytes().to_vec()),
341        })
342    }
343
344    /// CMS SignedData containing the KDC's DH public key.
345    #[getter]
346    fn dh_signed_data<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
347        PyBytes::new(py, &self.dh_signed_data)
348    }
349
350    /// Optional server DH nonce, or ``None``.
351    #[getter]
352    fn server_dhnonce<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
353        self.server_dhnonce.as_deref().map(|b| PyBytes::new(py, b))
354    }
355
356    fn __repr__(&self) -> String {
357        format!(
358            "DHRepInfo(dh_signed_data=<{} bytes>)",
359            self.dh_signed_data.len()
360        )
361    }
362}
363
364/// PKINIT KDCDHKeyInfo — RFC 4556 §3.2.4
365///
366/// The KDC's DH public key and associated PKINIT parameters.
367///
368/// ```python
369/// info = synta.krb5.KDCDHKeyInfo.from_der(der_bytes)
370/// print(info.nonce, info.subject_public_key.hex())
371/// ```
372#[pyclass(frozen, name = "KDCDHKeyInfo")]
373pub struct PyKDCDHKeyInfo {
374    subject_public_key: Vec<u8>,
375    nonce: i64,
376    dh_key_expiration: Option<String>,
377}
378
379#[pymethods]
380impl PyKDCDHKeyInfo {
381    /// Parse a DER-encoded KDCDHKeyInfo SEQUENCE.
382    #[staticmethod]
383    fn from_der(data: &[u8]) -> PyResult<Self> {
384        let mut dec = Decoder::new(data, Encoding::Der);
385        let parsed: synta_krb5::pkinit::KDCDHKeyInfo<'_> = dec.decode().map_err(SyntaErr)?;
386        Ok(Self {
387            subject_public_key: parsed.subject_public_key.as_bytes().to_vec(),
388            nonce: parsed
389                .nonce
390                .as_i64()
391                .map_err(|_| PyOverflowError::new_err("nonce out of i64 range"))?,
392            dh_key_expiration: parsed.dh_key_expiration.map(|t| t.to_string()),
393        })
394    }
395
396    /// KDC's DH public key as raw BIT STRING payload bytes.
397    #[getter]
398    fn subject_public_key<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
399        PyBytes::new(py, &self.subject_public_key)
400    }
401
402    /// Nonce echoed from the client's PA-PK-AS-REQ.
403    #[getter]
404    fn nonce(&self) -> i64 {
405        self.nonce
406    }
407
408    /// Optional DH key expiration time as a string (``YYYYMMDDHHMMSSz``), or ``None``.
409    #[getter]
410    fn dh_key_expiration(&self) -> Option<&str> {
411        self.dh_key_expiration.as_deref()
412    }
413
414    fn __repr__(&self) -> String {
415        format!(
416            "KDCDHKeyInfo(nonce={}, subject_public_key=<{} bytes>)",
417            self.nonce,
418            self.subject_public_key.len()
419        )
420    }
421}
422
423// ── Composite classes (contain nested pyclass fields) ─────────────────────────
424
425/// PKINIT ExternalPrincipalIdentifier — RFC 4556 §3.2.2
426///
427/// Identifies a certificate by subject name, issuer/serial pair, or SKI.
428/// All three fields are OPTIONAL; at least one SHOULD be present.
429///
430/// ```python
431/// epi = synta.krb5.ExternalPrincipalIdentifier.from_der(der_bytes)
432/// if epi.subject_key_identifier:
433///     print(epi.subject_key_identifier.hex())
434/// ```
435#[pyclass(frozen, name = "ExternalPrincipalIdentifier")]
436pub struct PyExternalPrincipalIdentifier {
437    subject_name: Option<Vec<u8>>,
438    issuer_and_serial_number: Option<Py<PyIssuerAndSerialNumber>>,
439    subject_key_identifier: Option<Vec<u8>>,
440}
441
442#[pymethods]
443impl PyExternalPrincipalIdentifier {
444    /// Parse a DER-encoded ExternalPrincipalIdentifier SEQUENCE.
445    #[staticmethod]
446    fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
447        let mut dec = Decoder::new(data, Encoding::Der);
448        let parsed: synta_krb5::pkinit::ExternalPrincipalIdentifier<'_> =
449            dec.decode().map_err(SyntaErr)?;
450        let issuer_and_serial_number = parsed
451            .issuer_and_serial_number
452            .map(|isn| {
453                Py::new(
454                    py,
455                    PyIssuerAndSerialNumber {
456                        issuer: isn.issuer.as_bytes().to_vec(),
457                        serial_number_bytes: isn.serial_number.as_bytes().to_vec(),
458                    },
459                )
460            })
461            .transpose()?;
462        Ok(Self {
463            subject_name: parsed.subject_name.map(|s| s.as_bytes().to_vec()),
464            issuer_and_serial_number,
465            subject_key_identifier: parsed.subject_key_identifier.map(|s| s.as_bytes().to_vec()),
466        })
467    }
468
469    /// DER-encoded subject Name, or ``None``.
470    #[getter]
471    fn subject_name<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
472        self.subject_name.as_deref().map(|b| PyBytes::new(py, b))
473    }
474
475    /// :class:`IssuerAndSerialNumber` identifying the certificate, or ``None``.
476    #[getter]
477    fn issuer_and_serial_number<'py>(
478        &self,
479        py: Python<'py>,
480    ) -> Option<Bound<'py, PyIssuerAndSerialNumber>> {
481        self.issuer_and_serial_number
482            .as_ref()
483            .map(|x| x.clone_ref(py).into_bound(py))
484    }
485
486    /// Subject key identifier bytes, or ``None``.
487    #[getter]
488    fn subject_key_identifier<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
489        self.subject_key_identifier
490            .as_deref()
491            .map(|b| PyBytes::new(py, b))
492    }
493}
494
495/// PKINIT ReplyKeyPack — RFC 4556 §3.2.3
496///
497/// The session key and checksum sent by the KDC to an authenticated client.
498///
499/// ```python
500/// pack = synta.krb5.ReplyKeyPack.from_der(der_bytes)
501/// print(pack.reply_key.keytype)
502/// ```
503#[pyclass(frozen, name = "ReplyKeyPack")]
504pub struct PyReplyKeyPack {
505    reply_key: Py<PyEncryptionKey>,
506    as_checksum: Py<PyChecksum>,
507}
508
509#[pymethods]
510impl PyReplyKeyPack {
511    /// Parse a DER-encoded ReplyKeyPack SEQUENCE.
512    #[staticmethod]
513    fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
514        let mut dec = Decoder::new(data, Encoding::Der);
515        let parsed: synta_krb5::pkinit::ReplyKeyPack<'_> = dec.decode().map_err(SyntaErr)?;
516        let reply_key = Py::new(
517            py,
518            PyEncryptionKey {
519                keytype: parsed
520                    .reply_key
521                    .keytype
522                    .as_i64()
523                    .map_err(|_| PyOverflowError::new_err("keytype out of i64 range"))?,
524                keyvalue: parsed.reply_key.keyvalue.as_bytes().to_vec(),
525            },
526        )?;
527        let as_checksum = Py::new(
528            py,
529            PyChecksum {
530                cksumtype: parsed
531                    .as_checksum
532                    .cksumtype
533                    .as_i64()
534                    .map_err(|_| PyOverflowError::new_err("cksumtype out of i64 range"))?,
535                checksum: parsed.as_checksum.checksum.as_bytes().to_vec(),
536            },
537        )?;
538        Ok(Self {
539            reply_key,
540            as_checksum,
541        })
542    }
543
544    /// The session key returned by the KDC.
545    #[getter]
546    fn reply_key<'py>(&self, py: Python<'py>) -> Bound<'py, PyEncryptionKey> {
547        self.reply_key.clone_ref(py).into_bound(py)
548    }
549
550    /// The AS-REQ checksum authenticating the exchange.
551    #[getter]
552    fn as_checksum<'py>(&self, py: Python<'py>) -> Bound<'py, PyChecksum> {
553        self.as_checksum.clone_ref(py).into_bound(py)
554    }
555
556    fn __repr__(&self) -> String {
557        String::from("ReplyKeyPack(...)")
558    }
559}
560
561/// PKINIT PA-PK-AS-REP — RFC 4556 §3.2.4
562///
563/// CHOICE: DH-based reply (``DhInfo``) or RSA-encrypted reply (``EncKeyPack``).
564///
565/// ```python
566/// rep = synta.krb5.PaPkAsRep.from_der(der_bytes)
567/// if rep.variant == "DhInfo":
568///     print(len(rep.dh_info.dh_signed_data))
569/// else:
570///     print(rep.enc_key_pack.hex())
571/// ```
572#[pyclass(frozen, name = "PaPkAsRep")]
573pub struct PyPaPkAsRep {
574    variant: &'static str,
575    dh_info: Option<Py<PyDHRepInfo>>,
576    enc_key_pack: Option<Vec<u8>>,
577}
578
579#[pymethods]
580impl PyPaPkAsRep {
581    /// Parse a DER-encoded PA-PK-AS-REP CHOICE.
582    #[staticmethod]
583    fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
584        let mut dec = Decoder::new(data, Encoding::Der);
585        let parsed: synta_krb5::pkinit::PaPkAsRep<'_> = dec.decode().map_err(SyntaErr)?;
586        match parsed {
587            synta_krb5::pkinit::PaPkAsRep::DhInfo(dh) => {
588                let dh_info = Py::new(
589                    py,
590                    PyDHRepInfo {
591                        dh_signed_data: dh.dh_signed_data.as_bytes().to_vec(),
592                        server_dhnonce: dh.server_dhnonce.map(|s| s.as_bytes().to_vec()),
593                    },
594                )?;
595                Ok(Self {
596                    variant: "DhInfo",
597                    dh_info: Some(dh_info),
598                    enc_key_pack: None,
599                })
600            }
601            synta_krb5::pkinit::PaPkAsRep::EncKeyPack(pack) => Ok(Self {
602                variant: "EncKeyPack",
603                dh_info: None,
604                enc_key_pack: Some(pack.as_bytes().to_vec()),
605            }),
606        }
607    }
608
609    /// Active CHOICE variant: ``"DhInfo"`` or ``"EncKeyPack"``.
610    #[getter]
611    fn variant(&self) -> &'static str {
612        self.variant
613    }
614
615    /// :class:`DHRepInfo` when the variant is ``"DhInfo"``, else ``None``.
616    #[getter]
617    fn dh_info<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyDHRepInfo>> {
618        self.dh_info
619            .as_ref()
620            .map(|x| x.clone_ref(py).into_bound(py))
621    }
622
623    /// CMS EnvelopedData bytes when the variant is ``"EncKeyPack"``, else ``None``.
624    #[getter]
625    fn enc_key_pack<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
626        self.enc_key_pack.as_deref().map(|b| PyBytes::new(py, b))
627    }
628
629    fn __repr__(&self) -> String {
630        format!("PaPkAsRep(variant='{}')", self.variant)
631    }
632}
633
634/// PKINIT AuthPack — RFC 4556 §3.2.1
635///
636/// The client's signed authentication package, embedded in a PA-PK-AS-REQ.
637///
638/// ```python
639/// pack = synta.krb5.AuthPack.from_der(der_bytes)
640/// print(pack.pk_authenticator.nonce)
641/// ```
642#[pyclass(frozen, name = "AuthPack")]
643pub struct PyAuthPack {
644    pk_authenticator: Py<PyPKAuthenticator>,
645    client_public_value: Option<Vec<u8>>,
646    supported_cmstypes: Option<Vec<u8>>,
647    client_dhnonce: Option<Vec<u8>>,
648    supported_kdfs: Option<Vec<Py<PyKDFAlgorithmId>>>,
649}
650
651#[pymethods]
652impl PyAuthPack {
653    /// Parse a DER-encoded AuthPack SEQUENCE.
654    #[staticmethod]
655    fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
656        let mut dec = Decoder::new(data, Encoding::Der);
657        let parsed: synta_krb5::pkinit::AuthPack<'_> = dec.decode().map_err(SyntaErr)?;
658        let pka = parsed.pk_authenticator;
659        let pk_authenticator = Py::new(
660            py,
661            PyPKAuthenticator {
662                cusec: pka
663                    .cusec
664                    .as_i64()
665                    .map_err(|_| PyOverflowError::new_err("cusec out of i64 range"))?,
666                ctime: pka.ctime.to_string(),
667                nonce: pka
668                    .nonce
669                    .as_i64()
670                    .map_err(|_| PyOverflowError::new_err("nonce out of i64 range"))?,
671                pa_checksum: pka.pa_checksum.map(|s| s.as_bytes().to_vec()),
672                freshness_token: pka.freshness_token.map(|s| s.as_bytes().to_vec()),
673            },
674        )?;
675        let client_public_value = parsed
676            .client_public_value
677            .as_ref()
678            .map(encode_element_to_vec)
679            .transpose()?;
680        let supported_cmstypes = parsed
681            .supported_cmstypes
682            .as_ref()
683            .map(encode_element_to_vec)
684            .transpose()?;
685        let client_dhnonce = parsed.client_dhnonce.map(|s| s.as_bytes().to_vec());
686        let supported_kdfs = parsed
687            .supported_kdfs
688            .map(|kdfs| {
689                kdfs.into_iter()
690                    .map(|kdf| Py::new(py, PyKDFAlgorithmId { kdf_id: kdf.kdf_id }))
691                    .collect::<PyResult<Vec<_>>>()
692            })
693            .transpose()?;
694        Ok(Self {
695            pk_authenticator,
696            client_public_value,
697            supported_cmstypes,
698            client_dhnonce,
699            supported_kdfs,
700        })
701    }
702
703    /// :class:`PKAuthenticator` proving client liveness.
704    #[getter]
705    fn pk_authenticator<'py>(&self, py: Python<'py>) -> Bound<'py, PyPKAuthenticator> {
706        self.pk_authenticator.clone_ref(py).into_bound(py)
707    }
708
709    /// DER-encoded client DH SubjectPublicKeyInfo, or ``None``.
710    #[getter]
711    fn client_public_value<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
712        self.client_public_value
713            .as_deref()
714            .map(|b| PyBytes::new(py, b))
715    }
716
717    /// DER-encoded supported CMS algorithm list, or ``None``.
718    #[getter]
719    fn supported_cmstypes<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
720        self.supported_cmstypes
721            .as_deref()
722            .map(|b| PyBytes::new(py, b))
723    }
724
725    /// Client DH nonce bytes, or ``None``.
726    #[getter]
727    fn client_dhnonce<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
728        self.client_dhnonce.as_deref().map(|b| PyBytes::new(py, b))
729    }
730
731    /// List of supported :class:`KDFAlgorithmId` entries, or ``None``.
732    #[getter]
733    fn supported_kdfs<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyList>>> {
734        self.supported_kdfs
735            .as_ref()
736            .map(|kdfs| {
737                let list = PyList::empty(py);
738                for kdf in kdfs {
739                    list.append(kdf.clone_ref(py).into_bound(py))?;
740                }
741                Ok(list)
742            })
743            .transpose()
744    }
745
746    fn __repr__(&self) -> String {
747        String::from("AuthPack(...)")
748    }
749}
750
751/// PKINIT PA-PK-AS-REQ — RFC 4556 §3.2.2
752///
753/// Pre-authentication data sent by the client in the initial AS-REQ.
754///
755/// ```python
756/// req = synta.krb5.PaPkAsReq.from_der(der_bytes)
757/// print(len(req.signed_auth_pack))
758/// ```
759#[pyclass(frozen, name = "PaPkAsReq")]
760pub struct PyPaPkAsReq {
761    signed_auth_pack: Vec<u8>,
762    trusted_certifiers: Option<Vec<Py<PyExternalPrincipalIdentifier>>>,
763    kdc_pk_id: Option<Vec<u8>>,
764}
765
766#[pymethods]
767impl PyPaPkAsReq {
768    /// Parse a DER-encoded PA-PK-AS-REQ SEQUENCE.
769    #[staticmethod]
770    fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
771        let mut dec = Decoder::new(data, Encoding::Der);
772        let parsed: synta_krb5::pkinit::PaPkAsReq<'_> = dec.decode().map_err(SyntaErr)?;
773        let signed_auth_pack = parsed.signed_auth_pack.as_bytes().to_vec();
774        let trusted_certifiers = parsed
775            .trusted_certifiers
776            .map(|certs| {
777                certs
778                    .into_iter()
779                    .map(|epi| {
780                        let isn = epi
781                            .issuer_and_serial_number
782                            .map(|isn| {
783                                Py::new(
784                                    py,
785                                    PyIssuerAndSerialNumber {
786                                        issuer: isn.issuer.as_bytes().to_vec(),
787                                        serial_number_bytes: isn.serial_number.as_bytes().to_vec(),
788                                    },
789                                )
790                            })
791                            .transpose()?;
792                        Py::new(
793                            py,
794                            PyExternalPrincipalIdentifier {
795                                subject_name: epi.subject_name.map(|s| s.as_bytes().to_vec()),
796                                issuer_and_serial_number: isn,
797                                subject_key_identifier: epi
798                                    .subject_key_identifier
799                                    .map(|s| s.as_bytes().to_vec()),
800                            },
801                        )
802                    })
803                    .collect::<PyResult<Vec<_>>>()
804            })
805            .transpose()?;
806        let kdc_pk_id = parsed.kdc_pk_id.map(|s| s.as_bytes().to_vec());
807        Ok(Self {
808            signed_auth_pack,
809            trusted_certifiers,
810            kdc_pk_id,
811        })
812    }
813
814    /// CMS SignedData wrapping the client's AuthPack.
815    #[getter]
816    fn signed_auth_pack<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
817        PyBytes::new(py, &self.signed_auth_pack)
818    }
819
820    /// List of :class:`ExternalPrincipalIdentifier` for acceptable KDC certs, or ``None``.
821    #[getter]
822    fn trusted_certifiers<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyList>>> {
823        self.trusted_certifiers
824            .as_ref()
825            .map(|certs| {
826                let list = PyList::empty(py);
827                for cert in certs {
828                    list.append(cert.clone_ref(py).into_bound(py))?;
829                }
830                Ok(list)
831            })
832            .transpose()
833    }
834
835    /// Subject key identifier of the preferred KDC certificate, or ``None``.
836    #[getter]
837    fn kdc_pk_id<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
838        self.kdc_pk_id.as_deref().map(|b| PyBytes::new(py, b))
839    }
840
841    fn __repr__(&self) -> String {
842        format!(
843            "PaPkAsReq(signed_auth_pack=<{} bytes>)",
844            self.signed_auth_pack.len()
845        )
846    }
847}
848
849// ── Module registration ───────────────────────────────────────────────────────
850
851/// Register all PKINIT Python classes into `m` (the ``synta.krb5`` submodule).
852///
853/// Called from ``synta_python::krb5::register_krb5_module``.
854pub fn register_pkinit_classes(m: &Bound<'_, PyModule>) -> PyResult<()> {
855    m.add_class::<PyEncryptionKey>()?;
856    m.add_class::<PyChecksum>()?;
857    m.add_class::<PyKDFAlgorithmId>()?;
858    m.add_class::<PyIssuerAndSerialNumber>()?;
859    m.add_class::<PyPKAuthenticator>()?;
860    m.add_class::<PyDHRepInfo>()?;
861    m.add_class::<PyKDCDHKeyInfo>()?;
862    m.add_class::<PyExternalPrincipalIdentifier>()?;
863    m.add_class::<PyReplyKeyPack>()?;
864    m.add_class::<PyPaPkAsRep>()?;
865    m.add_class::<PyAuthPack>()?;
866    m.add_class::<PyPaPkAsReq>()?;
867    Ok(())
868}