Skip to main content

zerodds_security_pki/
identity.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! PEM-Parsing + Trust-Anchor-Chain-Validation.
5
6use alloc::string::String;
7use alloc::vec::Vec;
8
9use rustls_pki_types::pem::PemObject;
10use rustls_pki_types::{CertificateDer, TrustAnchor};
11
12/// Ein-Input fuer [`crate::PkiAuthenticationPlugin::validate_with_config`]:
13/// Identity-Zertifikat + zugehoerige CA (beide PEM).
14#[derive(Debug, Clone)]
15pub struct IdentityConfig {
16    /// PEM-kodiertes X.509-Identity-Zertifikat (einzelnes Cert).
17    pub identity_cert_pem: Vec<u8>,
18    /// PEM-kodiertes CA-Bundle (kann mehrere Trust-Anchors enthalten).
19    pub identity_ca_pem: Vec<u8>,
20    /// PKCS8-PEM-kodierter Private-Key passend zum Identity-Cert.
21    /// Wird zum Signieren von Handshake-Tokens und Delegation-Links
22    /// verwendet. `None` = nur Validation-Modus (kein Handshake-Sign
23    /// moeglich).
24    pub identity_key_pem: Option<Vec<u8>>,
25}
26
27/// Interne Fehler des PKI-Backends. Werden in
28/// [`zerodds_security::SecurityError`] gehuellt.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum PkiError {
31    /// PEM-Parsing fehlgeschlagen.
32    InvalidPem(String),
33    /// PEM enthielt **keine** Zertifikate.
34    NoCertInPem,
35    /// Cert-Chain-Verifikation fehlgeschlagen (Signatur, Expiry, Name).
36    CertInvalid(String),
37    /// Trust-Anchor-Bundle leer.
38    EmptyTrustAnchors,
39}
40
41impl core::fmt::Display for PkiError {
42    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43        match self {
44            Self::InvalidPem(m) => write!(f, "invalid PEM: {m}"),
45            Self::NoCertInPem => write!(f, "no certificate in PEM"),
46            Self::CertInvalid(m) => write!(f, "certificate invalid: {m}"),
47            Self::EmptyTrustAnchors => write!(f, "trust-anchor bundle is empty"),
48        }
49    }
50}
51
52impl std::error::Error for PkiError {}
53
54/// Parsed-Repräsentation einer validierten Identity.
55#[derive(Debug, Clone)]
56pub(crate) struct ParsedIdentity {
57    /// Cert-DER des Identity-Zertifikats. Wird in C3.1 fuer signierte
58    /// Handshake-Tokens (`c.id`-Property) verwendet.
59    pub cert_der: Vec<u8>,
60    /// Trust-Anchor-DER-Liste (self-contained, damit das Objekt
61    /// verschiebbar bleibt).
62    pub trust_anchors_der: Vec<Vec<u8>>,
63    /// PKCS8-DER des Private-Keys (extrahiert aus PEM). `None` =
64    /// nur-Validation, kein Sign.
65    pub private_key_pkcs8_der: Option<Vec<u8>>,
66    /// Detektierter Cert-Key-Algorithmus.
67    pub key_algo: CertKeyAlgo,
68}
69
70/// Detektierter Algorithmus aus dem Identity-Cert. Entscheidet, welche
71/// `c.dsign_algo`-Werte und Sign-Routinen genutzt werden.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum CertKeyAlgo {
74    /// ECDSA P-256 mit SHA-256 (rcgen-Default; Spec-Standard).
75    EcdsaP256Sha256,
76    /// RSA-2048 PSS-SHA256 (Legacy/Interop).
77    RsaPssSha256,
78    /// Unbekannt — Cert-Public-Key-Algorithmus nicht in der Whitelist.
79    Unknown,
80}
81
82impl ParsedIdentity {
83    /// Parsed das Cert + CA-Bundle und verifiziert die Signatur-Kette.
84    pub fn from_config(cfg: &IdentityConfig) -> Result<Self, PkiError> {
85        let cert_der = first_cert_der(&cfg.identity_cert_pem)?;
86        let trust_anchors_der = all_certs_der(&cfg.identity_ca_pem)?;
87        if trust_anchors_der.is_empty() {
88            return Err(PkiError::EmptyTrustAnchors);
89        }
90        verify_cert_chain(&cert_der, &trust_anchors_der)?;
91        let key_algo = detect_cert_algo(&cert_der);
92        let private_key_pkcs8_der = match cfg.identity_key_pem.as_deref() {
93            Some(pem) => Some(parse_pkcs8_pem(pem)?),
94            None => None,
95        };
96        Ok(Self {
97            cert_der,
98            trust_anchors_der,
99            private_key_pkcs8_der,
100            key_algo,
101        })
102    }
103
104    /// Verifiziert ein Remote-DER-Zertifikat gegen die gespeicherten
105    /// Trust-Anchors.
106    pub fn verify_remote_der(&self, remote_cert_der: &[u8]) -> Result<(), PkiError> {
107        verify_cert_chain(remote_cert_der, &self.trust_anchors_der)
108    }
109}
110
111fn first_cert_der(pem: &[u8]) -> Result<Vec<u8>, PkiError> {
112    let certs = all_certs_der(pem)?;
113    certs.into_iter().next().ok_or(PkiError::NoCertInPem)
114}
115
116fn all_certs_der(pem: &[u8]) -> Result<Vec<Vec<u8>>, PkiError> {
117    // `rustls-pki-types` >= 1.9 bringt einen integrierten PEM-Parser
118    // (RUSTSEC-2025-0134 → wir haben rustls-pemfile entfernt).
119    let mut out = Vec::new();
120    for item in CertificateDer::pem_slice_iter(pem) {
121        let cert = item.map_err(|e| PkiError::InvalidPem(alloc::format!("{e:?}")))?;
122        out.push(cert.as_ref().to_vec());
123    }
124    Ok(out)
125}
126
127fn verify_cert_chain(end_entity_der: &[u8], trust_anchors_der: &[Vec<u8>]) -> Result<(), PkiError> {
128    let ee = CertificateDer::from_slice(end_entity_der);
129    let end_entity = webpki::EndEntityCert::try_from(&ee)
130        .map_err(|e| PkiError::CertInvalid(alloc::format!("parse: {e:?}")))?;
131
132    // TrustAnchors aus den DER-Bytes ableiten. CertificateDer muss
133    // lange genug leben, damit der daraus abgeleitete Anchor gilt —
134    // deshalb erst alle CertificateDer-Wrapper materialisieren, dann
135    // die TrustAnchors darauf.
136    let ta_certs: Vec<CertificateDer<'_>> = trust_anchors_der
137        .iter()
138        .map(|b| CertificateDer::from_slice(b))
139        .collect();
140    let mut anchors: Vec<TrustAnchor<'_>> = Vec::with_capacity(ta_certs.len());
141    for ta_cert in &ta_certs {
142        let ta = webpki::anchor_from_trusted_cert(ta_cert)
143            .map_err(|e| PkiError::CertInvalid(alloc::format!("trust-anchor: {e:?}")))?;
144        anchors.push(ta);
145    }
146
147    // Aktuelle Zeit (kein no_std hier — `std`-Feature vorausgesetzt).
148    let now = rustls_pki_types::UnixTime::now();
149
150    // Akzeptierte Signatur-Algorithmen: ring-default-Set.
151    let algs = webpki::ALL_VERIFICATION_ALGS;
152
153    // Keine Zwischen-Certs im default-Pfad — Identity-Cert ist direkt
154    // CA-signed (Sub-CA-Setups laufen ueber den Delegation-Chain-Pfad
155    // im `delegation`-Modul plus `security-permissions::delegation_check`).
156    end_entity
157        .verify_for_usage(
158            algs,
159            &anchors,
160            &[],
161            now,
162            webpki::KeyUsage::client_auth(),
163            None,
164            None,
165        )
166        .map_err(|e| PkiError::CertInvalid(alloc::format!("verify: {e:?}")))?;
167
168    Ok(())
169}
170
171/// Crate-internes Re-Export, damit `plugin.rs` die OID-Detection auf
172/// Peer-Cert-DER nochmal ausfuehren kann (kein eigener Helper-Bedarf).
173pub(crate) fn detect_cert_algo_pub(der: &[u8]) -> CertKeyAlgo {
174    detect_cert_algo(der)
175}
176
177/// Detektiert den Public-Key-Algorithmus aus dem Cert-DER. Dies ist
178/// ein pragmatischer SPKI-Match — wir suchen nach OID-Bytes in den
179/// ersten 200 byte des DER-Streams.
180///
181/// * 1.2.840.10045.2.1 (id-ecPublicKey) + 1.2.840.10045.3.1.7 (P-256) → ECDSA P-256.
182/// * 1.2.840.113549.1.1.1 (rsaEncryption) → RSA.
183fn detect_cert_algo(der: &[u8]) -> CertKeyAlgo {
184    // OID encoded as DER: 06 LL ...
185    // ECDSA P-256 SPKI hat: 1.2.840.10045.2.1 = 06 07 2A 86 48 CE 3D 02 01
186    // und params 1.2.840.10045.3.1.7 = 06 08 2A 86 48 CE 3D 03 01 07
187    const ECDSA_OID: &[u8] = &[0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
188    const P256_OID: &[u8] = &[0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
189    const RSA_OID: &[u8] = &[
190        0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01,
191    ];
192    if contains_subseq(der, ECDSA_OID) && contains_subseq(der, P256_OID) {
193        CertKeyAlgo::EcdsaP256Sha256
194    } else if contains_subseq(der, RSA_OID) {
195        CertKeyAlgo::RsaPssSha256
196    } else {
197        CertKeyAlgo::Unknown
198    }
199}
200
201fn contains_subseq(haystack: &[u8], needle: &[u8]) -> bool {
202    if needle.is_empty() || haystack.len() < needle.len() {
203        return false;
204    }
205    haystack.windows(needle.len()).any(|w| w == needle)
206}
207
208fn parse_pkcs8_pem(pem: &[u8]) -> Result<Vec<u8>, PkiError> {
209    use rustls_pki_types::PrivatePkcs8KeyDer;
210    PrivatePkcs8KeyDer::pem_slice_iter(pem)
211        .next()
212        .ok_or(PkiError::NoCertInPem)?
213        .map(|k| k.secret_pkcs8_der().to_vec())
214        .map_err(|e| PkiError::InvalidPem(alloc::format!("{e:?}")))
215}
216
217#[cfg(test)]
218#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
219mod mutation_killers {
220    use super::*;
221
222    /// Display-Format aller Error-Variants. Faengt Mutation
223    /// `replace fmt with Ok(Default::default())`.
224    #[test]
225    fn pki_error_display_messages_are_specific() {
226        assert_eq!(
227            alloc::format!(
228                "{}",
229                PkiError::InvalidPem(alloc::string::String::from("bad"))
230            ),
231            "invalid PEM: bad"
232        );
233        assert_eq!(
234            alloc::format!("{}", PkiError::NoCertInPem),
235            "no certificate in PEM"
236        );
237        assert_eq!(
238            alloc::format!(
239                "{}",
240                PkiError::CertInvalid(alloc::string::String::from("expired"))
241            ),
242            "certificate invalid: expired"
243        );
244        assert_eq!(
245            alloc::format!("{}", PkiError::EmptyTrustAnchors),
246            "trust-anchor bundle is empty"
247        );
248    }
249
250    /// `detect_cert_algo` braucht BEIDE OIDs (id-ecPublicKey UND P-256
251    /// curve), nicht nur eine. Faengt `&&` -> `||` Mutation.
252    #[test]
253    fn detect_cert_algo_requires_both_ecdsa_oids() {
254        let only_ecdsa = [0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
255        assert_eq!(detect_cert_algo(&only_ecdsa), CertKeyAlgo::Unknown);
256
257        let only_p256 = [0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
258        assert_eq!(detect_cert_algo(&only_p256), CertKeyAlgo::Unknown);
259
260        let mut both = Vec::new();
261        both.extend_from_slice(&only_ecdsa);
262        both.extend_from_slice(&only_p256);
263        assert_eq!(detect_cert_algo(&both), CertKeyAlgo::EcdsaP256Sha256);
264    }
265
266    /// `contains_subseq` ist NICHT konstant `true`. Faengt
267    /// `replace contains_subseq -> bool with true` Mutation.
268    #[test]
269    fn contains_subseq_not_constant_true() {
270        assert!(!contains_subseq(b"abc", b"xyz"));
271        assert!(!contains_subseq(b"", b"x"));
272        assert!(!contains_subseq(b"a", b"abc"));
273    }
274
275    /// `contains_subseq` Empty-Needle: `needle.is_empty() || hay<needle.len()`
276    /// muss bei leerem Needle false liefern. Faengt `||` -> `&&`.
277    #[test]
278    fn contains_subseq_empty_needle_returns_false() {
279        assert!(!contains_subseq(b"abc", b""));
280        assert!(!contains_subseq(b"", b""));
281    }
282
283    /// `contains_subseq` Hay==Needle muss true liefern.
284    /// Faengt `<` -> `==` und `<` -> `<=` auf der Length-Pre-Check-Branch.
285    #[test]
286    fn contains_subseq_exact_match_at_equal_length() {
287        assert!(contains_subseq(b"abc", b"abc"));
288        assert!(contains_subseq(b"\x06\x07\x2A", b"\x06\x07\x2A"));
289    }
290
291    /// `contains_subseq` ohne Match liefert false. Faengt `==` -> `!=`
292    /// Mutation in der windows.any()-Pruefung.
293    #[test]
294    fn contains_subseq_no_match_returns_false() {
295        assert!(!contains_subseq(b"abcde", b"xyz"));
296        assert!(!contains_subseq(b"\x01\x02\x03\x04", b"\x05\x06"));
297    }
298}