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            crate::encoding::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
463
464// =============================================================================
465// OCSP, CRL, exporter binding, and post-handshake re-auth modules.
466//
467// These extensions mirror the TS surface in `tools/tf-types-ts/src/core/bridge-tls.ts`
468// (`checkRevocation`, `deriveExporterKey`, `postHandshakeReauth`). They are
469// intentionally network-free: the caller supplies an `OcspFetcher` for OCSP
470// lookups and pre-loaded DER bytes for CRL parsing. RFC references:
471//   - RFC 6960 (OCSP)
472//   - RFC 5280 (CRL profile)
473//   - RFC 5705 / RFC 8446 §7.5 (TLS exporters)
474//   - RFC 8446 §4.6.2 (post-handshake authentication)
475// =============================================================================
476
477/// X.509 certificate handle used by the OCSP / CRL helpers. We deliberately
478/// keep this thin (raw DER + cached subject + serial) so callers don't have
479/// to depend on `x509-parser`'s `X509Certificate` lifetime in their own
480/// types.
481#[derive(Clone, Debug)]
482pub struct X509Cert {
483    pub der: Vec<u8>,
484    pub subject: String,
485    pub serial_be: Vec<u8>,
486}
487
488impl X509Cert {
489    /// Parse a single DER blob into an `X509Cert` snapshot.
490    pub fn from_der(der: &[u8]) -> Result<Self, BridgeError> {
491        let (_, parsed) = X509Certificate::from_der(der)
492            .map_err(|e| BridgeError::InvalidInput(format!("X509Cert: {}", e)))?;
493        let serial_be = parsed.tbs_certificate.raw_serial().to_vec();
494        Ok(X509Cert {
495            der: der.to_vec(),
496            subject: parsed.subject().to_string(),
497            serial_be,
498        })
499    }
500
501    /// Parse a single PEM block into an `X509Cert`.
502    pub fn from_pem(pem: &str) -> Result<Self, BridgeError> {
503        let der = parse_single_pem(pem)
504            .map_err(|e| BridgeError::InvalidInput(format!("X509Cert PEM: {}", e)))?;
505        Self::from_der(&der)
506    }
507}
508
509// ----- OCSP -----------------------------------------------------------------
510
511/// The decision an OCSP responder returned for a particular certificate.
512#[derive(Clone, Debug, PartialEq, Eq)]
513pub enum OcspStatus {
514    Good,
515    Revoked,
516    Unknown,
517}
518
519/// Trait for callers who actually speak OCSP. Implementations receive the
520/// `(cert, issuer, ocsp_url)` triple and return DER bytes of an
521/// `OCSPResponse` (RFC 6960). The bridge does no network IO itself.
522pub trait OcspFetcher {
523    fn fetch(
524        &self,
525        cert: &X509Cert,
526        issuer: &X509Cert,
527        ocsp_url: &str,
528    ) -> Result<Vec<u8>, BridgeError>;
529}
530
531/// Errors that can come out of OCSP DER parsing / status extraction.
532#[derive(Debug, thiserror::Error, PartialEq, Eq)]
533pub enum OcspError {
534    #[error("OCSP DER parse failed: {0}")]
535    Parse(String),
536    #[error("OCSP responder returned status code {0}")]
537    ResponderError(u8),
538    #[error("OCSP response is not a BasicOCSPResponse")]
539    NotBasic,
540    #[error("OCSP response thisUpdate={this_update} > now={now}")]
541    NotYetValid { this_update: i64, now: i64 },
542    #[error("OCSP response nextUpdate={next_update} < now={now}")]
543    Stale { next_update: i64, now: i64 },
544    #[error("OCSP response contained no SingleResponse entries")]
545    NoSingleResponses,
546}
547
548/// Stateless OCSP checker. Holds no configuration; the caller selects a
549/// fetcher and clock per call.
550pub struct OcspCheck;
551
552impl OcspCheck {
553    /// Run an OCSP query for `cert` (issued by `issuer`). The `fetcher`
554    /// returns DER for an `OCSPResponse`; we parse it, sanity-check the
555    /// `thisUpdate`/`nextUpdate` window against `now_unix_seconds`, and
556    /// extract the status for the first `SingleResponse`.
557    pub fn query(
558        cert: &X509Cert,
559        issuer: &X509Cert,
560        fetcher: &dyn OcspFetcher,
561        ocsp_url: &str,
562        now_unix_seconds: i64,
563    ) -> Result<OcspStatus, BridgeError> {
564        let der = fetcher.fetch(cert, issuer, ocsp_url)?;
565        Self::parse_response(&der, now_unix_seconds).map_err(|e| match e {
566            OcspError::Parse(s) => BridgeError::InvalidInput(format!("OCSP: {}", s)),
567            OcspError::ResponderError(n) => {
568                BridgeError::Rejected(format!("OCSP responder error {}", n))
569            }
570            OcspError::NotBasic => BridgeError::Rejected("OCSP not BasicOCSPResponse".into()),
571            OcspError::NotYetValid { this_update, now } => {
572                BridgeError::Rejected(format!("OCSP thisUpdate={} > now={}", this_update, now))
573            }
574            OcspError::Stale { next_update, now } => {
575                BridgeError::Rejected(format!("OCSP nextUpdate={} < now={}", next_update, now))
576            }
577            OcspError::NoSingleResponses => {
578                BridgeError::Rejected("OCSP had no SingleResponse entries".into())
579            }
580        })
581    }
582
583    /// Pure parser: walks the DER tree, validates the time window, and
584    /// returns the status of the first `SingleResponse`. Exposed for
585    /// testing.
586    pub fn parse_response(der: &[u8], now_unix_seconds: i64) -> Result<OcspStatus, OcspError> {
587        let parsed = ocsp::parse_ocsp_response(der)?;
588        if parsed.response_status != 0 {
589            return Err(OcspError::ResponderError(parsed.response_status));
590        }
591        let basic = parsed.basic.ok_or(OcspError::NotBasic)?;
592        let single = basic
593            .single_responses
594            .first()
595            .ok_or(OcspError::NoSingleResponses)?;
596        if single.this_update > now_unix_seconds {
597            return Err(OcspError::NotYetValid {
598                this_update: single.this_update,
599                now: now_unix_seconds,
600            });
601        }
602        if let Some(next) = single.next_update {
603            if next < now_unix_seconds {
604                return Err(OcspError::Stale {
605                    next_update: next,
606                    now: now_unix_seconds,
607                });
608            }
609        }
610        Ok(single.status.clone())
611    }
612}
613
614/// Internal OCSP DER walker. Implements the *minimum* RFC 6960 surface needed
615/// to decide good / revoked / unknown for the first SingleResponse and
616/// validate the freshness window. We do not verify the responder signature
617/// here; callers that need that should layer their own verification on top
618/// (the responder ID is exposed in `BasicResponseData`). This is consistent
619/// with the TS bridge, which also delegates signature verification to the
620/// caller via the `OcspStatusResolver` callback.
621pub mod ocsp {
622    use super::OcspError;
623    use super::OcspStatus;
624
625    #[derive(Clone, Debug)]
626    pub struct SingleResponse {
627        pub status: OcspStatus,
628        pub this_update: i64,
629        pub next_update: Option<i64>,
630    }
631
632    #[derive(Clone, Debug)]
633    pub struct BasicResponseData {
634        pub single_responses: Vec<SingleResponse>,
635    }
636
637    #[derive(Clone, Debug)]
638    pub struct OcspResponse {
639        pub response_status: u8,
640        pub basic: Option<BasicResponseData>,
641    }
642
643    /// OID `1.3.6.1.5.5.7.48.1.1` — `id-pkix-ocsp-basic`.
644    const ID_PKIX_OCSP_BASIC: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x01];
645
646    pub fn parse_ocsp_response(der: &[u8]) -> Result<OcspResponse, OcspError> {
647        let outer = read_seq(der, 0).ok_or_else(|| OcspError::Parse("outer SEQUENCE".into()))?;
648        let mut p = outer.content_start;
649        let end = outer.content_start + outer.content_len;
650
651        // responseStatus ENUMERATED
652        let status_tlv =
653            read_tlv(der, p).ok_or_else(|| OcspError::Parse("responseStatus tag".into()))?;
654        if status_tlv.tag != 0x0a {
655            return Err(OcspError::Parse(format!(
656                "responseStatus expected ENUMERATED (0x0a), got 0x{:02x}",
657                status_tlv.tag
658            )));
659        }
660        if status_tlv.content_len != 1 {
661            return Err(OcspError::Parse("responseStatus len != 1".into()));
662        }
663        let response_status = der[status_tlv.content_start];
664        p = status_tlv.content_start + status_tlv.content_len;
665
666        let mut basic: Option<BasicResponseData> = None;
667        if p < end {
668            // [0] EXPLICIT ResponseBytes OPTIONAL
669            let rb_tlv = read_tlv(der, p).ok_or_else(|| OcspError::Parse("[0] tag".into()))?;
670            if rb_tlv.tag == 0xa0 {
671                let rb_seq = read_seq(der, rb_tlv.content_start)
672                    .ok_or_else(|| OcspError::Parse("ResponseBytes SEQUENCE".into()))?;
673                let mut q = rb_seq.content_start;
674                let oid_tlv =
675                    read_tlv(der, q).ok_or_else(|| OcspError::Parse("responseType OID".into()))?;
676                if oid_tlv.tag != 0x06 {
677                    return Err(OcspError::Parse("responseType not OID".into()));
678                }
679                let oid_bytes =
680                    &der[oid_tlv.content_start..oid_tlv.content_start + oid_tlv.content_len];
681                if oid_bytes != ID_PKIX_OCSP_BASIC {
682                    // Not a BasicOCSPResponse — leave `basic` as None.
683                } else {
684                    q = oid_tlv.content_start + oid_tlv.content_len;
685                    let response_octets = read_tlv(der, q)
686                        .ok_or_else(|| OcspError::Parse("response OCTET STRING".into()))?;
687                    if response_octets.tag != 0x04 {
688                        return Err(OcspError::Parse("response not OCTET STRING".into()));
689                    }
690                    let basic_der = &der[response_octets.content_start
691                        ..response_octets.content_start + response_octets.content_len];
692                    basic = Some(parse_basic_response(basic_der)?);
693                }
694            }
695        }
696
697        Ok(OcspResponse {
698            response_status,
699            basic,
700        })
701    }
702
703    fn parse_basic_response(der: &[u8]) -> Result<BasicResponseData, OcspError> {
704        let basic_seq =
705            read_seq(der, 0).ok_or_else(|| OcspError::Parse("BasicOCSPResponse SEQ".into()))?;
706        let tbs = read_seq(der, basic_seq.content_start)
707            .ok_or_else(|| OcspError::Parse("tbsResponseData SEQ".into()))?;
708        // Inside tbsResponseData: skip optional [0] version, responderID,
709        // producedAt, responses SEQUENCE OF SingleResponse, [1] responseExtensions.
710        let mut p = tbs.content_start;
711        let end = tbs.content_start + tbs.content_len;
712        // optional [0] version
713        if p < end && der[p] == 0xa0 {
714            let v = read_tlv(der, p).ok_or_else(|| OcspError::Parse("version".into()))?;
715            p = v.content_start + v.content_len;
716        }
717        // responderID is CHOICE [1] byName Name | [2] byKey KeyHash; both are tagged.
718        if p < end {
719            let r = read_tlv(der, p).ok_or_else(|| OcspError::Parse("responderID".into()))?;
720            p = r.content_start + r.content_len;
721        }
722        // producedAt GeneralizedTime
723        if p < end {
724            let pa = read_tlv(der, p).ok_or_else(|| OcspError::Parse("producedAt".into()))?;
725            p = pa.content_start + pa.content_len;
726        }
727        // responses SEQUENCE OF SingleResponse
728        let resp_seq =
729            read_seq(der, p).ok_or_else(|| OcspError::Parse("responses SEQUENCE OF".into()))?;
730        let mut single_responses: Vec<SingleResponse> = Vec::new();
731        let mut q = resp_seq.content_start;
732        let qend = resp_seq.content_start + resp_seq.content_len;
733        while q < qend {
734            let sr = read_seq(der, q).ok_or_else(|| OcspError::Parse("SingleResponse".into()))?;
735            single_responses.push(parse_single_response(
736                &der[sr.content_start..sr.content_start + sr.content_len],
737            )?);
738            q = sr.content_start + sr.content_len;
739        }
740        Ok(BasicResponseData { single_responses })
741    }
742
743    fn parse_single_response(der: &[u8]) -> Result<SingleResponse, OcspError> {
744        // SingleResponse ::= SEQUENCE { certID, certStatus, thisUpdate,
745        //                                nextUpdate [0] OPTIONAL, ... }
746        let mut p = 0usize;
747        let end = der.len();
748        // certID SEQUENCE
749        let cert_id = read_seq(der, p).ok_or_else(|| OcspError::Parse("certID".into()))?;
750        p = cert_id.content_start + cert_id.content_len;
751        // certStatus CHOICE
752        let cs = read_tlv(der, p).ok_or_else(|| OcspError::Parse("certStatus".into()))?;
753        let status = match cs.tag {
754            0x80 => OcspStatus::Good,    // [0] IMPLICIT NULL
755            0xa1 => OcspStatus::Revoked, // [1] IMPLICIT RevokedInfo
756            0x82 => OcspStatus::Unknown, // [2] IMPLICIT UnknownInfo
757            other => {
758                return Err(OcspError::Parse(format!(
759                    "unknown certStatus tag 0x{:02x}",
760                    other
761                )))
762            }
763        };
764        p = cs.content_start + cs.content_len;
765        // thisUpdate GeneralizedTime (tag 0x18)
766        let tu = read_tlv(der, p).ok_or_else(|| OcspError::Parse("thisUpdate".into()))?;
767        if tu.tag != 0x18 {
768            return Err(OcspError::Parse(format!(
769                "thisUpdate expected 0x18, got 0x{:02x}",
770                tu.tag
771            )));
772        }
773        let this_update =
774            parse_generalized_time(&der[tu.content_start..tu.content_start + tu.content_len])?;
775        p = tu.content_start + tu.content_len;
776        // optional [0] nextUpdate
777        let mut next_update: Option<i64> = None;
778        if p < end && der[p] == 0xa0 {
779            let nu_outer =
780                read_tlv(der, p).ok_or_else(|| OcspError::Parse("nextUpdate [0]".into()))?;
781            let inner = read_tlv(der, nu_outer.content_start)
782                .ok_or_else(|| OcspError::Parse("nextUpdate inner".into()))?;
783            if inner.tag != 0x18 {
784                return Err(OcspError::Parse(format!(
785                    "nextUpdate expected 0x18, got 0x{:02x}",
786                    inner.tag
787                )));
788            }
789            next_update = Some(parse_generalized_time(
790                &der[inner.content_start..inner.content_start + inner.content_len],
791            )?);
792        }
793        Ok(SingleResponse {
794            status,
795            this_update,
796            next_update,
797        })
798    }
799
800    /// Parse a YYYYMMDDHHMMSS[.fff]Z GeneralizedTime into a unix timestamp.
801    /// Only the `Z` (UTC) form is accepted — RFC 5280 / 6960 require it.
802    fn parse_generalized_time(bytes: &[u8]) -> Result<i64, OcspError> {
803        let s = std::str::from_utf8(bytes)
804            .map_err(|_| OcspError::Parse("generalized time non-utf8".into()))?;
805        if !s.ends_with('Z') {
806            return Err(OcspError::Parse(
807                "generalized time must be Zulu-suffixed".into(),
808            ));
809        }
810        let core = &s[..s.len() - 1];
811        if core.len() < 14 {
812            return Err(OcspError::Parse("generalized time too short".into()));
813        }
814        let y: i32 = core[0..4]
815            .parse()
816            .map_err(|_| OcspError::Parse("year".into()))?;
817        let m: u32 = core[4..6]
818            .parse()
819            .map_err(|_| OcspError::Parse("month".into()))?;
820        let d: u32 = core[6..8]
821            .parse()
822            .map_err(|_| OcspError::Parse("day".into()))?;
823        let hh: u32 = core[8..10]
824            .parse()
825            .map_err(|_| OcspError::Parse("hour".into()))?;
826        let mm: u32 = core[10..12]
827            .parse()
828            .map_err(|_| OcspError::Parse("min".into()))?;
829        let ss: u32 = core[12..14]
830            .parse()
831            .map_err(|_| OcspError::Parse("sec".into()))?;
832        Ok(ymdhms_to_unix(y, m, d, hh, mm, ss))
833    }
834
835    fn ymdhms_to_unix(y: i32, m: u32, d: u32, hh: u32, mm: u32, ss: u32) -> i64 {
836        // Inverse of `secs_to_ymdhms` in the surrounding module. Howard Hinnant's
837        // days_from_civil algorithm.
838        let yy = if m <= 2 { y - 1 } else { y } as i64;
839        let era = if yy >= 0 { yy } else { yy - 399 } / 400;
840        let yoe = (yy - era * 400) as u64;
841        let mp = if m > 2 { m - 3 } else { m + 9 } as u64;
842        let doy = (153 * mp + 2) / 5 + (d as u64) - 1;
843        let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
844        let days = era * 146_097 + doe as i64 - 719_468;
845        days * 86_400 + hh as i64 * 3600 + mm as i64 * 60 + ss as i64
846    }
847
848    // ---- TLV walker ---------------------------------------------------------
849
850    pub(crate) struct Tlv {
851        pub tag: u8,
852        pub content_start: usize,
853        pub content_len: usize,
854    }
855
856    pub(crate) fn read_seq(buf: &[u8], pos: usize) -> Option<Tlv> {
857        let t = read_tlv(buf, pos)?;
858        if t.tag != 0x30 {
859            return None;
860        }
861        Some(t)
862    }
863
864    pub(crate) fn read_tlv(buf: &[u8], pos: usize) -> Option<Tlv> {
865        if pos >= buf.len() {
866            return None;
867        }
868        let tag = buf[pos];
869        let (len, header) = read_length(buf, pos + 1)?;
870        if pos + 1 + header + len > buf.len() {
871            return None;
872        }
873        Some(Tlv {
874            tag,
875            content_start: pos + 1 + header,
876            content_len: len,
877        })
878    }
879
880    fn read_length(buf: &[u8], pos: usize) -> Option<(usize, usize)> {
881        if pos >= buf.len() {
882            return None;
883        }
884        let b = buf[pos];
885        if b < 0x80 {
886            return Some((b as usize, 1));
887        }
888        let n = (b & 0x7f) as usize;
889        if n == 0 || n > 4 || pos + n >= buf.len() {
890            return None;
891        }
892        let mut len: usize = 0;
893        for i in 0..n {
894            len = (len << 8) | buf[pos + 1 + i] as usize;
895        }
896        Some((len, 1 + n))
897    }
898}
899
900// ----- CRL ------------------------------------------------------------------
901
902#[derive(Clone, Debug, PartialEq, Eq)]
903pub struct RevocationEntry {
904    /// Big-endian serial number bytes (no DER tag/length prefix, no sign byte).
905    pub serial_be: Vec<u8>,
906    /// `revocationDate` as a unix timestamp.
907    pub revocation_date: i64,
908    /// Optional `reasonCode` extension (RFC 5280 §5.3.1). `None` if absent.
909    pub reason_code: Option<u8>,
910}
911
912#[derive(Debug, thiserror::Error, PartialEq, Eq)]
913pub enum CrlError {
914    #[error("CRL DER parse failed: {0}")]
915    Parse(String),
916}
917
918/// Indexed CRL — a parsed RFC 5280 v2 CRL with a `BTreeMap` keyed on serial
919/// for `O(log n)` lookups. We hold the *normalised* big-endian serial bytes
920/// (leading 0x00 sign-extension byte stripped), so callers can pass
921/// either `cert.serial_be` or a raw hex value normalised the same way.
922pub struct CrlIndex {
923    pub issuer: String,
924    pub this_update: i64,
925    pub next_update: Option<i64>,
926    revoked: std::collections::BTreeMap<Vec<u8>, RevocationEntry>,
927}
928
929impl CrlIndex {
930    pub fn issuer(&self) -> &str {
931        &self.issuer
932    }
933    pub fn len(&self) -> usize {
934        self.revoked.len()
935    }
936    pub fn is_empty(&self) -> bool {
937        self.revoked.is_empty()
938    }
939    /// Fast lookup. Pass big-endian serial bytes; we normalise the leading
940    /// 0x00 byte that DER inserts on positive integers whose top bit is
941    /// set, so `0x00 || …` and the bare integer compare equal.
942    pub fn is_revoked(&self, serial_be: &[u8]) -> Option<&RevocationEntry> {
943        let key = normalise_serial(serial_be);
944        self.revoked.get(&key)
945    }
946    /// Iterator over all entries (sorted by serial).
947    pub fn entries(&self) -> impl Iterator<Item = &RevocationEntry> {
948        self.revoked.values()
949    }
950}
951
952fn normalise_serial(serial_be: &[u8]) -> Vec<u8> {
953    let mut s = serial_be;
954    while s.len() > 1 && s[0] == 0x00 {
955        s = &s[1..];
956    }
957    s.to_vec()
958}
959
960pub struct CrlCheck;
961
962impl CrlCheck {
963    pub fn load(crl_bytes: &[u8]) -> Result<CrlIndex, CrlError> {
964        use x509_parser::revocation_list::CertificateRevocationList;
965        let (_, crl) = CertificateRevocationList::from_der(crl_bytes)
966            .map_err(|e| CrlError::Parse(format!("{}", e)))?;
967        let issuer = crl.issuer().to_string();
968        let this_update = crl.last_update().timestamp();
969        let next_update = crl.next_update().map(|t| t.timestamp());
970        let mut revoked = std::collections::BTreeMap::new();
971        for r in crl.iter_revoked_certificates() {
972            let serial_be = normalise_serial(r.raw_serial());
973            let revocation_date = r.revocation_date.timestamp();
974            let reason_code = r
975                .extensions()
976                .iter()
977                .find_map(|ext| match ext.parsed_extension() {
978                    x509_parser::extensions::ParsedExtension::ReasonCode(rc) => Some(rc.0),
979                    _ => None,
980                });
981            revoked.insert(
982                serial_be.clone(),
983                RevocationEntry {
984                    serial_be,
985                    revocation_date,
986                    reason_code,
987                },
988            );
989        }
990        Ok(CrlIndex {
991            issuer,
992            this_update,
993            next_update,
994            revoked,
995        })
996    }
997}
998
999// ----- Exporter binding ------------------------------------------------------
1000
1001pub struct ExporterBinding;
1002
1003impl ExporterBinding {
1004    /// Mirrors `TlsBridge.deriveExporterKey` in TS:
1005    ///   salt = utf8("tf-tls-exporter:" + label)
1006    ///   ikm  = transport_secret || context
1007    ///   prk1 = HMAC-SHA256(salt, ikm)
1008    ///   okm  = HKDF(sha256, prk1, salt=undefined, info=salt, length)
1009    ///        = HKDF-Expand(HMAC-SHA256(zeros[32], prk1), info=salt, length)
1010    ///
1011    /// This is *not* RFC 5705 by itself — the `transport_secret` is the
1012    /// output of `RFC 5705 §4` exporter on the underlying TLS / QUIC
1013    /// session, and we layer another HKDF on top so the TrustForge
1014    /// session PSK is domain-separated from anything the application
1015    /// may already derive from the same exporter.
1016    pub fn derive(transport_secret: &[u8], label: &str, context: &[u8], length: usize) -> Vec<u8> {
1017        if transport_secret.is_empty() {
1018            // Stay in lock-step with the TS bridge, which throws on empty.
1019            // We can't return BridgeError here; the TS shape uses a runtime
1020            // exception. Returning an empty vec would be misleading, so we
1021            // panic — same effect as a thrown exception, same surface as
1022            // `expect("non-empty")` elsewhere in this crate.
1023            panic!("ExporterBinding::derive: transport_secret must be non-empty");
1024        }
1025        let salt_str = format!("tf-tls-exporter:{}", label);
1026        let salt = salt_str.as_bytes();
1027
1028        // prk1 = HMAC-SHA256(salt, ikm)
1029        use hmac::{Hmac, Mac};
1030        type HmacSha256 = Hmac<sha2::Sha256>;
1031        let mut mac = HmacSha256::new_from_slice(salt).expect("hmac key");
1032        mac.update(transport_secret);
1033        mac.update(context);
1034        let prk1 = mac.finalize().into_bytes();
1035
1036        // okm = HKDF(prk1, salt=undefined → zero, info=salt, length)
1037        // i.e. extract zeros over prk1, then expand with info=salt.
1038        let hk = hkdf::Hkdf::<sha2::Sha256>::new(None, &prk1);
1039        let mut out = vec![0u8; length];
1040        hk.expand(salt, &mut out).expect("HKDF expand");
1041        out
1042    }
1043}
1044
1045// ----- Post-handshake re-auth ------------------------------------------------
1046
1047pub struct PostHandshakeReauth;
1048
1049impl PostHandshakeReauth {
1050    /// Returns 32 random challenge bytes the verifier sends to the peer.
1051    pub fn challenge() -> Vec<u8> {
1052        use rand::RngCore;
1053        let mut buf = vec![0u8; 32];
1054        rand::rngs::OsRng.fill_bytes(&mut buf);
1055        buf
1056    }
1057
1058    /// Verifies an Ed25519 signature over the previously issued challenge.
1059    /// Returns `true` iff the signature is valid.
1060    pub fn verify_response(challenge: &[u8], pubkey: &[u8; 32], signature: &[u8; 64]) -> bool {
1061        crate::crypto::ed25519_verify(pubkey, challenge, signature).is_ok()
1062    }
1063}