Skip to content

Commit e352efe

Browse files
committed
feat: custom TLS certificate verification
1 parent 942172a commit e352efe

File tree

11 files changed

+289
-29
lines changed

11 files changed

+289
-29
lines changed

src/context.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::key::self_fingerprint;
2525
use crate::log::warn;
2626
use crate::logged_debug_assert;
2727
use crate::message::{self, MessageState, MsgId};
28-
use crate::net::tls::TlsSessionStore;
28+
use crate::net::tls::{SpkiHashStore, TlsSessionStore};
2929
use crate::peer_channels::Iroh;
3030
use crate::push::PushSubscriber;
3131
use crate::quota::QuotaInfo;
@@ -308,6 +308,13 @@ pub struct InnerContext {
308308
/// TLS session resumption cache.
309309
pub(crate) tls_session_store: TlsSessionStore,
310310

311+
/// Store for TLS SPKI hashes.
312+
///
313+
/// Used to remember public keys
314+
/// of TLS certificates to accept them
315+
/// even after they expire.
316+
pub(crate) spki_hash_store: SpkiHashStore,
317+
311318
/// Iroh for realtime peer channels.
312319
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
313320

@@ -511,6 +518,7 @@ impl Context {
511518
push_subscriber,
512519
push_subscribed: AtomicBool::new(false),
513520
tls_session_store: TlsSessionStore::new(),
521+
spki_hash_store: SpkiHashStore::new(),
514522
iroh: Arc::new(RwLock::new(None)),
515523
self_fingerprint: OnceLock::new(),
516524
self_public_key: Mutex::new(None),

src/imap/client.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ impl Client {
220220
alpn(addr.port()),
221221
logging_stream,
222222
&context.tls_session_store,
223+
&context.spki_hash_store,
224+
&context.sql,
223225
)
224226
.await?;
225227
let buffered_stream = BufWriter::new(tls_stream);
@@ -282,6 +284,8 @@ impl Client {
282284
"",
283285
tcp_stream,
284286
&context.tls_session_store,
287+
&context.spki_hash_store,
288+
&context.sql,
285289
)
286290
.await
287291
.context("STARTTLS upgrade failed")?;
@@ -310,6 +314,8 @@ impl Client {
310314
alpn(port),
311315
proxy_stream,
312316
&context.tls_session_store,
317+
&context.spki_hash_store,
318+
&context.sql,
313319
)
314320
.await?;
315321
let buffered_stream = BufWriter::new(tls_stream);
@@ -373,6 +379,8 @@ impl Client {
373379
"",
374380
proxy_stream,
375381
&context.tls_session_store,
382+
&context.spki_hash_store,
383+
&context.sql,
376384
)
377385
.await
378386
.context("STARTTLS upgrade failed")?;

src/net.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tokio_io_timeout::TimeoutStream;
1212

1313
use crate::context::Context;
1414
use crate::net::session::SessionStream;
15-
use crate::net::tls::TlsSessionStore;
15+
use crate::net::tls::{SpkiHashStore, TlsSessionStore};
1616
use crate::sql::Sql;
1717
use crate::tools::time;
1818

@@ -130,6 +130,8 @@ pub(crate) async fn connect_tls_inner(
130130
strict_tls: bool,
131131
alpn: &str,
132132
tls_session_store: &TlsSessionStore,
133+
spki_hash_store: &SpkiHashStore,
134+
sql: &Sql,
133135
) -> Result<impl SessionStream + 'static> {
134136
let use_sni = true;
135137
let tcp_stream = connect_tcp_inner(addr).await?;
@@ -141,6 +143,8 @@ pub(crate) async fn connect_tls_inner(
141143
alpn,
142144
tcp_stream,
143145
tls_session_store,
146+
spki_hash_store,
147+
sql,
144148
)
145149
.await?;
146150
Ok(tls_stream)

src/net/http.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ where
8787
"",
8888
proxy_stream,
8989
&context.tls_session_store,
90+
&context.spki_hash_store,
91+
&context.sql,
9092
)
9193
.await?;
9294
Box::new(tls_stream)
@@ -99,6 +101,8 @@ where
99101
"",
100102
tcp_stream,
101103
&context.tls_session_store,
104+
&context.spki_hash_store,
105+
&context.sql,
102106
)
103107
.await?;
104108
Box::new(tls_stream)

