Skip to content

Commit 219615c

Browse files
committed
feat: implement Trust On First Use (TOFU) for TLS connections
- Added using to persist server certificates - Integrated TOFU verification in for both OpenSSL and Rustls backends when domain validation is disabled - Implemented for Rustls to handle custom certificate validation logic - Added and variants to the enum. - Added dependency in
1 parent 5d7be37 commit 219615c

File tree

5 files changed

+197
-5
lines changed

5 files changed

+197
-5
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ rustls = { version = "0.23.21", optional = true, default-features = false }
3030
webpki-roots = { version = "0.25", optional = true }
3131

3232
byteorder = { version = "1.0", optional = true }
33+
dirs = "6.0.0"
34+
rusqlite = "0.38.0"
3335

3436
[target.'cfg(unix)'.dependencies]
3537
libc = { version = "0.2", optional = true }

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,5 @@ pub use batch::Batch;
8181
pub use client::*;
8282
pub use config::{Config, ConfigBuilder, Socks5Config};
8383
pub use types::*;
84+
85+
mod tofu;

src/raw_client.rs

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,10 +285,40 @@ impl RawClient<ElectrumSslStream> {
285285
.connect(&domain, stream)
286286
.map_err(Error::SslHandshakeError)?;
287287

288+
if !validate_domain {
289+
let store = TofuData::setup()
290+
.map_err(|e| Error::TofuPersistError(e.to_string()))?;
291+
292+
if let Some(peer_cert) = stream.ssl().peer_certificate() {
293+
let der = peer_cert.to_der()
294+
.map_err(|e| Error::TofuPersistError(e.to_string()))?;
295+
296+
match store.getData(&domain).map_err(|e| Error::TofuPersistError(e.to_string()))? {
297+
Some(saved_der) => {
298+
if saved_der != der {
299+
return Err(Error::TlsCertificateChanged(domain));
300+
}
301+
}
302+
None => {
303+
// first time: persist certificate
304+
store
305+
.setData(&domain, der)
306+
.map_err(|e| Error::TofuPersistError(e.to_string()))?;
307+
}
308+
}
309+
} else {
310+
return Err(Error::TofuPersistError(
311+
"Peer Certificate not available".to_string(),
312+
));
313+
}
314+
}
315+
288316
Ok(stream.into())
289317
}
290318
}
291319

