Skip to content

Commit 4004eed

Browse files
committed
refactor: convert TOFU to trait-based architecture
Remove concrete SQLite implementation and replace with TofuStore trait to allow flexible certificate storage backends. This provides better flexibility for users to implement custom certificate storage. This change: - Removes rusqlite dependency - Create a trait with get_certificate/set_certificate methods - Updates `raw_client.rs` to accept TofuStore implementations - Adds new_ssl_with_tofu constructors for SSL clients implementations - Refactors TofuVerifier to use trait instead of concrete implementation - Updates TOFU tests to use in-memory implementation
1 parent 2cbc38a commit 4004eed

File tree

4 files changed

+205
-204
lines changed

4 files changed

+205
-204
lines changed

Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ 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"
3533

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

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,5 @@ pub use client::*;
8282
pub use config::{Config, ConfigBuilder, Socks5Config};
8383
pub use types::*;
8484

85-
mod tofu;
85+
mod tofu;
86+
pub use tofu::TofuStore;

src/raw_client.rs

Lines changed: 153 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use std::sync::atomic::{AtomicUsize, Ordering};
1111
use std::sync::mpsc::{channel, Receiver, Sender};
1212
use std::sync::{Arc, Mutex, TryLockError};
1313
use std::time::Duration;
14+
use std::convert::TryFrom;
15+
1416

1517
#[allow(unused_imports)]
1618
use log::{debug, error, info, trace, warn};
@@ -34,6 +36,7 @@ use rustls::{
3436
pki_types::ServerName,
3537
pki_types::{Der, TrustAnchor},
3638
ClientConfig, ClientConnection, RootCertStore, StreamOwned,
39+
crypto::CryptoProvider
3740
};
3841

3942
#[cfg(any(feature = "default", feature = "proxy"))]
@@ -286,31 +289,58 @@ impl RawClient<ElectrumSslStream> {
286289
.map_err(Error::SslHandshakeError)?;
287290

288291
if !validate_domain {
289-
let store = TofuData::setup()
290-
.map_err(|e| Error::TofuPersistError(e.to_string()))?;
292+
return Err(Error::Message(
293+
"TOFU certificate validation requires a TofuStore implementation. \
294+
Please use a constructor that accepts a TofuStore, or enable domain validation."
295+
.to_string(),
296+
));
297+
}
291298

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()))?;
299+
Ok(stream.into())
300+
}
295301

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()))?;
302+
/// Creates a new SSL client with TOFU (Trust On First Use) certificate validation.
303+
/// This method establishes an SSL connection and verify certificates. On first connection,
304+
/// the certificate is stored. On subsequent onnections, the certificate must match the stored one.
305+
pub fn new_ssl_with_tofu<S: crate::TofuStore + 'static>(
306+
socket_addrs: &dyn ToSocketAddrsDomain,
307+
tofu_store: std::sync::Arc<S>,
308+
stream: TcpStream,
309+
) -> Result<Self, Error> {
310+
let mut builder =
311+
SslConnector::builder(SslMethod::tls()).map_err(Error::InvalidSslMethod)?;
312+
313+
builder.set_verify(SslVerifyMode::NONE);
314+
let connector = builder.build();
315+
316+
let domain = socket_addrs.domain().unwrap_or("NONE").to_string();
317+
318+
let stream = connector
319+
.connect(&domain, stream)
320+
.map_err(Error::SslHandshakeError)?;
321+
322+
if let Some(peer_cert) = stream.ssl().peer_certificate() {
323+
let der = peer_cert.to_der()
324+
.map_err(|e| Error::TofuPersistError(e.to_string()))?;
325+
326+
match tofu_store.get_certificate(&domain)
327+
.map_err(|e| Error::TofuPersistError(e.to_string()))? {
328+
Some(saved_der) => {
329+
if saved_der != der {
330+
return Err(Error::TlsCertificateChanged(domain));
307331
}
308332
}
309-
} else {
310-
return Err(Error::TofuPersistError(
311-
"Peer Certificate not available".to_string(),
312-
));
333+
None => {
334+
// first time: persist certificate
335+
tofu_store
336+
.set_certificate(&domain, der)
337+
.map_err(|e| Error::TofuPersistError(e.to_string()))?;
338+
}
313339
}
340+
} else {
341+
return Err(Error::TofuPersistError(
342+
"Peer Certificate not available".to_string(),
343+
));
314344
}
315345

316346
Ok(stream.into())
@@ -329,12 +359,13 @@ impl RawClient<ElectrumSslStream> {
329359
))]
330360
mod danger {
331361
use crate::raw_client::ServerName;
332-
use crate::tofu::TofuData;
362+
use crate::TofuStore;
333363
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
334364
use rustls::crypto::CryptoProvider;
335365
use rustls::pki_types::{CertificateDer, UnixTime};
336366
use rustls::DigitallySignedStruct;
337-
use std::sync::{Arc, Mutex};
367+
use rustls::client::danger::ServerCertVerifier;
368+
use std::sync::Arc;
338369

339370
#[derive(Debug)]
340371
pub struct NoCertificateVerification(CryptoProvider);
@@ -380,37 +411,30 @@ mod danger {
380411
}
381412
}
382413

