pub mod known_hosts;
use std::sync::{Arc, Mutex};
use der::{Reader, SliceReader, Tag, TagNumber};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::crypto::{CryptoProvider, ring, verify_tls13_signature_with_raw_key};
use rustls::pki_types::{CertificateDer, ServerName, SubjectPublicKeyInfoDer, UnixTime};
use rustls::{DigitallySignedStruct, Error as RustlsError, SignatureScheme};
use sha2::{Digest, Sha256};
use crate::error::{Result, UgosError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CertFingerprint([u8; 32]);
impl CertFingerprint {
#[must_use]
pub fn of(der: &[u8]) -> Self {
let digest = Sha256::digest(der);
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&digest);
Self(bytes)
}
#[must_use]
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
pub fn from_hex(input: &str) -> Result<Self> {
let decoded = hex::decode(input)
.map_err(|e| UgosError::Encryption(format!("bad fingerprint: {e}")))?;
let bytes: [u8; 32] = decoded
.try_into()
.map_err(|_| UgosError::Encryption("fingerprint must be 32 bytes".into()))?;
Ok(Self(bytes))
}
}
impl std::fmt::Display for CertFingerprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let hex = hex::encode(self.0);
let pairs: Vec<&str> = (0..hex.len()).step_by(2).map(|i| &hex[i..i + 2]).collect();
write!(f, "{}", pairs.join(":"))
}
}
#[derive(Debug, Clone)]
pub enum TlsPolicy {
Insecure,
Pinned(CertFingerprint),
}
#[derive(Debug)]
struct PinnedVerifier {
expected: CertFingerprint,
provider: Arc<CryptoProvider>,
}
#[derive(Debug)]
struct LearningVerifier {
seen: Arc<Mutex<Option<CertFingerprint>>>,
provider: Arc<CryptoProvider>,
}
fn extract_spki(cert_der: &[u8]) -> Result<Vec<u8>> {
fn parse(cert_der: &[u8]) -> std::result::Result<Vec<u8>, der::Error> {
let mut outer = SliceReader::new(cert_der)?;
outer.sequence(|certificate| {
let spki = certificate.sequence(|tbs| {
let version_tag = Tag::ContextSpecific {
constructed: true,
number: TagNumber::N0,
};
if tbs.peek_tag()? == version_tag {
let _version = tbs.tlv_bytes()?;
}
let _serial = tbs.tlv_bytes()?;
let _signature_alg = tbs.tlv_bytes()?;
let _issuer = tbs.tlv_bytes()?;
let _validity = tbs.tlv_bytes()?;
let _subject = tbs.tlv_bytes()?;
let spki = tbs.tlv_bytes()?.to_vec();
while !tbs.is_finished() {
let _rest = tbs.tlv_bytes()?;
}
Ok(spki)
})?;
while !certificate.is_finished() {
let _rest = certificate.tlv_bytes()?;
}
Ok(spki)
})
}
parse(cert_der)
.map_err(|e| UgosError::Encryption(format!("cannot read certificate public key: {e}")))
}
fn verify_with_cert_key(
cert: &CertificateDer<'_>,
message: &[u8],
dss: &DigitallySignedStruct,
provider: &CryptoProvider,
) -> std::result::Result<HandshakeSignatureValid, RustlsError> {
let spki = extract_spki(cert.as_ref()).map_err(|e| RustlsError::General(format!("{e}")))?;
verify_tls13_signature_with_raw_key(
message,
&SubjectPublicKeyInfoDer::from(spki.as_slice()),
dss,
&provider.signature_verification_algorithms,
)
}
fn tls12_unsupported() -> RustlsError {
RustlsError::General(
"TLS 1.2 is not supported against a version 1 certificate; the client offers TLS 1.3 only"
.into(),
)
}
impl ServerCertVerifier for PinnedVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> std::result::Result<ServerCertVerified, RustlsError> {
let actual = CertFingerprint::of(end_entity.as_ref());
if actual == self.expected {
Ok(ServerCertVerified::assertion())
} else {
Err(RustlsError::General(format!(
"certificate fingerprint mismatch: expected {}, got {actual}",
self.expected
)))
}
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, RustlsError> {
Err(tls12_unsupported())
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, RustlsError> {
let actual = CertFingerprint::of(cert.as_ref());
if actual != self.expected {
return Err(RustlsError::General(
"certificate changed mid-handshake".into(),
));
}
verify_with_cert_key(cert, message, dss, &self.provider)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.provider
.signature_verification_algorithms
.supported_schemes()
}
}
impl ServerCertVerifier for LearningVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> std::result::Result<ServerCertVerified, RustlsError> {
{
let mut slot = self
.seen
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*slot = Some(CertFingerprint::of(end_entity.as_ref()));
}
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, RustlsError> {
Err(tls12_unsupported())
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, RustlsError> {
verify_with_cert_key(cert, message, dss, &self.provider)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.provider
.signature_verification_algorithms
.supported_schemes()
}
}
fn config_with(verifier: Arc<dyn ServerCertVerifier>) -> Result<rustls::ClientConfig> {
let config = rustls::ClientConfig::builder_with_provider(Arc::new(ring::default_provider()))
.with_protocol_versions(&[&rustls::version::TLS13])
.map_err(|e| UgosError::Encryption(format!("TLS 1.3 unavailable: {e}")))?
.dangerous()
.with_custom_certificate_verifier(verifier)
.with_no_client_auth();
Ok(config)
}
pub fn http_client(policy: &TlsPolicy) -> Result<reqwest::Client> {
let builder = reqwest::Client::builder()
.cookie_store(true)
.timeout(std::time::Duration::from_secs(30));
let builder = match policy {
TlsPolicy::Insecure => builder.danger_accept_invalid_certs(true),
TlsPolicy::Pinned(expected) => {
let verifier = Arc::new(PinnedVerifier {
expected: *expected,
provider: Arc::new(ring::default_provider()),
});
builder.use_preconfigured_tls(config_with(verifier)?)
}
};
builder
.build()
.map_err(|e| UgosError::Encryption(format!("HTTP client build: {e}")))
}
pub async fn probe_fingerprint(host: &str, port: u16) -> Result<CertFingerprint> {
let seen = Arc::new(Mutex::new(None));
let verifier = Arc::new(LearningVerifier {
seen: Arc::clone(&seen),
provider: Arc::new(ring::default_provider()),
});
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.use_preconfigured_tls(config_with(verifier)?)
.build()
.map_err(|e| UgosError::Encryption(format!("HTTP client build: {e}")))?;
let _ = client.get(format!("https://{host}:{port}/")).send().await?;
let observed = seen
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
observed.ok_or_else(|| UgosError::Encryption("no certificate seen during handshake".into()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_round_trips_through_hex() {
let fp = CertFingerprint::of(b"some certificate bytes");
let parsed = CertFingerprint::from_hex(&fp.to_hex());
assert_eq!(parsed.ok(), Some(fp));
}
#[test]
fn hex_form_is_64_lowercase_digits() {
let hex = CertFingerprint::of(b"x").to_hex();
assert_eq!(hex.len(), 64);
assert!(
hex.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
);
}
#[test]
fn display_form_is_colon_separated() {
let shown = CertFingerprint::of(b"x").to_string();
assert_eq!(shown.matches(':').count(), 31);
assert_eq!(shown.len(), 95);
}
#[test]
fn differing_certificates_differ() {
assert_ne!(CertFingerprint::of(b"a"), CertFingerprint::of(b"b"));
}
#[test]
fn from_hex_rejects_wrong_length() {
assert!(CertFingerprint::from_hex("abcd").is_err());
}
#[test]
fn from_hex_rejects_non_hex() {
assert!(CertFingerprint::from_hex(&"z".repeat(64)).is_err());
}
const V1_CERT: &str = "302d30250201013000300030003000301830032a8648031100ababababababab ababababababababab3000030200ff";
const V3_CERT: &str = "3036302ea0030201020201013000300030003000301830032a8648031100abab ababababababababababababababa30230003000030200ff";
const SPKI: &str = "301830032a8648031100abababababababababababababababab";
fn der(hex_str: &str) -> Vec<u8> {
hex::decode(hex_str.replace(' ', "")).unwrap_or_default()
}
#[test]
fn extracts_key_from_a_version_1_certificate() {
let spki = extract_spki(&der(V1_CERT));
assert_eq!(spki.map(hex::encode).ok(), Some(SPKI.to_string()));
}
#[test]
fn extracts_key_from_a_version_3_certificate() {
let spki = extract_spki(&der(V3_CERT));
assert_eq!(spki.map(hex::encode).ok(), Some(SPKI.to_string()));
}
#[test]
fn both_versions_yield_the_same_key() {
assert_eq!(
extract_spki(&der(V1_CERT)).ok(),
extract_spki(&der(V3_CERT)).ok()
);
}
#[test]
fn rejects_input_that_is_not_a_certificate() {
assert!(extract_spki(b"not der at all").is_err());
assert!(extract_spki(&[]).is_err());
assert!(extract_spki(&der("3000")).is_err());
}
#[test]
fn rejects_a_truncated_certificate() {
let full = der(V1_CERT);
assert!(extract_spki(&full[..full.len() / 2]).is_err());
}
}