1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
//! 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<String, String> {
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<Range<usize>, 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<usize>, Range<usize>), 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))
}
|