320+
321+
292322
#[cfg(all(
293323
any(
294324
feature = "default",
@@ -299,10 +329,12 @@ impl RawClient<ElectrumSslStream> {
299329
))]
300330
mod danger {
301331
use crate::raw_client::ServerName;
332+
use crate::tofu::TofuData;
302333
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
303334
use rustls::crypto::CryptoProvider;
304335
use rustls::pki_types::{CertificateDer, UnixTime};
305336
use rustls::DigitallySignedStruct;
337+
use std::sync::{Arc, Mutex};
306338

307339
#[derive(Debug)]
308340
pub struct NoCertificateVerification(CryptoProvider);
@@ -347,6 +379,94 @@ mod danger {
347379
self.0.signature_verification_algorithms.supported_schemes()
348380
}
349381
}
382+
383+
#[derive(Debug)]
384+
pub struct TofuVerifier {
385+
provider: CryptoProvider,
386+
host: String,
387+
tofu_store: Arc<Mutex<Option<Arc<TofuData>>>>,
388+
}
389+
390+
impl TofuVerifier {
391+
pub fn new(provider: CryptoProvider, host: String) -> Self {
392+
Self {
393+
provider,
394+
host,
395+
tofu_store: Arc::new(Mutex::new(None)),
396+
}
397+
}
398+
399+
fn get_tofu_store(&self) -> Result<Arc<TofuData>, crate::Error> {
400+
let mut store = self.tofu_store.lock().unwrap();
401+
if store.is_none() {
402+
*store = Some(Arc::new(
403+
TofuData::setup()
404+
.map_err(|e| crate::Error::TofuPersistError(e.to_string()))?,
405+
));
406+
}
407+
Ok(store.as_ref().unwrap().clone())
408+
}
409+
410+
fn verify_tofu(&self, cert_der: &[u8]) -> Result<(), crate::Error> {
411+
let store = self.get_tofu_store()?;
412+
match store
413+
.getData(&self.host)
414+
.map_err(|e| crate::Error::TofuPersistError(e.to_string()))?
415+
{
416+
Some(saved_der) => {
417+
if saved_der != cert_der {
418+
return Err(crate::Error::TlsCertificateChanged(self.host.clone()));
419+
}
420+
}
421+
None => {
422+
// First time: persist certificate.
423+
store
424+
.setData(&self.host, cert_der.to_vec())
425+
.map_err(|e| crate::Error::TofuPersistError(e.to_string()))?;
426+
}
427+
}
428+
429+
Ok(())
430+
}
431+
}
432+
433+
impl rustls::client::danger::ServerCertVerifier for TofuVerifier {
434+
fn verify_server_cert(
435+
&self,
436+
end_entity: &CertificateDer<'_>,
437+
_intermediates: &[CertificateDer<'_>],
438+
_server_name: &ServerName<'_>,
439+
_ocsp: &[u8],
440+
_now: UnixTime,
441+
) -> Result<ServerCertVerified, rustls::Error> {
442+
// Verify using TOFU
443+
self.verify_tofu(end_entity.as_ref())
444+
.map_err(|e| rustls::Error::General(format!("{:?}", e)))?;
445+
Ok(ServerCertVerified::assertion())
446+
}
447+
448+
fn verify_tls12_signature(
449+
&self,
450+
_message: &[u8],
451+
_cert: &CertificateDer<'_>,
452+
_dss: &DigitallySignedStruct,
453+
) -> Result<HandshakeSignatureValid, rustls::Error> {
454+
Ok(HandshakeSignatureValid::assertion())
455+
}
456+
457+
fn verify_tls13_signature(
458+
&self,
459+
_message: &[u8],
460+
_cert: &CertificateDer<'_>,
461+
_dss: &DigitallySignedStruct,
462+
) -> Result<HandshakeSignatureValid, rustls::Error> {
463+
Ok(HandshakeSignatureValid::assertion())
464+
}
465+
466+
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
467+
self.provider.signature_verification_algorithms.supported_schemes()
468+
}
469+
}
350470
}
351471

