//! Trust-On-First-Use host verification. //! //! Model (SSH known_hosts style): //! - First contact with a host: record the SHA-256 fingerprint of the //! certificate's SubjectPublicKeyInfo (SPKI, not the whole certificate, so //! routine cert renewal with the same key does not break clients). //! - Later contacts must present the same key or the connection is rejected. use sha2::{Digest, Sha256}; use std::ops::Range; /// Outcome of comparing an observed fingerprint against stored trust state. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TrustDecision { /// Host never seen: accept and store `fingerprint`. TrustNew, /// Stored fingerprint matches: proceed. Trusted, /// Key changed since first use: reject with details. Mismatch { expected: String, actual: String }, } /// Hex-encoded SHA-256 of the SPKI inside a DER-encoded X.509 certificate. /// /// Fails if `cert_der` is not parseable as a certificate. pub fn spki_fingerprint(cert_der: &[u8]) -> Result { let spki = der_spki(cert_der)?; Ok(hex_encode(&Sha256::digest(&cert_der[spki]))) } /// Decide what to do with an observed fingerprint given stored state /// (`None` when the host has never been trusted). pub fn decide(stored: Option<&str>, observed: &str) -> TrustDecision { match stored { None => TrustDecision::TrustNew, Some(expected) if expected == observed => TrustDecision::Trusted, Some(expected) => TrustDecision::Mismatch { expected: expected.to_string(), actual: observed.to_string(), }, } } fn hex_encode(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() } /// Locate the complete DER encoding of the certificate's SubjectPublicKeyInfo /// (TLV including tag and length bytes — the same bytes /// `openssl pkey -pubin -outform DER` produces). fn der_spki(der: &[u8]) -> Result, String> { let mut pos = 0; // Certificate ::= SEQUENCE { tbsCertificate, ... } let (_, _cert_full, cert_body) = read_tlv(der, &mut pos)?; // TBSCertificate ::= SEQUENCE { version [0] OPTIONAL, serial, signature, // issuer, validity, subject, subjectPublicKeyInfo, ... } let mut pos = cert_body.start; let (_, _tbs_full, tbs) = read_tlv(der, &mut pos)?; let mut pos = tbs.start; // Slots before SPKI: [0] version (optional), serial, signature, let mut next = || read_tlv(der, &mut pos); let (first_tag, _, _) = next()?; let skip = if first_tag == 0xA0 { 5 } else { 4 }; for _ in 0..skip { next()?; } let (spki_tag, spki_full, _) = next()?; if spki_tag != 0x30 { return Err(format!("expected SPKI SEQUENCE, got tag {spki_tag:#x}")); } Ok(spki_full) } /// Read one DER TLV at *pos. Returns (tag, full range incl. header, /// content range) and advances pos past the element. fn read_tlv(der: &[u8], pos: &mut usize) -> Result<(u8, Range, Range), String> { let elem_start = *pos; if *pos + 2 > der.len() { return Err("truncated DER".into()); } let tag = der[*pos]; *pos += 1; let first = der[*pos]; *pos += 1; let content_len = if first < 0x80 { first as usize } else { let n = (first & 0x7F) as usize; if n == 0 || n > 4 || *pos + n > der.len() { return Err("unsupported DER length".into()); } let mut v = 0usize; for b in &der[*pos..*pos + n] { v = (v << 8) | *b as usize; } *pos += n; v }; let content_start = *pos; let end = content_start .checked_add(content_len) .ok_or("DER length overflow")?; if end > der.len() { return Err("truncated DER element".into()); } *pos = end; Ok((tag, elem_start..end, content_start..end)) }