Skip to main content

tf_types/
bridge_tls.rs

1//! TLS / mTLS bridge — accept a peer-supplied X.509 certificate chain,
2//! verify it against a configured set of trust anchors, and project the
3//! verified leaf into a TrustForge actor identity + capabilities.
4//!
5//! Uses `x509-parser` for ASN.1 parsing and signature verification, so we
6//! avoid embedding our own ASN.1/DER walker.
7
8use std::collections::HashSet;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use x509_parser::certificate::X509Certificate;
12use x509_parser::extensions::{GeneralName, ParsedExtension};
13use x509_parser::pem::Pem;
14use x509_parser::prelude::FromDer;
15use x509_parser::time::ASN1Time;
16
17use crate::bridges::{Bridge, BridgeError, BridgeKind};
18use crate::generated::{
19    ActorIdentity, ActorIdentity_IdentityVersion, ActorType, AuthorityRoot, AuthorityRoot_Kind,
20    PublicKey, PublicKey_Purpose, TrustLevel,
21};
22
23/// Mapping from X.509 Extended Key Usage OIDs to TrustForge action names.
24pub fn default_eku_to_action(oid: &str) -> Option<&'static str> {
25    match oid {
26        "1.3.6.1.5.5.7.3.1" => Some("tls.server-auth"),
27        "1.3.6.1.5.5.7.3.2" => Some("tls.client-auth"),
28        "1.3.6.1.5.5.7.3.3" => Some("code.sign"),
29        "1.3.6.1.5.5.7.3.4" => Some("email.protect"),
30        "1.3.6.1.5.5.7.3.8" => Some("timestamp.sign"),
31        "1.3.6.1.5.5.7.3.9" => Some("ocsp.sign"),
32        _ => None,
33    }
34}
35
36#[derive(Clone, Debug)]
37pub struct TlsBridgeConfig {
38    pub bridge_id: String,
39    pub trust_domain: String,
40    pub root_certificates_pem: Vec<String>,
41    pub max_chain_length: Option<usize>,
42    pub required_san_uri: Option<String>,
43    pub now_unix_seconds: Option<u64>,
44}
45
46#[derive(Clone, Debug)]
47pub struct TlsVerificationResult {
48    pub identity: ActorIdentity,
49    pub capabilities: Vec<String>,
50    pub leaf_subject: String,
51    pub chain_subjects: Vec<String>,
52}
53
54pub struct TlsBridge {
55    cfg: TlsBridgeConfig,
56    roots: Vec<Vec<u8>>, // owned DER for each configured root
57}
58
59impl TlsBridge {
60    pub fn new(cfg: TlsBridgeConfig) -> Result<Self, BridgeError> {
61        if cfg.root_certificates_pem.is_empty() {
62            return Err(BridgeError::InvalidInput(
63                "TLS bridge requires at least one trust anchor".into(),
64            ));
65        }
66        let mut roots = Vec::with_capacity(cfg.root_certificates_pem.len());
67        for (i, pem) in cfg.root_certificates_pem.iter().enumerate() {
68            let der = parse_single_pem(pem)
69                .map_err(|e| BridgeError::InvalidInput(format!("root[{}]: {}", i, e)))?;
70            roots.push(der);
71        }
72        Ok(TlsBridge { cfg, roots })
73    }
74
75    pub fn verify_chain(&self, chain_pem: &[String]) -> Result<TlsVerificationResult, BridgeError> {
76        let mut chain_der: Vec<Vec<u8>> = Vec::new();
77        for (i, pem) in chain_pem.iter().enumerate() {
78            for der in parse_pem_bundle(pem)
79                .map_err(|e| BridgeError::InvalidInput(format!("chain[{}]: {}", i, e)))?
80            {
81                chain_der.push(der);
82            }
83        }
84        if chain_der.is_empty() {
85            return Err(BridgeError::InvalidInput("empty chain".into()));
86        }
87        let max = self.cfg.max_chain_length.unwrap_or(6);
88        if chain_der.len() > max {
89            return Err(BridgeError::Rejected(format!(
90                "chain longer than max ({} > {})",
91                chain_der.len(),
92                max
93            )));
94        }
95
96        let now = self.cfg.now_unix_seconds.unwrap_or_else(|| {
97            SystemTime::now()
98                .duration_since(UNIX_EPOCH)
99                .unwrap()
100                .as_secs()
101        });
102        let now_asn1 = ASN1Time::from_timestamp(now as i64)
103            .map_err(|e| BridgeError::InvalidInput(format!("now overflow: {}", e)))?;
104
105        // Parse all certs once; we'll re-borrow as we walk.
106        let parsed: Vec<X509Certificate> = chain_der
107            .iter()
108            .map(|d| {
109                let (_, c) = X509Certificate::from_der(d)
110                    .map_err(|e| BridgeError::InvalidInput(format!("DER parse: {}", e)))?;
111                Ok::<_, BridgeError>(c)
112            })
113            .collect::<Result<_, _>>()?;
114
115        // Validity windows for everything in the chain.
116        for c in &parsed {
117            let validity = c.validity();
118            if !validity.is_valid_at(now_asn1) {
119                return Err(BridgeError::Rejected(format!(
120                    "cert {} outside validity window",
121                    c.subject()
122                )));
123            }
124        }
125
126        // Walk leaf → chain → root.
127        let leaf = &parsed[0];
128        let mut chain_subjects: Vec<String> = vec![leaf.subject().to_string()];
129        let mut current_idx: Option<usize> = Some(0);
130        let mut visited: HashSet<String> = HashSet::new();
131        visited.insert(leaf.subject().to_string());
132
133        let roots_parsed: Vec<X509Certificate> = self
134            .roots
135            .iter()
136            .map(|d| X509Certificate::from_der(d).map(|p| p.1))
137            .collect::<Result<_, _>>()
138            .map_err(|e| BridgeError::Internal(format!("root DER reparse: {}", e)))?;
139
140        for _ in 0..max {
141            let cur = &parsed[current_idx.expect("current set")];
142            // Try to find an issuer in the supplied chain (other than `cur`).
143            let inter_idx = parsed
144                .iter()
145                .enumerate()
146                .find(|(i, c)| *i != current_idx.unwrap() && c.subject() == cur.issuer())
147                .map(|(i, _)| i);
148            let issuer_in_chain = inter_idx.map(|i| &parsed[i]);
149            let root_match = roots_parsed.iter().find(|r| r.subject() == cur.issuer());
150
151            let issuer = match issuer_in_chain.or(root_match) {
152                Some(c) => c,
153                None => {
154                    return Err(BridgeError::Rejected(format!(
155                        "no issuer cert for {} (issuer={})",
156                        cur.subject(),
157                        cur.issuer()
158                    )))
159                }
160            };
161
162            cur.verify_signature(Some(issuer.public_key()))
163                .map_err(|e| {
164                    BridgeError::Rejected(format!(
165                        "signature verification failed for {}: {}",
166                        cur.subject(),
167                        e
168                    ))
169                })?;
170
171            chain_subjects.push(issuer.subject().to_string());
172            if root_match.is_some() && issuer_in_chain.is_none() {
173                // Reached a configured trust anchor; validate the root is
174                // self-signed and current.
175                issuer
176                    .verify_signature(Some(issuer.public_key()))
177                    .map_err(|e| {
178                        BridgeError::Rejected(format!(
179                            "root {} not self-consistent: {}",
180                            issuer.subject(),
181                            e
182                        ))
183                    })?;
184                return self.project(leaf, issuer, chain_subjects);
185            }
186            current_idx = inter_idx;
187            if !visited.insert(issuer.subject().to_string()) {
188                return Err(BridgeError::Rejected("chain loop detected".into()));
189            }
190        }
191        Err(BridgeError::Rejected(format!(
192            "chain exceeds max depth {} without reaching trust anchor",
193            max
194        )))
195    }
196
197    fn project(
198        &self,
199        leaf: &X509Certificate,
200        root: &X509Certificate,
201        chain_subjects: Vec<String>,
202    ) -> Result<TlsVerificationResult, BridgeError> {
203        let san_uris = collect_san_uris(leaf);
204        if let Some(req) = &self.cfg.required_san_uri {
205            if !san_uris.iter().any(|u| u == req) {
206                return Err(BridgeError::Rejected(format!(
207                    "leaf SAN URIs {:?} missing required {}",
208                    san_uris, req
209                )));
210            }
211        }
212        let cn = parse_common_name(&leaf.subject().to_string());
213        let san_dns = collect_san_dns(leaf);
214        let spiffe_san = san_uris
215            .iter()
216            .find(|u| u.starts_with("spiffe://"))
217            .cloned();
218        let subject = spiffe_san
219            .clone()
220            .or(cn.clone())
221            .or_else(|| san_dns.first().cloned())
222            .unwrap_or_else(|| leaf.subject().to_string());
223        let actor_type = if spiffe_san.is_some() {
224            ActorType::Service
225        } else {
226            ActorType::Device
227        };
228        let type_str = match actor_type {
229            ActorType::Service => "service",
230            _ => "device",
231        };
232        let actor_id = format!(
233            "tf:actor:{}:{}/{}",
234            type_str,
235            self.cfg.trust_domain,
236            encode_actor_path(&subject)
237        );
238
239        let pk = leaf.public_key();
240        let alg_oid = pk.algorithm.algorithm.to_id_string();
241        let algorithm = match alg_oid.as_str() {
242            "1.2.840.113549.1.1.1" => "rsa",
243            "1.2.840.10045.2.1" => "p256",
244            "1.3.101.112" => "ed25519",
245            _ => "unknown",
246        };
247        let public_key_b64 =
248            base64::engine::general_purpose::STANDARD.encode(pk.subject_public_key.data.as_ref());
249
250        let fingerprint = sha256_hex(leaf.as_ref());
251
252        let identity = ActorIdentity {
253            identity_version: ActorIdentity_IdentityVersion::V1,
254            actor_id,
255            actor_type: actor_type.clone(),
256            instance_id: None,
257            public_keys: vec![PublicKey {
258                key_id: fingerprint,
259                algorithm: algorithm.to_string(),
260                public_key: public_key_b64,
261                purpose: PublicKey_Purpose::Signing,
262                valid_from: None,
263                valid_until: None,
264            }],
265            trust_levels: vec![if matches!(actor_type, ActorType::Service) {
266                TrustLevel::T4
267            } else {
268                TrustLevel::T3
269            }],
270            authority_roots: vec![AuthorityRoot {
271                kind: AuthorityRoot_Kind::Organization,
272                id: parse_common_name(&root.subject().to_string())
273                    .unwrap_or_else(|| root.subject().to_string()),
274            }],
275            attestations: None,
276            valid_from: rfc3339_from_unix(leaf.validity().not_before.timestamp()),
277            valid_until: Some(rfc3339_from_unix(leaf.validity().not_after.timestamp())),
278            revocation_ref: None,
279            signature: None,
280        };
281
282        let capabilities = collect_eku_actions(leaf);
283
284        Ok(TlsVerificationResult {
285            identity,
286            capabilities,
287            leaf_subject: leaf.subject().to_string(),
288            chain_subjects,
289        })
290    }
291}
292
293impl Bridge for TlsBridge {
294    fn bridge_id(&self) -> &str {
295        &self.cfg.bridge_id
296    }
297    fn kind(&self) -> BridgeKind {
298        BridgeKind::Tls
299    }
300    fn trust_domain(&self) -> &str {
301        &self.cfg.trust_domain
302    }
303}
304
305fn parse_single_pem(pem: &str) -> Result<Vec<u8>, String> {
306    let mut all = parse_pem_bundle(pem)?;
307    if all.is_empty() {
308        return Err("no CERTIFICATE block".into());
309    }
310    Ok(all.remove(0))
311}
312
313fn parse_pem_bundle(pem: &str) -> Result<Vec<Vec<u8>>, String> {
314    let mut bytes = pem.as_bytes();
315    let mut out = Vec::new();
316    while !bytes.is_empty() {
317        match Pem::read(std::io::Cursor::new(bytes)) {
318            Ok((p, consumed)) => {
319                if p.label != "CERTIFICATE" {
320                    bytes = &bytes[consumed..];
321                    continue;
322                }
323                out.push(p.contents);
324                bytes = &bytes[consumed..];
325            }
326            Err(_) => break,
327        }
328    }
329    Ok(out)
330}
331
332fn collect_san_uris(cert: &X509Certificate) -> Vec<String> {
333    cert.extensions()
334        .iter()
335        .flat_map(|ext| match ext.parsed_extension() {
336            ParsedExtension::SubjectAlternativeName(san) => san
337                .general_names
338                .iter()
339                .filter_map(|gn| match gn {
340                    GeneralName::URI(u) => Some(u.to_string()),
341                    _ => None,
342                })
343                .collect::<Vec<_>>(),
344            _ => Vec::new(),
345        })
346        .collect()
347}
348
349fn collect_san_dns(cert: &X509Certificate) -> Vec<String> {
350    cert.extensions()
351        .iter()
352        .flat_map(|ext| match ext.parsed_extension() {
353            ParsedExtension::SubjectAlternativeName(san) => san
354                .general_names
355                .iter()
356                .filter_map(|gn| match gn {
357                    GeneralName::DNSName(d) => Some(d.to_string()),
358                    _ => None,
359                })
360                .collect::<Vec<_>>(),
361            _ => Vec::new(),
362        })
363        .collect()
364}
365
366fn collect_eku_actions(cert: &X509Certificate) -> Vec<String> {
367    let mut out = Vec::new();
368    for ext in cert.extensions() {
369        if let ParsedExtension::ExtendedKeyUsage(eku) = ext.parsed_extension() {
370            if eku.any {
371                continue;
372            }
373            for oid in &eku.other {
374                if let Some(action) = default_eku_to_action(&oid.to_id_string()) {
375                    out.push(action.to_string());
376                }
377            }
378            if eku.client_auth {
379                out.push("tls.client-auth".to_string());
380            }
381            if eku.server_auth {
382                out.push("tls.server-auth".to_string());
383            }
384            if eku.code_signing {
385                out.push("code.sign".to_string());
386            }
387            if eku.email_protection {
388                out.push("email.protect".to_string());
389            }
390            if eku.time_stamping {
391                out.push("timestamp.sign".to_string());
392            }
393            if eku.ocsp_signing {
394                out.push("ocsp.sign".to_string());
395            }
396        }
397    }
398    // Deduplicate while preserving order.
399    let mut seen = HashSet::new();
400    out.into_iter().filter(|s| seen.insert(s.clone())).collect()
401}
402
403fn parse_common_name(distinguished_name: &str) -> Option<String> {
404    for part in distinguished_name.split(['\n', ',']) {
405        let trimmed = part.trim();
406        if let Some(rest) = trimmed
407            .strip_prefix("CN=")
408            .or_else(|| trimmed.strip_prefix("cn="))
409        {
410            return Some(rest.to_string());
411        }
412    }
413    None
414}
415
416fn encode_actor_path(s: &str) -> String {
417    let mut out = String::with_capacity(s.len());
418    for b in s.bytes() {
419        match b {
420            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
421                out.push(b as char);
422            }
423            _ => out.push_str(&format!("%{:02X}", b)),
424        }
425    }
426    out
427}
428
429fn sha256_hex(bytes: &[u8]) -> String {
430    use sha2::{Digest, Sha256};
431    let digest = Sha256::digest(bytes);
432    digest.iter().map(|b| format!("{:02x}", b)).collect()
433}
434
435fn rfc3339_from_unix(secs: i64) -> String {
436    let (y, m, d, h, mi, s) = secs_to_ymdhms(secs);
437    format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, h, mi, s)
438}
439
440fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
441    let days = secs.div_euclid(86_400);
442    let time = secs.rem_euclid(86_400);
443    let hour = (time / 3600) as u32;
444    let minute = ((time % 3600) / 60) as u32;
445    let second = (time % 60) as u32;
446    let z = days + 719_468;
447    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
448    let doe = (z - era * 146_097) as u64;
449    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
450    let y = yoe as i64 + era * 400;
451    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
452    let mp = (5 * doy + 2) / 153;
453    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
454    let m = if mp < 10 {
455        (mp + 3) as u32
456    } else {
457        (mp - 9) as u32
458    };
459    let year = if m <= 2 { y + 1 } else { y };
460    (year as i32, m, d, hour, minute, second)
461}
462
463use base64::Engine as _;
464
465// =============================================================================
466// OCSP, CRL, exporter binding, and post-handshake re-auth modules.
467//
468// These extensions mirror the TS surface in `tools/tf-types-ts/src/core/bridge-tls.ts`
469// (`checkRevocation`, `deriveExporterKey`, `postHandshakeReauth`). They are
470// intentionally network-free: the caller supplies an `OcspFetcher` for OCSP
471// lookups and pre-loaded DER bytes for CRL parsing. RFC references:
472//   - RFC 6960 (OCSP)
473//   - RFC 5280 (CRL profile)
474//   - RFC 5705 / RFC 8446 §7.5 (TLS exporters)
475//   - RFC 8446 §4.6.2 (post-handshake authentication)
476// =============================================================================
477
478/// X.509 certificate handle used by the OCSP / CRL helpers. We deliberately
479/// keep this thin (raw DER + cached subject + serial) so callers don't have
480/// to depend on `x509-parser`'s `X509Certificate` lifetime in their own
481/// types.
482#[derive(Clone, Debug)]
483pub struct X509Cert {
484    pub der: Vec<u8>,
485    pub subject: String,
486    pub serial_be: Vec<u8>,
487}
488
489impl X509Cert {
490    /// Parse a single DER blob into an `X509Cert` snapshot.
491    pub fn from_der(der: &[u8]) -> Result<Self, BridgeError> {
492        let (_, parsed) = X509Certificate::from_der(der)
493            .map_err(|e| BridgeError::InvalidInput(format!("X509Cert: {}", e)))?;
494        let serial_be = parsed.tbs_certificate.raw_serial().to_vec();
495        Ok(X509Cert {
496            der: der.to_vec(),
497            subject: parsed.subject().to_string(),
498            serial_be,
499        })
500    }
501
502    /// Parse a single PEM block into an `X509Cert`.
503    pub fn from_pem(pem: &str) -> Result<Self, BridgeError> {
504        let der = parse_single_pem(pem)
505            .map_err(|e| BridgeError::InvalidInput(format!("X509Cert PEM: {}", e)))?;
506        Self::from_der(&der)
507    }
508}
509
510// ----- OCSP -----------------------------------------------------------------
511
512/// The decision an OCSP responder returned for a particular certificate.
513#[derive(Clone, Debug, PartialEq, Eq)]
514pub enum OcspStatus {
515    Good,
516    Revoked,
517    Unknown,
518}
519
520/// Trait for callers who actually speak OCSP. Implementations receive the
521/// `(cert, issuer, ocsp_url)` triple and return DER bytes of an
522/// `OCSPResponse` (RFC 6960). The bridge does no network IO itself.
523pub trait OcspFetcher {
524    fn fetch(
525        &self,
526        cert: &X509Cert,
527        issuer: &X509Cert,
528        ocsp_url: &str,
529    ) -> Result<Vec<u8>, BridgeError>;
530}
531
532/// Errors that can come out of OCSP DER parsing / status extraction.
533#[derive(Debug, thiserror::Error, PartialEq, Eq)]
534pub enum OcspError {
535    #[error("OCSP DER parse failed: {0}")]
536    Parse(String),
537    #[error("OCSP responder returned status code {0}")]
538    ResponderError(u8),
539    #[error("OCSP response is not a BasicOCSPResponse")]
540    NotBasic,
541    #[error("OCSP response thisUpdate={this_update} > now={now}")]
542    NotYetValid { this_update: i64, now: i64 },
543    #[error("OCSP response nextUpdate={next_update} < now={now}")]
544    Stale { next_update: i64, now: i64 },
545    #[error("OCSP response contained no SingleResponse entries")]
546    NoSingleResponses,
547}
548
549/// Stateless OCSP checker. Holds no configuration; the caller selects a
550/// fetcher and clock per call.
551pub struct OcspCheck;
552
553impl OcspCheck {
554    /// Run an OCSP query for `cert` (issued by `issuer`). The `fetcher`
555    /// returns DER for an `OCSPResponse`; we parse it, sanity-check the
556    /// `thisUpdate`/`nextUpdate` window against `now_unix_seconds`, and
557    /// extract the status for the first `SingleResponse`.
558    pub fn query(
559        cert: &X509Cert,
560        issuer: &X509Cert,
561        fetcher: &dyn OcspFetcher,
562        ocsp_url: &str,
563        now_unix_seconds: i64,
564    ) -> Result<OcspStatus, BridgeError> {
565        let der = fetcher.fetch(cert, issuer, ocsp_url)?;
566        Self::parse_response(&der, now_unix_seconds).map_err(|e| match e {
567            OcspError::Parse(s) => BridgeError::InvalidInput(format!("OCSP: {}", s)),
568            OcspError::ResponderError(n) => {
569                BridgeError::Rejected(format!("OCSP responder error {}", n))
570            }
571            OcspError::NotBasic => BridgeError::Rejected("OCSP not BasicOCSPResponse".into()),
572            OcspError::NotYetValid { this_update, now } => {
573                BridgeError::Rejected(format!("OCSP thisUpdate={} > now={}", this_update, now))
574            }
575            OcspError::Stale { next_update, now } => {
576                BridgeError::Rejected(format!("OCSP nextUpdate={} < now={}", next_update, now))
577            }
578            OcspError::NoSingleResponses => {
579                BridgeError::Rejected("OCSP had no SingleResponse entries".into())
580            }
581        })
582    }
583
584    /// Pure parser: walks the DER tree, validates the time window, and
585    /// returns the status of the first `SingleResponse`. Exposed for
586    /// testing.
587    pub fn parse_response(der: &[u8], now_unix_seconds: i64) -> Result<OcspStatus, OcspError> {
588        let parsed = ocsp::parse_ocsp_response(der)?;
589        if parsed.response_status != 0 {
590            return Err(OcspError::ResponderError(parsed.response_status));
591        }
592        let basic = parsed.basic.ok_or(OcspError::NotBasic)?;
593        let single = basic
594            .single_responses
595            .first()
596            .ok_or(OcspError::NoSingleResponses)?;
597        if single.this_update > now_unix_seconds {
598            return Err(OcspError::NotYetValid {
599                this_update: single.this_update,
600                now: now_unix_seconds,
601            });
602        }
603        if let Some(next) = single.next_update {
604            if next < now_unix_seconds {
605                return Err(OcspError::Stale {
606                    next_update: next,
607                    now: now_unix_seconds,
608                });
609            }
610        }
611        Ok(single.status.clone())
612    }
613}
614
615/// Internal OCSP DER walker. Implements the *minimum* RFC 6960 surface needed
616/// to decide good / revoked / unknown for the first SingleResponse and
617/// validate the freshness window. We do not verify the responder signature
618/// here; callers that need that should layer their own verification on top
619/// (the responder ID is exposed in `BasicResponseData`). This is consistent
620/// with the TS bridge, which also delegates signature verification to the
621/// caller via the `OcspStatusResolver` callback.
622pub mod ocsp {
623    use super::OcspError;
624    use super::OcspStatus;
625
626    #[derive(Clone, Debug)]
627    pub struct SingleResponse {
628        pub status: OcspStatus,
629        pub this_update: i64,
630        pub next_update: Option<i64>,
631    }
632
633    #[derive(Clone, Debug)]
634    pub struct BasicResponseData {
635        pub single_responses: Vec<SingleResponse>,
636    }
637
638    #[derive(Clone, Debug)]
639    pub struct OcspResponse {
640        pub response_status: u8,
641        pub basic: Option<BasicResponseData>,
642    }
643
644    /// OID `1.3.6.1.5.5.7.48.1.1` — `id-pkix-ocsp-basic`.
645    const ID_PKIX_OCSP_BASIC: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x01];
646
647    pub fn parse_ocsp_response(der: &[u8]) -> Result<OcspResponse, OcspError> {
648        let outer = read_seq(der, 0).ok_or_else(|| OcspError::Parse("outer SEQUENCE".into()))?;
649        let mut p = outer.content_start;
650        let end = outer.content_start + outer.content_len;
651
652        // responseStatus ENUMERATED
653        let status_tlv =
654            read_tlv(der, p).ok_or_else(|| OcspError::Parse("responseStatus tag".into()))?;
655        if status_tlv.tag != 0x0a {
656            return Err(OcspError::Parse(format!(
657                "responseStatus expected ENUMERATED (0x0a), got 0x{:02x}",
658                status_tlv.tag
659            )));
660        }
661        if status_tlv.content_len != 1 {
662            return Err(OcspError::Parse("responseStatus len != 1".into()));
663        }
664        let response_status = der[status_tlv.content_start];
665        p = status_tlv.content_start + status_tlv.content_len;
666
667        let mut basic: Option<BasicResponseData> = None;
668        if p < end {
669            // [0] EXPLICIT ResponseBytes OPTIONAL
670            let rb_tlv = read_tlv(der, p).ok_or_else(|| OcspError::Parse("[0] tag".into()))?;
671            if rb_tlv.tag == 0xa0 {
672                let rb_seq = read_seq(der, rb_tlv.content_start)
673                    .ok_or_else(|| OcspError::Parse("ResponseBytes SEQUENCE".into()))?;
674                let mut q = rb_seq.content_start;
675                let oid_tlv =
676                    read_tlv(der, q).ok_or_else(|| OcspError::Parse("responseType OID".into()))?;
677                if oid_tlv.tag != 0x06 {
678                    return Err(OcspError::Parse("responseType not OID".into()));
679                }
680                let oid_bytes =
681                    &der[oid_tlv.content_start..oid_tlv.content_start + oid_tlv.content_len];
682                if oid_bytes != ID_PKIX_OCSP_BASIC {
683                    // Not a BasicOCSPResponse — leave `basic` as None.
684                } else {
685                    q = oid_tlv.content_start + oid_tlv.content_len;
686                    let response_octets = read_tlv(der, q)
687                        .ok_or_else(|| OcspError::Parse("response OCTET STRING".into()))?;
688                    if response_octets.tag != 0x04 {
689                        return Err(OcspError::Parse("response not OCTET STRING".into()));
690                    }
691                    let basic_der = &der[response_octets.content_start
692                        ..response_octets.content_start + response_octets.content_len];
693                    basic = Some(parse_basic_response(basic_der)?);
694                }
695            }
696        }
697
698        Ok(OcspResponse {
699            response_status,
700            basic,
701        })
702    }
703
704    fn parse_basic_response(der: &[u8]) -> Result<BasicResponseData, OcspError> {
705        let basic_seq =
706            read_seq(der, 0).ok_or_else(|| OcspError::Parse("BasicOCSPResponse SEQ".into()))?;
707        let tbs = read_seq(der, basic_seq.content_start)
708            .ok_or_else(|| OcspError::Parse("tbsResponseData SEQ".into()))?;
709        // Inside tbsResponseData: skip optional [0] version, responderID,
710        // producedAt, responses SEQUENCE OF SingleResponse, [1] responseExtensions.
711        let mut p = tbs.content_start;
712        let end = tbs.content_start + tbs.content_len;
713        // optional [0] version
714        if p < end && der[p] == 0xa0 {
715            let v = read_tlv(der, p).ok_or_else(|| OcspError::Parse("version".into()))?;
716            p = v.content_start + v.content_len;
717        }
718        // responderID is CHOICE [1] byName Name | [2] byKey KeyHash; both are tagged.
719        if p < end {
720            let r = read_tlv(der, p).ok_or_else(|| OcspError::Parse("responderID".into()))?;
721            p = r.content_start + r.content_len;
722        }
723        // producedAt GeneralizedTime
724        if p < end {
725            let pa = read_tlv(der, p).ok_or_else(|| OcspError::Parse("producedAt".into()))?;
726            p = pa.content_start + pa.content_len;
727        }
728        // responses SEQUENCE OF SingleResponse
729        let resp_seq =
730            read_seq(der, p).ok_or_else(|| OcspError::Parse("responses SEQUENCE OF".into()))?;
731        let mut single_responses: Vec<SingleResponse> = Vec::new();
732        let mut q = resp_seq.content_start;
733        let qend = resp_seq.content_start + resp_seq.content_len;
734        while q < qend {
735            let sr = read_seq(der, q).ok_or_else(|| OcspError::Parse("SingleResponse".into()))?;
736            single_responses.push(parse_single_response(
737                &der[sr.content_start..sr.content_start + sr.content_len],
738            )?);
739            q = sr.content_start + sr.content_len;
740        }
741        Ok(BasicResponseData { single_responses })
742    }
743
744    fn parse_single_response(der: &[u8]) -> Result<SingleResponse, OcspError> {
745        // SingleResponse ::= SEQUENCE { certID, certStatus, thisUpdate,
746        //                                nextUpdate [0] OPTIONAL, ... }
747        let mut p = 0usize;
748        let end = der.len();
749        // certID SEQUENCE
750        let cert_id = read_seq(der, p).ok_or_else(|| OcspError::Parse("certID".into()))?;
751        p = cert_id.content_start + cert_id.content_len;
752        // certStatus CHOICE
753        let cs = read_tlv(der, p).ok_or_else(|| OcspError::Parse("certStatus".into()))?;
754        let status = match cs.tag {
755            0x80 => OcspStatus::Good,    // [0] IMPLICIT NULL
756            0xa1 => OcspStatus::Revoked, // [1] IMPLICIT RevokedInfo
757            0x82 => OcspStatus::Unknown, // [2] IMPLICIT UnknownInfo
758            other => {
759                return Err(OcspError::Parse(format!(
760                    "unknown certStatus tag 0x{:02x}",
761                    other
762                )))
763            }
764        };
765        p = cs.content_start + cs.content_len;
766        // thisUpdate GeneralizedTime (tag 0x18)
767        let tu = read_tlv(der, p).ok_or_else(|| OcspError::Parse("thisUpdate".into()))?;
768        if tu.tag != 0x18 {
769            return Err(OcspError::Parse(format!(
770                "thisUpdate expected 0x18, got 0x{:02x}",
771                tu.tag
772            )));
773        }
774        let this_update =
775            parse_generalized_time(&der[tu.content_start..tu.content_start + tu.content_len])?;
776        p = tu.content_start + tu.content_len;
777        // optional [0] nextUpdate
778        let mut next_update: Option<i64> = None;
779        if p < end && der[p] == 0xa0 {
780            let nu_outer =
781                read_tlv(der, p).ok_or_else(|| OcspError::Parse("nextUpdate [0]".into()))?;
782            let inner = read_tlv(der, nu_outer.content_start)
783                .ok_or_else(|| OcspError::Parse("nextUpdate inner".into()))?;
784            if inner.tag != 0x18 {
785                return Err(OcspError::Parse(format!(
786                    "nextUpdate expected 0x18, got 0x{:02x}",
787                    inner.tag
788                )));
789            }
790            next_update = Some(parse_generalized_time(
791                &der[inner.content_start..inner.content_start + inner.content_len],
792            )?);
793        }
794        Ok(SingleResponse {
795            status,
796            this_update,
797            next_update,
798        })
799    }
800
801    /// Parse a YYYYMMDDHHMMSS[.fff]Z GeneralizedTime into a unix timestamp.
802    /// Only the `Z` (UTC) form is accepted — RFC 5280 / 6960 require it.
803    fn parse_generalized_time(bytes: &[u8]) -> Result<i64, OcspError> {
804        let s = std::str::from_utf8(bytes)
805            .map_err(|_| OcspError::Parse("generalized time non-utf8".into()))?;
806        if !s.ends_with('Z') {
807            return Err(OcspError::Parse(
808                "generalized time must be Zulu-suffixed".into(),
809            ));
810        }
811        let core = &s[..s.len() - 1];
812        if core.len() < 14 {
813            return Err(OcspError::Parse("generalized time too short".into()));
814        }
815        let y: i32 = core[0..4]
816            .parse()
817            .map_err(|_| OcspError::Parse("year".into()))?;
818        let m: u32 = core[4..6]
819            .parse()
820            .map_err(|_| OcspError::Parse("month".into()))?;
821        let d: u32 = core[6..8]
822            .parse()
823            .map_err(|_| OcspError::Parse("day".into()))?;
824        let hh: u32 = core[8..10]
825            .parse()
826            .map_err(|_| OcspError::Parse("hour".into()))?;
827        let mm: u32 = core[10..12]
828            .parse()
829            .map_err(|_| OcspError::Parse("min".into()))?;
830        let ss: u32 = core[12..14]
831            .parse()
832            .map_err(|_| OcspError::Parse("sec".into()))?;
833        Ok(ymdhms_to_unix(y, m, d, hh, mm, ss))
834    }
835
836    fn ymdhms_to_unix(y: i32, m: u32, d: u32, hh: u32, mm: u32, ss: u32) -> i64 {
837        // Inverse of `secs_to_ymdhms` in the surrounding module. Howard Hinnant's
838        // days_from_civil algorithm.
839        let yy = if m <= 2 { y - 1 } else { y } as i64;
840        let era = if yy >= 0 { yy } else { yy - 399 } / 400;
841        let yoe = (yy - era * 400) as u64;
842        let mp = if m > 2 { m - 3 } else { m + 9 } as u64;
843        let doy = (153 * mp + 2) / 5 + (d as u64) - 1;
844        let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
845        let days = era * 146_097 + doe as i64 - 719_468;
846        days * 86_400 + hh as i64 * 3600 + mm as i64 * 60 + ss as i64
847    }
848
849    // ---- TLV walker ---------------------------------------------------------
850
851    pub(crate) struct Tlv {
852        pub tag: u8,
853        pub content_start: usize,
854        pub content_len: usize,
855    }
856
857    pub(crate) fn read_seq(buf: &[u8], pos: usize) -> Option<Tlv> {
858        let t = read_tlv(buf, pos)?;
859        if t.tag != 0x30 {
860            return None;
861        }
862        Some(t)
863    }
864
865    pub(crate) fn read_tlv(buf: &[u8], pos: usize) -> Option<Tlv> {
866        if pos >= buf.len() {
867            return None;
868        }
869        let tag = buf[pos];
870        let (len, header) = read_length(buf, pos + 1)?;
871        if pos + 1 + header + len > buf.len() {
872            return None;
873        }
874        Some(Tlv {
875            tag,
876            content_start: pos + 1 + header,
877            content_len: len,
878        })
879    }
880
881    fn read_length(buf: &[u8], pos: usize) -> Option<(usize, usize)> {
882        if pos >= buf.len() {
883            return None;
884        }
885        let b = buf[pos];
886        if b < 0x80 {
887            return Some((b as usize, 1));
888        }
889        let n = (b & 0x7f) as usize;
890        if n == 0 || n > 4 || pos + n >= buf.len() {
891            return None;
892        }
893        let mut len: usize = 0;
894        for i in 0..n {
895            len = (len << 8) | buf[pos + 1 + i] as usize;
896        }
897        Some((len, 1 + n))
898    }
899}
900
901// ----- CRL ------------------------------------------------------------------
902
903#[derive(Clone, Debug, PartialEq, Eq)]
904pub struct RevocationEntry {
905    /// Big-endian serial number bytes (no DER tag/length prefix, no sign byte).
906    pub serial_be: Vec<u8>,
907    /// `revocationDate` as a unix timestamp.
908    pub revocation_date: i64,
909    /// Optional `reasonCode` extension (RFC 5280 §5.3.1). `None` if absent.
910    pub reason_code: Option<u8>,
911}
912
913#[derive(Debug, thiserror::Error, PartialEq, Eq)]
914pub enum CrlError {
915    #[error("CRL DER parse failed: {0}")]
916    Parse(String),
917}
918
919/// Indexed CRL — a parsed RFC 5280 v2 CRL with a `BTreeMap` keyed on serial
920/// for `O(log n)` lookups. We hold the *normalised* big-endian serial bytes
921/// (leading 0x00 sign-extension byte stripped), so callers can pass
922/// either `cert.serial_be` or a raw hex value normalised the same way.
923pub struct CrlIndex {
924    pub issuer: String,
925    pub this_update: i64,
926    pub next_update: Option<i64>,
927    revoked: std::collections::BTreeMap<Vec<u8>, RevocationEntry>,
928}
929
930impl CrlIndex {
931    pub fn issuer(&self) -> &str {
932        &self.issuer
933    }
934    pub fn len(&self) -> usize {
935        self.revoked.len()
936    }
937    pub fn is_empty(&self) -> bool {
938        self.revoked.is_empty()
939    }
940    /// Fast lookup. Pass big-endian serial bytes; we normalise the leading
941    /// 0x00 byte that DER inserts on positive integers whose top bit is
942    /// set, so `0x00 || …` and the bare integer compare equal.
943    pub fn is_revoked(&self, serial_be: &[u8]) -> Option<&RevocationEntry> {
944        let key = normalise_serial(serial_be);
945        self.revoked.get(&key)
946    }
947    /// Iterator over all entries (sorted by serial).
948    pub fn entries(&self) -> impl Iterator<Item = &RevocationEntry> {
949        self.revoked.values()
950    }
951}
952
953fn normalise_serial(serial_be: &[u8]) -> Vec<u8> {
954    let mut s = serial_be;
955    while s.len() > 1 && s[0] == 0x00 {
956        s = &s[1..];
957    }
958    s.to_vec()
959}
960
961pub struct CrlCheck;
962
963impl CrlCheck {
964    pub fn load(crl_bytes: &[u8]) -> Result<CrlIndex, CrlError> {
965        use x509_parser::revocation_list::CertificateRevocationList;
966        let (_, crl) = CertificateRevocationList::from_der(crl_bytes)
967            .map_err(|e| CrlError::Parse(format!("{}", e)))?;
968        let issuer = crl.issuer().to_string();
969        let this_update = crl.last_update().timestamp();
970        let next_update = crl.next_update().map(|t| t.timestamp());
971        let mut revoked = std::collections::BTreeMap::new();
972        for r in crl.iter_revoked_certificates() {
973            let serial_be = normalise_serial(r.raw_serial());
974            let revocation_date = r.revocation_date.timestamp();
975            let reason_code = r
976                .extensions()
977                .iter()
978                .find_map(|ext| match ext.parsed_extension() {
979                    x509_parser::extensions::ParsedExtension::ReasonCode(rc) => Some(rc.0),
980                    _ => None,
981                });
982            revoked.insert(
983                serial_be.clone(),
984                RevocationEntry {
985                    serial_be,
986                    revocation_date,
987                    reason_code,
988                },
989            );
990        }
991        Ok(CrlIndex {
992            issuer,
993            this_update,
994            next_update,
995            revoked,
996        })
997    }
998}
999
1000// ----- Exporter binding ------------------------------------------------------
1001
1002pub struct ExporterBinding;
1003
1004impl ExporterBinding {
1005    /// Mirrors `TlsBridge.deriveExporterKey` in TS:
1006    ///   salt = utf8("tf-tls-exporter:" + label)
1007    ///   ikm  = transport_secret || context
1008    ///   prk1 = HMAC-SHA256(salt, ikm)
1009    ///   okm  = HKDF(sha256, prk1, salt=undefined, info=salt, length)
1010    ///        = HKDF-Expand(HMAC-SHA256(zeros[32], prk1), info=salt, length)
1011    ///
1012    /// This is *not* RFC 5705 by itself — the `transport_secret` is the
1013    /// output of `RFC 5705 §4` exporter on the underlying TLS / QUIC
1014    /// session, and we layer another HKDF on top so the TrustForge
1015    /// session PSK is domain-separated from anything the application
1016    /// may already derive from the same exporter.
1017    pub fn derive(transport_secret: &[u8], label: &str, context: &[u8], length: usize) -> Vec<u8> {
1018        if transport_secret.is_empty() {
1019            // Stay in lock-step with the TS bridge, which throws on empty.
1020            // We can't return BridgeError here; the TS shape uses a runtime
1021            // exception. Returning an empty vec would be misleading, so we
1022            // panic — same effect as a thrown exception, same surface as
1023            // `expect("non-empty")` elsewhere in this crate.
1024            panic!("ExporterBinding::derive: transport_secret must be non-empty");
1025        }
1026        let salt_str = format!("tf-tls-exporter:{}", label);
1027        let salt = salt_str.as_bytes();
1028
1029        // prk1 = HMAC-SHA256(salt, ikm)
1030        use hmac::{Hmac, Mac};
1031        type HmacSha256 = Hmac<sha2::Sha256>;
1032        let mut mac = HmacSha256::new_from_slice(salt).expect("hmac key");
1033        mac.update(transport_secret);
1034        mac.update(context);
1035        let prk1 = mac.finalize().into_bytes();
1036
1037        // okm = HKDF(prk1, salt=undefined → zero, info=salt, length)
1038        // i.e. extract zeros over prk1, then expand with info=salt.
1039        let hk = hkdf::Hkdf::<sha2::Sha256>::new(None, &prk1);
1040        let mut out = vec![0u8; length];
1041        hk.expand(salt, &mut out).expect("HKDF expand");
1042        out
1043    }
1044}
1045
1046// ----- Post-handshake re-auth ------------------------------------------------
1047
1048pub struct PostHandshakeReauth;
1049
1050impl PostHandshakeReauth {
1051    /// Returns 32 random challenge bytes the verifier sends to the peer.
1052    pub fn challenge() -> Vec<u8> {
1053        use rand::RngCore;
1054        let mut buf = vec![0u8; 32];
1055        rand::rngs::OsRng.fill_bytes(&mut buf);
1056        buf
1057    }
1058
1059    /// Verifies an Ed25519 signature over the previously issued challenge.
1060    /// Returns `true` iff the signature is valid.
1061    pub fn verify_response(challenge: &[u8], pubkey: &[u8; 32], signature: &[u8; 64]) -> bool {
1062        crate::crypto::ed25519_verify(pubkey, challenge, signature).is_ok()
1063    }
1064}