414+
/// A certificate verifier that uses TOFU (Trust On First Use) validation.
383415
#[derive(Debug)]
384416
pub struct TofuVerifier {
385417
provider: CryptoProvider,
386418
host: String,
387-
tofu_store: Arc<Mutex<Option<Arc<TofuData>>>>,
419+
tofu_store: Arc<dyn TofuStore>,
388420
}
389421

390422
impl TofuVerifier {
391-
pub fn new(provider: CryptoProvider, host: String) -> Self {
423+
pub fn new<S: TofuStore + 'static>(
424+
provider: CryptoProvider,
425+
host: String,
426+
tofu_store: Arc<S>,
427+
) -> Self {
392428
Self {
393429
provider,
394430
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-
));
431+
tofu_store,
406432
}
407-
Ok(store.as_ref().unwrap().clone())
408433
}
409434

410435
fn verify_tofu(&self, cert_der: &[u8]) -> Result<(), crate::Error> {
411-
let store = self.get_tofu_store()?;
412-
match store
413-
.getData(&self.host)
436+
match self.tofu_store
437+
.get_certificate(&self.host)
414438
.map_err(|e| crate::Error::TofuPersistError(e.to_string()))?
415439
{
416440
Some(saved_der) => {
@@ -420,8 +444,8 @@ mod danger {
420444
}
421445
None => {
422446
// First time: persist certificate.
423-
store
424-
.setData(&self.host, cert_der.to_vec())
447+
self.tofu_store
448+
.set_certificate(&self.host, cert_der.to_vec())
425449
.map_err(|e| crate::Error::TofuPersistError(e.to_string()))?;
426450
}
427451
}
@@ -430,7 +454,7 @@ mod danger {
430454
}
431455
}
432456

433-
impl rustls::client::danger::ServerCertVerifier for TofuVerifier {
457+
impl ServerCertVerifier for TofuVerifier {
434458
fn verify_server_cert(
435459
&self,
436460
end_entity: &CertificateDer<'_>,
@@ -525,8 +549,6 @@ impl RawClient<ElectrumSslStream> {
525549
validate_domain: bool,
526550
tcp_stream: TcpStream,
527551
) -> Result<Self, Error> {
528-
use std::convert::TryFrom;
529-
530552
if rustls::crypto::CryptoProvider::get_default().is_none() {
531553
// We install a crypto provider depending on the set feature.
532554
#[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))]
@@ -568,13 +590,17 @@ impl RawClient<ElectrumSslStream> {
568590
// TODO: cert pinning
569591
builder.with_root_certificates(store).with_no_client_auth()
570592
} else {
593+
// Without domain validation, we skip certificate validation entirely
594+
// For TOFU support, use new_ssl_with_tofu instead
571595
builder
572596
.dangerous()
573597
.with_custom_certificate_verifier(std::sync::Arc::new(
574-
#[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))]
575-
danger::TofuVerifier::new(rustls::crypto::aws_lc_rs::default_provider(), domain.clone()),
576-
#[cfg(feature = "use-rustls-ring")]
577-
danger::TofuVerifier::new(rustls::crypto::ring::default_provider(), domain.clone()),
598+
danger::NoCertificateVerification::new(
599+
#[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))]
600+
rustls::crypto::aws_lc_rs::default_provider(),
601+
#[cfg(feature = "use-rustls-ring")]
602+
rustls::crypto::ring::default_provider(),
603+
)
578604
))
579605
.with_no_client_auth()
580606
};
@@ -598,6 +624,85 @@ impl RawClient<ElectrumSslStream> {
598624

599625
Ok(stream.into())
600626
}
627+
628+
/// Create a new SSL client with TOFU (Trust On First Use) certificate validation.
629+
/// This method establishes an SSL connection and verify certificates. On first connection,
630+
/// the certificate is stored. On subsequent connections, the certificate must match the stored one.
631+
pub fn new_ssl_with_tofu<A: ToSocketAddrsDomain + Clone, S: crate::TofuStore + 'static>(
632+
socket_addrs: A,
633+
tofu_store: std::sync::Arc<S>,
634+
timeout: Option<Duration>,
635+
) -> Result<Self, Error> {
636+
637+
if CryptoProvider::get_default().is_none() {
638+
#[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))]
639+
CryptoProvider::install_default(
640+
rustls::crypto::aws_lc_rs::default_provider(),
641+
)
642+
.map_err(|_| {
643+
Error::CouldNotCreateConnection(rustls::Error::General(
644+
"Failed to install CryptoProvider".to_string(),
645+
))
646+
})?;
647+
648+
#[cfg(feature = "use-rustls-ring")]
649+
CryptoProvider::install_default(
650+
rustls::crypto::ring::default_provider(),
651+
)
652+
.map_err(|_| {
653+
Error::CouldNotCreateConnection(rustls::Error::General(
654+
"Failed to install CryptoProvider".to_string(),
655+
))
656+
})?;
657+
}
658+
659+
let domain = socket_addrs.domain().ok_or(Error::MissingDomain)?.to_string();
660+
661+
let tcp_stream = match timeout {
662+
Some(timeout) => {
663+
let stream = connect_with_total_timeout(socket_addrs.clone(), timeout)?;
664+
stream.set_read_timeout(Some(timeout))?;
665+
stream.set_write_timeout(Some(timeout))?;
666+
stream
667+
}
668+
None => TcpStream::connect(socket_addrs)?,
669+
};
670+
671+
let builder = rustls::ClientConfig::builder();
672+
673+
let verifier = danger::TofuVerifier::new(
674+
#[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))]
675+
rustls::crypto::aws_lc_rs::default_provider(),
676+
#[cfg(feature = "use-rustls-ring")]
677+
rustls::crypto::ring::default_provider(),
678+
domain.clone(),
679+
tofu_store,
680+
);
681+
682+
let config = builder
683+
.dangerous()
684+
.with_custom_certificate_verifier(std::sync::Arc::new(verifier))
685+
.with_no_client_auth();
686+
687+
let session = ClientConnection::new(
688+
std::sync::Arc::new(config),
689+
ServerName::try_from(domain.clone())
690+
.map_err(|_| Error::InvalidDNSNameError(domain.clone()))?,
691+
)
692+
.map_err(|e| {
693+
let error_msg = format!("{}", e);
694+
if error_msg.contains("TLS certificate changed") {
695+
Error::TlsCertificateChanged(domain.clone())
696+
} else if error_msg.contains("TOFU") {
697+
Error::TofuPersistError(error_msg)
698+
} else {
699+
Error::CouldNotCreateConnection(e)
700+
}
701+
})?;
702+
let stream = StreamOwned::new(session, tcp_stream);
703+
704+
Ok(stream.into())
705+
}
601706
}
602707

603708
#[cfg(any(feature = "default", feature = "proxy"))]

0 commit comments

Comments
 (0)