352472
#[cfg(all(
@@ -381,6 +501,7 @@ impl RawClient<ElectrumSslStream> {
381501
validate_domain,
382502
timeout
383503
);
504+
384505
if validate_domain {
385506
socket_addrs.domain().ok_or(Error::MissingDomain)?;
386507
}
@@ -431,6 +552,7 @@ impl RawClient<ElectrumSslStream> {
431552

432553
let builder = ClientConfig::builder();
433554

555+
let domain = socket_addr.domain().unwrap_or("NONE").to_string();
434556
let config = if validate_domain {
435557
socket_addr.domain().ok_or(Error::MissingDomain)?;
436558

@@ -450,20 +572,28 @@ impl RawClient<ElectrumSslStream> {
450572
.dangerous()
451573
.with_custom_certificate_verifier(std::sync::Arc::new(
452574
#[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))]
453-
danger::NoCertificateVerification::new(rustls::crypto::aws_lc_rs::default_provider()),
575+
danger::TofuVerifier::new(rustls::crypto::aws_lc_rs::default_provider(), domain.clone()),
454576
#[cfg(feature = "use-rustls-ring")]
455-
danger::NoCertificateVerification::new(rustls::crypto::ring::default_provider()),
577+
danger::TofuVerifier::new(rustls::crypto::ring::default_provider(), domain.clone()),
456578
))
457579
.with_no_client_auth()
458580
};
459581

460-
let domain = socket_addr.domain().unwrap_or("NONE").to_string();
461582
let session = ClientConnection::new(
462583
std::sync::Arc::new(config),
463584
ServerName::try_from(domain.clone())
464585
.map_err(|_| Error::InvalidDNSNameError(domain.clone()))?,
465586
)
466-
.map_err(Error::CouldNotCreateConnection)?;
587+
.map_err(|e| {
588+
let error_msg = format!("{}", e);
589+
if error_msg.contains("TLS certificate changed") {
590+
Error::TlsCertificateChanged(domain.clone())
591+
} else if error_msg.contains("TOFU") {
592+
Error::TofuPersistError(error_msg)
593+
} else {
594+
Error::CouldNotCreateConnection(e)
595+
}
596+
})?;
467597
let stream = StreamOwned::new(session, tcp_stream);
468598

469599
Ok(stream.into())

src/tofu/mod.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::{fs::create_dir_all, path::PathBuf, sync::Mutex};
2+
use rusqlite::{Connection, OptionalExtension, Result as SqlResult, params};
3+
4+
5+
#[derive(Debug)]
6+
pub struct TofuData {
7+
db_path: PathBuf,
8+
connection: Mutex<Connection>
9+
}
10+
11+
12+
13+
impl TofuData {
14+
pub fn setData(&self, host: &str, cert: Vec<u8>) -> SqlResult<()> {
15+
let connection = self.connection.lock().unwrap();
16+
let sql = "INSERT INTO tofu (host, cert) VALUES (?1, ?2)
17+
ON CONFLICT(host) DO UPDATE SET cert = excluded.cert";
18+
19+
connection.execute(sql, params![host, cert])?;
20+
21+
Ok(())
22+
}
23+
24+
pub fn getData(&self, host: &str) -> SqlResult<Option<Vec<u8>>> {
25+
let connection = self.connection.lock().unwrap();
26+
let sql = "SELECT cert FROM tofu WHERE host = ?1";
27+
28+
connection
29+
.query_row(sql, params![host], |row| row.get(0))
30+
.optional()
31+
}
32+
33+
pub fn setup() -> SqlResult<Self> {
34+
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
35+
path.push(".electrum-client");
36+
create_dir_all(&path).ok();
37+
path.push("tofu.sqlite");
38+
39+
let connection = Connection::open(&path)?;
40+
let sql = "CREATE TABLE IF NOT EXISTS tofu(
41+
host TEXT PRIMARY KEY,
42+
cert BLOB NOT NULL
43+
)";
44+
45+
connection.execute(sql, [])?;
46+
47+
Ok(TofuData {
48+
db_path: path,
49+
connection: Mutex::new(connection)
50+
})
51+
}
52+
}

src/types.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,11 @@ pub enum Error {
320320
AllAttemptsErrored(Vec<Error>),
321321
/// There was an io error reading the socket, to be shared between threads
322322
SharedIOError(Arc<std::io::Error>),
323-
323+
/// Certificate presented by server changed vs saved TOFU value
324+
TlsCertificateChanged(String),
325+
/// Could not persist TOFU store
326+
TofuPersistError(String),
327+
324328
/// Couldn't take a lock on the reader mutex. This means that there's already another reader
325329
/// thread running
326330
CouldntLockReader,
@@ -376,6 +380,8 @@ impl Display for Error {
376380
Error::MissingDomain => f.write_str("Missing domain while it was explicitly asked to validate it"),
377381
Error::CouldntLockReader => f.write_str("Couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"),
378382
Error::Mpsc => f.write_str("Broken IPC communication channel: the other thread probably has exited"),
383+
Error::TlsCertificateChanged(domain) => write!(f, "TLS certificate changed for host: {}", domain),
384+
Error::TofuPersistError(msg) => write!(f, "TOFU persistence error: {}", msg),
379385
}
380386
}
381387
}

0 commit comments

Comments
 (0)