src/net/proxy.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,8 @@ impl ProxyConfig {
436436
"",
437437
tcp_stream,
438438
&context.tls_session_store,
439+
&context.spki_hash_store,
440+
&context.sql,
439441
)
440442
.await?;
441443
let auth = if let Some((username, password)) = &https_config.user_password {

src/net/tls.rs

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@ use std::sync::Arc;
66
use anyhow::Result;
77

88
use crate::net::session::SessionStream;
9+
use crate::sql::Sql;
10+
use crate::tools::time;
911

1012
use tokio_rustls::rustls;
1113
use tokio_rustls::rustls::client::ClientSessionStore;
14+
use tokio_rustls::rustls::server::ParsedCertificate;
1215

1316
mod danger;
14-
use danger::NoCertificateVerification;
17+
use danger::CustomCertificateVerifier;
1518

19+
mod spki;
20+
pub use spki::SpkiHashStore;
21+
22+
#[expect(clippy::too_many_arguments)]
1623
pub async fn wrap_tls<'a>(
1724
strict_tls: bool,
1825
hostname: &str,
@@ -21,10 +28,21 @@ pub async fn wrap_tls<'a>(
2128
alpn: &str,
2229
stream: impl SessionStream + 'static,
2330
tls_session_store: &TlsSessionStore,
31+
spki_hash_store: &SpkiHashStore,
32+
sql: &Sql,
2433
) -> Result<impl SessionStream + 'a> {
2534
if strict_tls {
26-
let tls_stream =
27-
wrap_rustls(hostname, port, use_sni, alpn, stream, tls_session_store).await?;
35+
let tls_stream = wrap_rustls(
36+
hostname,
37+
port,
38+
use_sni,
39+
alpn,
40+
stream,
41+
tls_session_store,
42+
spki_hash_store,
43+
sql,
44+
)
45+
.await?;
2846
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
2947
Ok(boxed_stream)
3048
} else {
@@ -94,13 +112,16 @@ impl TlsSessionStore {
94112
}
95113
}
96114

115+
#[expect(clippy::too_many_arguments)]
97116
pub async fn wrap_rustls<'a>(
98117
hostname: &str,
99118
port: u16,
100119
use_sni: bool,
101120
alpn: &str,
102121
stream: impl SessionStream + 'a,
103122
tls_session_store: &TlsSessionStore,
123+
spki_hash_store: &SpkiHashStore,
124+
sql: &Sql,
104125
) -> Result<impl SessionStream + 'a> {
105126
let mut root_cert_store = rustls::RootCertStore::empty();
106127
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
@@ -127,20 +148,28 @@ pub async fn wrap_rustls<'a>(
127148
config.resumption = resumption;
128149
config.enable_sni = use_sni;
129150

130-
// Do not verify certificates for hostnames starting with `_`.
131-
// They are used for servers with self-signed certificates, e.g. for local testing.
132-
// Hostnames starting with `_` can have only self-signed TLS certificates or wildcard certificates.
133-
// It is not possible to get valid non-wildcard TLS certificates because CA/Browser Forum requirements
134-
// explicitly state that domains should start with a letter, digit or hyphen:
135-
// https://github.com/cabforum/servercert/blob/24f38fd4765e019db8bb1a8c56bf63c7115ce0b0/docs/BR.md
136-
if hostname.starts_with("_") {
137-
config
138-
.dangerous()
139-
.set_certificate_verifier(Arc::new(NoCertificateVerification::new()));
140-
}
151+
config
152+
.dangerous()
153+
.set_certificate_verifier(Arc::new(CustomCertificateVerifier::new(
154+
spki_hash_store.get_spki_hash(hostname, sql).await?,
155+
)));
141156

142157
let tls = tokio_rustls::TlsConnector::from(Arc::new(config));
143158
let name = tokio_rustls::rustls::pki_types::ServerName::try_from(hostname)?.to_owned();
144159
let tls_stream = tls.connect(name, stream).await?;
160+
161+
// Successfully connected.
162+
// Remember SPKI hash to accept it later if certificate expires.
163+
let (_io, client_connection) = tls_stream.get_ref();
164+
if let Some(end_entity) = client_connection
165+
.peer_certificates()
166+
.and_then(|certs| certs.first())
167+
{
168+
let now = time();
169+
let parsed_certificate = ParsedCertificate::try_from(end_entity)?;
170+
let spki = parsed_certificate.subject_public_key_info();
171+
spki_hash_store.save_spki(hostname, &spki, sql, now).await?;
172+
}
173+
145174
Ok(tls_stream)
146175
}

