Skip to main content

_synta/
krb5.rs

1//! Python bindings for Kerberos V5 and PKINIT types from [`synta_krb5`].
2//!
3//! This module builds the ``synta.krb5`` Python submodule, exposing the
4//! Kerberos V5 and PKINIT ASN.1 types needed for certificate construction
5//! and for parsing Kerberos AS exchanges.
6//!
7//! # Contents
8//!
9//! ## Principal-name support
10//!
11//! * [`PyKrb5PrincipalName`] — the RFC 4556 `KRB5PrincipalName` OtherName
12//!   value embedded in PKINIT certificates.
13//! * `KRB5_PRINCIPAL_NAME_OID` — `ObjectIdentifier` for `id-pkinit-san`
14//!   (1.3.6.1.5.2.2, RFC 4556 §3.2.2).
15//! * `NT_*` integer constants — principal name types from RFC 4120 §6.2.
16//!
17//! ## PKINIT protocol types (RFC 4556 + RFC 6112 + RFC 8636)
18//!
19//! Provided by [`crate::pkinit::register_pkinit_classes`]:
20//!
21//! | Python class                  | RFC / section       | Description                                    |
22//! |-------------------------------|---------------------|------------------------------------------------|
23//! | `EncryptionKey`               | RFC 3961 §2         | Algorithm type and raw key material            |
24//! | `Checksum`                    | RFC 3961 §4         | Algorithm type and raw checksum bytes          |
25//! | `KDFAlgorithmId`              | RFC 8636 §3.1       | KDF OID used in the PKINIT exchange            |
26//! | `IssuerAndSerialNumber`       | RFC 4556 §3.2.2     | Certificate identified by issuer + serial      |
27//! | `ExternalPrincipalIdentifier` | RFC 4556 §3.2.2     | Certificate by name, issuer/serial, or SKI     |
28//! | `PKAuthenticator`             | RFC 4556 §3.2.1     | Client proof of liveness in AS-REQ             |
29//! | `AuthPack`                    | RFC 4556 §3.2.1     | Signed client authentication package           |
30//! | `PaPkAsReq`                   | RFC 4556 §3.2.2     | PKINIT pre-authentication request              |
31//! | `DHRepInfo`                   | RFC 4556 §3.2.4     | KDC Diffie-Hellman reply data                  |
32//! | `KDCDHKeyInfo`                | RFC 4556 §3.2.4     | KDC DH public key and nonce                   |
33//! | `ReplyKeyPack`                | RFC 4556 §3.2.3     | Session key and checksum from KDC              |
34//! | `PaPkAsRep`                   | RFC 4556 §3.2.4     | PKINIT pre-authentication reply (CHOICE)       |
35//!
36//! # Principal name types (`NT_*`)
37//!
38//! | Constant           | Value | RFC / source     | Typical use                          |
39//! |--------------------|-------|------------------|--------------------------------------|
40//! | `NT_UNKNOWN`       | 0     | RFC 4120 §6.2    | Unknown / unspecified                |
41//! | `NT_PRINCIPAL`     | 1     | RFC 4120 §6.2    | User or host principal               |
42//! | `NT_SRV_INST`      | 2     | RFC 4120 §6.2    | Service + instance (e.g. `krbtgt`)   |
43//! | `NT_SRV_HST`       | 3     | RFC 4120 §6.2    | Service + hostname                   |
44//! | `NT_SRV_XHST`      | 4     | RFC 4120 §6.2    | Service + host (remaining components)|
45//! | `NT_UID`           | 5     | RFC 4120 §6.2    | Unique ID                            |
46//! | `NT_X500_PRINCIPAL`| 6     | RFC 4120 §6.2    | Encoded X.500 DN                     |
47//! | `NT_SMTP_NAME`     | 7     | RFC 4120 §6.2    | SMTP email address                   |
48//! | `NT_ENTERPRISE`    | 10    | RFC 6806 §5      | Enterprise principal (UPN-like)      |
49//! | `NT_WELLKNOWN`     | 11    | RFC 8062 §3      | Well-known principal (anonymous)     |
50//! | `NT_SRV_HST_DOMAIN`| 12    | MS-SFU §2.1      | Host-based service (Windows)         |
51//!
52//! # Register
53//!
54//! Call [`register_krb5_module`] from the extension crate's `#[pymodule]`:
55//!
56//! ```rust,ignore
57//! krb5::register_krb5_module(m)?;
58//! ```
59
60use pyo3::prelude::*;
61use pyo3::types::PyBytes;
62
63use synta::tag::{TagClass, TAG_SEQUENCE};
64use synta::traits::{Decode, Encode};
65use synta::{Decoder, Encoder, Encoding};
66
67use synta_krb5::constants::{
68    ETYPE_AES128_CTS_HMAC_SHA1_96, ETYPE_AES128_CTS_HMAC_SHA256_128, ETYPE_AES256_CTS_HMAC_SHA1_96,
69    ETYPE_AES256_CTS_HMAC_SHA384_192, ETYPE_CAMELLIA128_CTS_CMAC, ETYPE_CAMELLIA256_CTS_CMAC,
70    ETYPE_DES3_CBC_MD5, ETYPE_DES3_CBC_SHA1, ETYPE_DES3_CBC_SHA1_KD, ETYPE_DES_CBC_CRC,
71    ETYPE_DES_CBC_MD4, ETYPE_DES_CBC_MD5, ETYPE_DES_HMAC_SHA1, ETYPE_RC4_HMAC, ETYPE_RC4_HMAC_EXP,
72    ID_PKINIT_SAN_COMPONENTS, NT_ENTERPRISE, NT_PRINCIPAL, NT_SMTP_NAME, NT_SRV_HST,
73    NT_SRV_HST_DOMAIN, NT_SRV_INST, NT_SRV_XHST, NT_UID, NT_UNKNOWN, NT_WELLKNOWN,
74    NT_X500_PRINCIPAL,
75};
76use synta_krb5::kerberos_v5::Int32;
77
78use crate::error::SyntaErr;
79use crate::types::PyObjectIdentifier;
80
81/// Build a `PyObjectIdentifier` from a slice of OID arc components.
82///
83/// Returns `Err` only on OOM; the components are hardcoded constants
84/// that are always valid OIDs.
85fn oid_const(py: Python<'_>, components: &[u32]) -> PyResult<Py<pyo3::PyAny>> {
86    use synta::ObjectIdentifier;
87    let inner = ObjectIdentifier::new(components).expect("hardcoded OID is always valid");
88    Ok(Py::new(py, PyObjectIdentifier::from_oid(inner))?.into_any())
89}
90
91// ─── encoding helpers ────────────────────────────────────────────────────────
92
93/// Append a SEQUENCE TLV wrapping `inner` to `enc`.
94fn write_sequence(enc: &mut Encoder, inner: &[u8]) -> Result<(), synta::Error> {
95    enc.write_tag(synta::Tag::universal_constructed(TAG_SEQUENCE))?;
96    enc.write_length(inner.len())?;
97    enc.write_bytes(inner);
98    Ok(())
99}
100
101/// Append a context-specific explicit tag `[tag_num]` wrapping `inner`.
102fn write_explicit_ctx(enc: &mut Encoder, tag_num: u32, inner: &[u8]) -> Result<(), synta::Error> {
103    enc.write_tag(synta::Tag::new(TagClass::ContextSpecific, true, tag_num))?;
104    enc.write_length(inner.len())?;
105    enc.write_bytes(inner);
106    Ok(())
107}
108
109/// Encode `value` as DER, returning the bytes.
110fn encode_to_vec<T: Encode>(value: &T) -> Result<Vec<u8>, synta::Error> {
111    let mut enc = Encoder::new(Encoding::Der);
112    value.encode(&mut enc)?;
113    enc.finish()
114}
115
116// ─── decoding helpers ────────────────────────────────────────────────────────
117
118fn read_tag_check(
119    dec: &mut Decoder<'_>,
120    class: TagClass,
121    number: u32,
122    constructed: bool,
123    label: &'static str,
124) -> PyResult<()> {
125    let tag = dec.read_tag().map_err(SyntaErr)?;
126    if tag.class() != class || tag.number() != number || tag.is_constructed() != constructed {
127        return Err(pyo3::exceptions::PyValueError::new_err(format!(
128            "KRB5PrincipalName: expected {label} (class={class:?}, tag={number}), \
129             got class={:?} tag={} constructed={}",
130            tag.class(),
131            tag.number(),
132            tag.is_constructed()
133        )));
134    }
135    Ok(())
136}
137
138fn read_definite_len(dec: &mut Decoder<'_>) -> PyResult<usize> {
139    Ok(dec
140        .read_length()
141        .map_err(SyntaErr)?
142        .definite()
143        .map_err(SyntaErr)?)
144}
145
146/// Read a constructed TLV with the given tag and return the content bytes.
147fn read_content<'a>(
148    dec: &mut Decoder<'a>,
149    class: TagClass,
150    number: u32,
151    label: &'static str,
152) -> PyResult<&'a [u8]> {
153    read_tag_check(dec, class, number, true, label)?;
154    let len = read_definite_len(dec)?;
155    Ok(dec.read_bytes(len).map_err(SyntaErr)?)
156}
157
158/// Read a SEQUENCE and return its content bytes.
159fn read_sequence<'a>(dec: &mut Decoder<'a>, label: &'static str) -> PyResult<&'a [u8]> {
160    read_content(dec, TagClass::Universal, TAG_SEQUENCE, label)
161}
162
163/// Read a context-specific explicit tag `[N]` and return its content bytes.
164fn read_explicit_ctx<'a>(
165    dec: &mut Decoder<'a>,
166    tag_num: u32,
167    label: &'static str,
168) -> PyResult<&'a [u8]> {
169    read_content(dec, TagClass::ContextSpecific, tag_num, label)
170}
171
172// ─── KRB5PrincipalName encode / decode ──────────────────────────────────────
173
174/// Encode a `KRB5PrincipalName` (RFC 4556) SEQUENCE to DER.
175///
176/// ```asn1
177/// KRB5PrincipalName ::= SEQUENCE {
178///     realm         [0] GeneralString,
179///     principalName [1] PrincipalName
180/// }
181/// PrincipalName ::= SEQUENCE {
182///     name-type   [0] Int32,
183///     name-string [1] SEQUENCE OF GeneralString
184/// }
185/// ```
186fn encode_krb5principalname(
187    realm: &str,
188    name_type: i32,
189    components: &[String],
190) -> PyResult<Vec<u8>> {
191    // 1. name-string: SEQUENCE OF GeneralString
192    let ns_items: Result<Vec<Vec<u8>>, synta::Error> = components
193        .iter()
194        .map(|c| encode_to_vec(&synta::GeneralString::new(c.as_bytes().to_vec())))
195        .collect();
196    let ns_items = ns_items.map_err(SyntaErr)?;
197    let ns_flat: Vec<u8> = ns_items.concat();
198
199    let mut ns_seq_enc = Encoder::new(Encoding::Der);
200    write_sequence(&mut ns_seq_enc, &ns_flat).map_err(SyntaErr)?;
201    let ns_seq_bytes = ns_seq_enc.finish().map_err(SyntaErr)?;
202
203    // 2. name-type: INTEGER
204    let nt_bytes = encode_to_vec(&synta::Integer::from_i64(name_type as i64)).map_err(SyntaErr)?;
205
206    // 3. PrincipalName = SEQUENCE { [0] name-type, [1] name-string }
207    let mut nt_ctx = Encoder::new(Encoding::Der);
208    write_explicit_ctx(&mut nt_ctx, 0, &nt_bytes).map_err(SyntaErr)?;
209    let nt_ctx_bytes = nt_ctx.finish().map_err(SyntaErr)?;
210
211    let mut ns_ctx = Encoder::new(Encoding::Der);
212    write_explicit_ctx(&mut ns_ctx, 1, &ns_seq_bytes).map_err(SyntaErr)?;
213    let ns_ctx_bytes = ns_ctx.finish().map_err(SyntaErr)?;
214
215    let pn_inner = [nt_ctx_bytes, ns_ctx_bytes].concat();
216    let mut pn_enc = Encoder::new(Encoding::Der);
217    write_sequence(&mut pn_enc, &pn_inner).map_err(SyntaErr)?;
218    let pn_bytes = pn_enc.finish().map_err(SyntaErr)?;
219
220    // 4. realm: GeneralString
221    let realm_gs_bytes =
222        encode_to_vec(&synta::GeneralString::new(realm.as_bytes().to_vec())).map_err(SyntaErr)?;
223
224    // 5. Outer SEQUENCE { [0] realm, [1] principalName }
225    let mut realm_ctx = Encoder::new(Encoding::Der);
226    write_explicit_ctx(&mut realm_ctx, 0, &realm_gs_bytes).map_err(SyntaErr)?;
227    let realm_ctx_bytes = realm_ctx.finish().map_err(SyntaErr)?;
228
229    let mut pn_ctx = Encoder::new(Encoding::Der);
230    write_explicit_ctx(&mut pn_ctx, 1, &pn_bytes).map_err(SyntaErr)?;
231    let pn_ctx_bytes = pn_ctx.finish().map_err(SyntaErr)?;
232
233    let outer_content = [realm_ctx_bytes, pn_ctx_bytes].concat();
234    let mut outer = Encoder::new(Encoding::Der);
235    write_sequence(&mut outer, &outer_content).map_err(SyntaErr)?;
236    Ok(outer.finish().map_err(SyntaErr)?)
237}
238
239/// Decode a `KRB5PrincipalName` (RFC 4556) DER blob.
240///
241/// Returns `(realm, name_type, components)`.
242fn decode_krb5principalname(data: &[u8]) -> PyResult<(String, i32, Vec<String>)> {
243    let ve = |s: &str| pyo3::exceptions::PyValueError::new_err(s.to_string());
244
245    // Outer SEQUENCE
246    let mut outer = Decoder::new(data, Encoding::Der);
247    let outer_content = read_sequence(&mut outer, "outer SEQUENCE")?;
248
249    // [0] realm GeneralString
250    let mut seq = Decoder::new(outer_content, Encoding::Der);
251    let realm_bytes = read_explicit_ctx(&mut seq, 0, "[0] realm")?;
252    let mut realm_dec = Decoder::new(realm_bytes, Encoding::Der);
253    let realm_gs = synta::GeneralString::decode(&mut realm_dec).map_err(SyntaErr)?;
254    let realm = String::from_utf8(realm_gs.as_bytes().to_vec())
255        .map_err(|_| ve("KRB5PrincipalName: realm is not valid UTF-8"))?;
256
257    // [1] principalName PrincipalName
258    let pn_bytes = read_explicit_ctx(&mut seq, 1, "[1] principalName")?;
259
260    // PrincipalName SEQUENCE
261    let mut pn_dec = Decoder::new(pn_bytes, Encoding::Der);
262    let pn_inner = read_sequence(&mut pn_dec, "PrincipalName SEQUENCE")?;
263
264    // [0] name-type Int32
265    let mut pn_inner_dec = Decoder::new(pn_inner, Encoding::Der);
266    let nt_bytes = read_explicit_ctx(&mut pn_inner_dec, 0, "[0] name-type")?;
267    let mut nt_dec = Decoder::new(nt_bytes, Encoding::Der);
268    let int32 = Int32::decode(&mut nt_dec).map_err(SyntaErr)?;
269    let name_type = int32.get();
270
271    // [1] name-string SEQUENCE OF GeneralString
272    let ns_ctx_bytes = read_explicit_ctx(&mut pn_inner_dec, 1, "[1] name-string")?;
273    let mut ns_ctx_dec = Decoder::new(ns_ctx_bytes, Encoding::Der);
274    let ns_bytes = read_sequence(&mut ns_ctx_dec, "name-string SEQUENCE")?;
275
276    let mut components = Vec::new();
277    let mut ns_dec = Decoder::new(ns_bytes, Encoding::Der);
278    while !ns_dec.is_empty() {
279        let gs = synta::GeneralString::decode(&mut ns_dec).map_err(SyntaErr)?;
280        let s = String::from_utf8(gs.as_bytes().to_vec())
281            .map_err(|_| ve("KRB5PrincipalName: name component is not valid UTF-8"))?;
282        components.push(s);
283    }
284
285    Ok((realm, name_type, components))
286}
287
288// ─── Python class ────────────────────────────────────────────────────────────
289
290/// RFC 4556 ``KRB5PrincipalName`` — Kerberos principal embedded in a PKINIT
291/// certificate as an ``OtherName`` Subject Alternative Name.
292///
293/// ``KRB5PrincipalName`` is defined in RFC 4556 §3.2.2 as:
294///
295/// ```text
296/// KRB5PrincipalName ::= SEQUENCE {
297///     realm         [0] Realm,          -- GeneralString
298///     principalName [1] PrincipalName   -- RFC 4120 §5.2.2
299/// }
300///
301/// PrincipalName ::= SEQUENCE {
302///     name-type   [0] Int32,
303///     name-string [1] SEQUENCE OF KerberosString  -- GeneralString
304/// }
305/// ```
306///
307/// The ``OtherName`` OID for this type is ``1.3.6.1.5.2.2``,
308/// available as ``synta.krb5.KRB5_PRINCIPAL_NAME_OID``.
309///
310/// **Name types** — use the ``NT_*`` module constants:
311///
312/// * ``NT_PRINCIPAL`` (1) — user principal, e.g. ``alice@EXAMPLE.COM``
313/// * ``NT_SRV_INST``  (2) — service + instance, e.g. ``krbtgt/EXAMPLE.COM``
314/// * ``NT_SRV_HST``   (3) — service + hostname, e.g. ``host/server.example.com``
315/// * ``NT_ENTERPRISE`` (10) — enterprise (UPN-style), e.g. ``user@domain.example``
316///
317/// **Display format**: Kerberos principal strings conventionally use
318/// ``/`` to join components and ``@`` to separate from the realm:
319/// ``service/instance@REALM``.  This class stores the components and realm
320/// individually; format them yourself if you need a display string.
321///
322/// Example — PKINIT TGT principal for ``EXAMPLE.COM``:
323///
324/// ```python
325/// import synta.krb5
326///
327/// name = synta.krb5.Krb5PrincipalName(
328///     realm="EXAMPLE.COM",
329///     name_type=synta.krb5.NT_SRV_INST,
330///     components=["krbtgt", "EXAMPLE.COM"],
331/// )
332/// # OID for OtherName: synta.krb5.KRB5_PRINCIPAL_NAME_OID  (1.3.6.1.5.2.2)
333/// der_bytes = name.to_der()   # DER SEQUENCE bytes for OtherName.value
334/// ```
335///
336/// Example — decode from an OtherName SAN value:
337///
338/// ```python
339/// parsed = synta.krb5.Krb5PrincipalName.from_der(der_bytes)
340/// print(parsed.realm)              # "EXAMPLE.COM"
341/// print("/".join(parsed.components) + "@" + parsed.realm)
342/// # "krbtgt/EXAMPLE.COM@EXAMPLE.COM"
343/// ```
344#[pyclass(name = "Krb5PrincipalName", frozen)]
345pub struct PyKrb5PrincipalName {
346    realm: String,
347    name_type: i32,
348    components: Vec<String>,
349}
350
351#[pymethods]
352impl PyKrb5PrincipalName {
353    /// Create a new ``Krb5PrincipalName``.
354    ///
355    /// Args:
356    ///     realm: Kerberos realm (ASCII string, e.g. ``"EXAMPLE.COM"``).
357    ///     name_type: Principal name type.  Use one of the ``NT_*`` module
358    ///         constants: ``NT_PRINCIPAL``, ``NT_SRV_INST``, ``NT_SRV_HST``,
359    ///         ``NT_ENTERPRISE``, etc.
360    ///     components: Name-string components.  The number and meaning of
361    ///         components depends on ``name_type``:
362    ///
363    ///         * ``NT_PRINCIPAL`` — one component: the user name.
364    ///         * ``NT_SRV_INST`` — two components: service name and instance
365    ///           (e.g. ``["krbtgt", "EXAMPLE.COM"]``).
366    ///         * ``NT_SRV_HST`` — two components: service name and hostname
367    ///           (e.g. ``["host", "server.example.com"]``).
368    ///         * ``NT_ENTERPRISE`` — one component: the enterprise name.
369    ///
370    /// Raises ``ValueError`` if ``realm`` or any component contains characters
371    /// outside ASCII (Kerberos strings are ``GeneralString``, which is a subset
372    /// of ASCII in practice).
373    #[new]
374    #[pyo3(signature = (realm, name_type, components))]
375    fn new(realm: String, name_type: i32, components: Vec<String>) -> PyResult<Self> {
376        if !realm.is_ascii() {
377            return Err(pyo3::exceptions::PyValueError::new_err(
378                "realm must be an ASCII string",
379            ));
380        }
381        for comp in &components {
382            if !comp.is_ascii() {
383                return Err(pyo3::exceptions::PyValueError::new_err(format!(
384                    "name component {comp:?} must be an ASCII string"
385                )));
386            }
387        }
388        Ok(Self {
389            realm,
390            name_type,
391            components,
392        })
393    }
394
395    /// Kerberos realm string (e.g. ``"EXAMPLE.COM"``).
396    ///
397    /// Always ASCII.  Conventionally upper-case DNS domain name.
398    #[getter]
399    fn realm(&self) -> &str {
400        &self.realm
401    }
402
403    /// Principal name type integer (one of the ``NT_*`` module constants).
404    ///
405    /// Common values: ``NT_PRINCIPAL`` (1), ``NT_SRV_INST`` (2),
406    /// ``NT_SRV_HST`` (3), ``NT_ENTERPRISE`` (10).
407    #[getter]
408    fn name_type(&self) -> i32 {
409        self.name_type
410    }
411
412    /// Name-string components as a list of ASCII strings.
413    ///
414    /// The list length and semantics depend on ``name_type``.  For
415    /// ``NT_SRV_INST`` the conventional display form joins components with
416    /// ``"/"`` and appends ``"@" + realm``, e.g.:
417    ///
418    /// ```python
419    /// "/".join(name.components) + "@" + name.realm
420    /// # -> "krbtgt/EXAMPLE.COM@EXAMPLE.COM"
421    /// ```
422    #[getter]
423    fn components<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, pyo3::types::PyList>> {
424        pyo3::types::PyList::new(py, self.components.iter())
425    }
426
427    /// Encode this ``Krb5PrincipalName`` to DER bytes.
428    ///
429    /// Returns the complete DER encoding of the ``KRB5PrincipalName``
430    /// ``SEQUENCE``.  These bytes are the ``value`` field of an X.509
431    /// ``OtherName`` SAN with OID ``1.3.6.1.5.2.2`` (id-pkinit-san).
432    ///
433    /// Example — build an OtherName value for a PKINIT TGT principal:
434    ///
435    /// ```python
436    /// import synta.krb5
437    ///
438    /// name = synta.krb5.Krb5PrincipalName("EXAMPLE.COM", synta.krb5.NT_SRV_INST,
439    ///                                      ["krbtgt", "EXAMPLE.COM"])
440    /// oid = synta.krb5.KRB5_PRINCIPAL_NAME_OID   # ObjectIdentifier("1.3.6.1.5.2.2")
441    /// other_name_value = name.to_der()
442    /// # Pass (oid, other_name_value) to your X.509 library's OtherName constructor.
443    /// ```
444    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
445        let der = encode_krb5principalname(&self.realm, self.name_type, &self.components)?;
446        Ok(PyBytes::new(py, &der))
447    }
448
449    /// Decode a ``Krb5PrincipalName`` from DER bytes.
450    ///
451    /// Parses the DER encoding of a ``KRB5PrincipalName`` SEQUENCE as it
452    /// appears in the ``value`` field of an X.509 ``OtherName`` SAN with
453    /// OID ``1.3.6.1.5.2.2``.
454    ///
455    /// Args:
456    ///     data: DER-encoded ``KRB5PrincipalName`` SEQUENCE bytes.
457    ///
458    /// Returns a new ``Krb5PrincipalName`` instance.
459    ///
460    /// Raises ``ValueError`` on malformed or truncated input.
461    ///
462    /// Example — decode a ``KRB5PrincipalName`` from a certificate OtherName:
463    ///
464    /// ```python
465    /// import synta, synta.krb5, synta.oids
466    ///
467    /// cert = synta.Certificate.from_der(der)
468    /// san_der = cert.get_extension_value_der(synta.oids.SUBJECT_ALT_NAME)
469    /// # ... extract the OtherName value bytes from the SAN extension ...
470    /// # Check OtherName OID == synta.krb5.KRB5_PRINCIPAL_NAME_OID
471    /// name = synta.krb5.Krb5PrincipalName.from_der(other_name_value)
472    /// print(name.realm, name.name_type, name.components)
473    /// ```
474    #[staticmethod]
475    fn from_der(data: &[u8]) -> PyResult<Self> {
476        let (realm, name_type, components) = decode_krb5principalname(data)?;
477        Ok(Self {
478            realm,
479            name_type,
480            components,
481        })
482    }
483
484    /// Return a developer-readable string representation.
485    ///
486    /// Example:
487    ///
488    /// ```python
489    /// >>> repr(synta.krb5.Krb5PrincipalName("EXAMPLE.COM", 2, ["krbtgt", "EXAMPLE.COM"]))
490    /// "Krb5PrincipalName(realm='EXAMPLE.COM', name_type=2, components=['krbtgt', 'EXAMPLE.COM'])"
491    /// ```
492    fn __repr__(&self) -> String {
493        format!(
494            "Krb5PrincipalName(realm={:?}, name_type={}, components={:?})",
495            self.realm, self.name_type, self.components
496        )
497    }
498
499    /// Return ``True`` if both instances have identical realm, name_type,
500    /// and components.
501    fn __eq__(&self, other: PyRef<'_, Self>) -> bool {
502        self.realm == other.realm
503            && self.name_type == other.name_type
504            && self.components == other.components
505    }
506}
507
508// ─── submodule registration ──────────────────────────────────────────────────
509
510/// Build and register the ``synta.krb5`` submodule into `parent`.
511///
512/// Adds the following to the submodule:
513///
514/// **Classes** — principal-name support:
515/// `Krb5PrincipalName`
516///
517/// **Classes** — PKINIT protocol types (RFC 4556 + RFC 6112 + RFC 8636):
518/// `EncryptionKey`, `Checksum`, `KDFAlgorithmId`, `IssuerAndSerialNumber`,
519/// `ExternalPrincipalIdentifier`, `PKAuthenticator`, `AuthPack`, `PaPkAsReq`,
520/// `DHRepInfo`, `KDCDHKeyInfo`, `ReplyKeyPack`, `PaPkAsRep`
521///
522/// **OID constant**: `KRB5_PRINCIPAL_NAME_OID` — `ObjectIdentifier` for
523/// `id-pkinit-san` (1.3.6.1.5.2.2, RFC 4556 §3.2.2).
524///
525/// **Name-type constants** (RFC 4120 §6.2):
526/// `NT_UNKNOWN`, `NT_PRINCIPAL`, `NT_SRV_INST`, `NT_SRV_HST`, `NT_SRV_XHST`,
527/// `NT_UID`, `NT_X500_PRINCIPAL`, `NT_SMTP_NAME`, `NT_ENTERPRISE`,
528/// `NT_WELLKNOWN`, `NT_SRV_HST_DOMAIN`
529///
530/// **Encryption-type constants** (RFC 3961 / RFC 3962 / RFC 6803 / RFC 8009):
531/// `ETYPE_DES_CBC_CRC`, `ETYPE_DES_CBC_MD4`, `ETYPE_DES_CBC_MD5`,
532/// `ETYPE_DES3_CBC_MD5`, `ETYPE_DES3_CBC_SHA1`, `ETYPE_DES_HMAC_SHA1`,
533/// `ETYPE_DES3_CBC_SHA1_KD`, `ETYPE_AES128_CTS_HMAC_SHA1_96`,
534/// `ETYPE_AES256_CTS_HMAC_SHA1_96`, `ETYPE_AES128_CTS_HMAC_SHA256_128`,
535/// `ETYPE_AES256_CTS_HMAC_SHA384_192`, `ETYPE_RC4_HMAC`, `ETYPE_RC4_HMAC_EXP`,
536/// `ETYPE_CAMELLIA128_CTS_CMAC`, `ETYPE_CAMELLIA256_CTS_CMAC`
537///
538/// Also installs the submodule into `sys.modules["synta.krb5"]` so that
539/// ``import synta.krb5`` and ``from synta.krb5 import ...`` work after
540/// ``import synta``.
541pub fn register_krb5_module(parent: &Bound<'_, PyModule>) -> PyResult<()> {
542    let py = parent.py();
543    let m = PyModule::new(py, "krb5")?;
544
545    // Principal name type constants (RFC 4120 §6.2)
546    m.add("NT_UNKNOWN", NT_UNKNOWN)?;
547    m.add("NT_PRINCIPAL", NT_PRINCIPAL)?;
548    m.add("NT_SRV_INST", NT_SRV_INST)?;
549    m.add("NT_SRV_HST", NT_SRV_HST)?;
550    m.add("NT_SRV_XHST", NT_SRV_XHST)?;
551    m.add("NT_UID", NT_UID)?;
552    m.add("NT_X500_PRINCIPAL", NT_X500_PRINCIPAL)?;
553    m.add("NT_SMTP_NAME", NT_SMTP_NAME)?;
554    m.add("NT_ENTERPRISE", NT_ENTERPRISE)?;
555    m.add("NT_WELLKNOWN", NT_WELLKNOWN)?;
556    m.add("NT_SRV_HST_DOMAIN", NT_SRV_HST_DOMAIN)?;
557
558    // Encryption type constants (RFC 3961, RFC 3962, RFC 6803, RFC 8009)
559    m.add("ETYPE_DES_CBC_CRC", ETYPE_DES_CBC_CRC)?;
560    m.add("ETYPE_DES_CBC_MD4", ETYPE_DES_CBC_MD4)?;
561    m.add("ETYPE_DES_CBC_MD5", ETYPE_DES_CBC_MD5)?;
562    m.add("ETYPE_DES3_CBC_MD5", ETYPE_DES3_CBC_MD5)?;
563    m.add("ETYPE_DES3_CBC_SHA1", ETYPE_DES3_CBC_SHA1)?;
564    m.add("ETYPE_DES_HMAC_SHA1", ETYPE_DES_HMAC_SHA1)?;
565    m.add("ETYPE_DES3_CBC_SHA1_KD", ETYPE_DES3_CBC_SHA1_KD)?;
566    m.add(
567        "ETYPE_AES128_CTS_HMAC_SHA1_96",
568        ETYPE_AES128_CTS_HMAC_SHA1_96,
569    )?;
570    m.add(
571        "ETYPE_AES256_CTS_HMAC_SHA1_96",
572        ETYPE_AES256_CTS_HMAC_SHA1_96,
573    )?;
574    m.add(
575        "ETYPE_AES128_CTS_HMAC_SHA256_128",
576        ETYPE_AES128_CTS_HMAC_SHA256_128,
577    )?;
578    m.add(
579        "ETYPE_AES256_CTS_HMAC_SHA384_192",
580        ETYPE_AES256_CTS_HMAC_SHA384_192,
581    )?;
582    m.add("ETYPE_RC4_HMAC", ETYPE_RC4_HMAC)?;
583    m.add("ETYPE_RC4_HMAC_EXP", ETYPE_RC4_HMAC_EXP)?;
584    m.add("ETYPE_CAMELLIA128_CTS_CMAC", ETYPE_CAMELLIA128_CTS_CMAC)?;
585    m.add("ETYPE_CAMELLIA256_CTS_CMAC", ETYPE_CAMELLIA256_CTS_CMAC)?;
586
587    // OID constants
588    m.add(
589        "KRB5_PRINCIPAL_NAME_OID",
590        oid_const(py, ID_PKINIT_SAN_COMPONENTS)?,
591    )?;
592
593    // KRB5PrincipalName class
594    m.add_class::<PyKrb5PrincipalName>()?;
595
596    // PKINIT protocol types (RFC 4556 + RFC 6112 + RFC 8636)
597    crate::pkinit::register_pkinit_classes(&m)?;
598
599    crate::install_submodule(
600        parent,
601        &m,
602        "synta.krb5",
603        Some(
604            "Kerberos V5 ASN.1 types: principal names, PKINIT protocol types, \
605             name-type constants (NT_*), and encryption-type constants (ETYPE_*).",
606        ),
607    )
608}