src/net/tls/danger.rs

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,85 @@
1-
//! Dangerous TLS implementation of accepting invalid certificates for Rustls.
1+
//! Custom TLS verification.
2+
//!
3+
//! We want to accept expired certificates.
24
5+
use rustls::RootCertStore;
6+
use rustls::client::{verify_server_cert_signed_by_trust_anchor, verify_server_name};
37
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
8+
use rustls::server::ParsedCertificate;
49
use tokio_rustls::rustls;
510

11+
use crate::net::tls::spki::spki_hash;
12+
613
#[derive(Debug)]
7-
pub(super) struct NoCertificateVerification();
14+
pub(super) struct CustomCertificateVerifier {
15+
/// Root certificates.
16+
root_cert_store: RootCertStore,
17+
18+
/// Expected SPKI hash as a base64 of SHA-256.
19+
spki_hash: Option<String>,
20+
}
821

9-
impl NoCertificateVerification {
10-
pub(super) fn new() -> Self {
11-
Self()
22+
impl CustomCertificateVerifier {
23+
pub(super) fn new(spki_hash: Option<String>) -> Self {
24+
let root_cert_store =
25+
RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
26+
Self {
27+
root_cert_store,
28+
spki_hash,
29+
}
1230
}
1331
}
1432

15-
impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
33+
impl rustls::client::danger::ServerCertVerifier for CustomCertificateVerifier {
1634
fn verify_server_cert(
1735
&self,
18-
_end_entity: &CertificateDer<'_>,
19-
_intermediates: &[CertificateDer<'_>],
20-
_server_name: &ServerName<'_>,
36+
end_entity: &CertificateDer<'_>,
37+
intermediates: &[CertificateDer<'_>],
38+
server_name: &ServerName<'_>,
2139
_ocsp_response: &[u8],
22-
_now: UnixTime,
40+
now: UnixTime,
2341
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
42+
let parsed_certificate = ParsedCertificate::try_from(end_entity)?;
43+
44+
let spki = parsed_certificate.subject_public_key_info();
45+
46+
let provider = rustls::crypto::ring::default_provider();
47+
48+
if let ServerName::DnsName(dns_name) = server_name
49+
&& dns_name.as_ref().starts_with("_")
50+
{
51+
// Do not verify certificates for hostnames starting with `_`.
52+
// They are used for servers with self-signed certificates, e.g. for local testing.
53+
// Hostnames starting with `_` can have only self-signed TLS certificates or wildcard certificates.
54+
// It is not possible to get valid non-wildcard TLS certificates because CA/Browser Forum requirements
55+
// explicitly state that domains should start with a letter, digit or hyphen:
56+
// https://github.com/cabforum/servercert/blob/24f38fd4765e019db8bb1a8c56bf63c7115ce0b0/docs/BR.md
57+
} else if let Some(hash) = &self.spki_hash
58+
&& spki_hash(&spki) == *hash
59+
{
60+
// Last time we successfully connected to this hostname with TLS checks,
61+
// SPKI had this hash.
62+
// It does not matter if certificate has now expired.
63+
} else {
64+
// verify_server_cert_signed_by_trust_anchor does no revocation checking:
65+
// <https://docs.rs/rustls/0.23.37/rustls/client/fn.verify_server_cert_signed_by_trust_anchor.html>
66+
// We don't do it either.
67+
verify_server_cert_signed_by_trust_anchor(
68+
&parsed_certificate,
69+
&self.root_cert_store,
70+
intermediates,
71+
now,
72+
provider.signature_verification_algorithms.all,
73+
)?;
74+
}
75+
76+
// Verify server name unconditionally.
77+
//
78+
// We do this even for self-signed certificates when hostname starts with `_`
79+
// so we don't try to connect to captive portals
80+
// and fail on MITM certificates if they are generated once
81+
// and reused for all hostnames.
82+
verify_server_name(&parsed_certificate, server_name)?;
2483
Ok(rustls::client::danger::ServerCertVerified::assertion())
2584
}
2685

0 commit comments

Comments
 (0)