Skip to content

Commit 3adefc6

Browse files
committed
feat(tofu): add option for Trust On First Use validation for SSL connections
Trust On First Use (TOFU) is a security model where a client trusts a certificate upon the first connection and subsequent connections are verified against that initially stored record. This commit introduces the following features: - `TofuStore` trait to manage certificates - Implement TOFU validations both for OpenSSL and Rustls configurations - Add custom certificate verifier for Rustls - Support TOFU validation in proxy SSL connections - Extend enum with error variants specifics to TOFU - New method to initialize a client with TOFU from config - Update existing constructors to support the revised SSL backend signatures - Add some unit tests related to TOFU. The tests cover the following cases: first-use storage and certificate matching/replacement - Add a TOFU certificates validation example, based on a in-memory store implementation for demonstration purposes NOTE: Unit tests, supporting mock implementation, and the custom verifier for Rustls were created with AI assistance.
1 parent bd4d441 commit 3adefc6

File tree

6 files changed

+457
-65
lines changed

6 files changed

+457
-65
lines changed

examples/tofu.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
extern crate electrum_client;
2+
3+
use electrum_client::{Client, Config, ElectrumApi, TofuStore};
4+
use std::collections::HashMap;
5+
use std::io::Result;
6+
use std::sync::{Arc, Mutex};
7+
8+
/// A simple in-memory implementation of TofuStore for demonstration purposes.
9+
#[derive(Debug, Default)]
10+
struct MyTofuStore {
11+
certs: Mutex<HashMap<String, Vec<u8>>>,
12+
}
13+
14+
impl TofuStore for MyTofuStore {
15+
fn get_certificate(&self, host: &str) -> Result<Option<Vec<u8>>> {
16+
let certs = self.certs.lock().unwrap();
17+
Ok(certs.get(host).cloned())
18+
}
19+
20+
fn set_certificate(&self, host: &str, cert: Vec<u8>) -> Result<()> {
21+
let mut certs = self.certs.lock().unwrap();
22+
certs.insert(host.to_string(), cert);
23+
Ok(())
24+
}
25+
}
26+
27+
fn main() {
28+
let store = Arc::new(MyTofuStore::default());
29+
30+
let client = Client::from_config_with_tofu(
31+
"ssl://electrum.blockstream.info:50002",
32+
Config::default(),
33+
store,
34+
)
35+
.unwrap();
36+
37+
let res = client.server_features();
38+
println!("{:#?}", res);
39+
}

src/client.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Electrum Client
22
3+
use std::sync::Arc;
34
use std::{borrow::Borrow, sync::RwLock};
45

56
use log::{info, warn};
@@ -10,6 +11,7 @@ use crate::api::ElectrumApi;
1011
use crate::batch::Batch;
1112
use crate::config::Config;
1213
use crate::raw_client::*;
14+
use crate::tofu::TofuStore;
1315
use crate::types::*;
1416
use std::convert::TryFrom;
1517

@@ -35,6 +37,7 @@ pub struct Client {
3537
client_type: RwLock<ClientType>,
3638
config: Config,
3739
url: String,
40+
tofu_store: Option<Arc<dyn TofuStore>>,
3841
}
3942

4043
macro_rules! impl_inner_call {
@@ -74,7 +77,7 @@ macro_rules! impl_inner_call {
7477
if let Ok(mut write_client) = $self.client_type.try_write() {
7578
loop {
7679
std::thread::sleep(std::time::Duration::from_secs((1 << errors.len()).min(30) as u64));
77-
match ClientType::from_config(&$self.url, &$self.config) {
80+
match ClientType::from_config(&$self.url, &$self.config, $self.tofu_store.clone()) {
7881
Ok(new_client) => {
7982
info!("Succesfully created new client");
8083
*write_client = new_client;
@@ -111,7 +114,11 @@ fn retries_exhausted(failed_attempts: usize, configured_retries: u8) -> bool {
111114
impl ClientType {
112115
/// Constructor that supports multiple backends and allows configuration through
113116
/// the [Config]
114-
pub fn from_config(url: &str, config: &Config) -> Result<Self, Error> {
117+
pub fn from_config(
118+
url: &str,
119+
config: &Config,
120+
tofu_store: Option<Arc<dyn TofuStore>>,
121+
) -> Result<Self, Error> {
115122
#[cfg(any(feature = "openssl", feature = "rustls", feature = "rustls-ring"))]
116123
if url.starts_with("ssl://") {
117124
let url = url.replacen("ssl://", "", 1);
@@ -122,14 +129,22 @@ impl ClientType {
122129
config.validate_domain(),
123130
socks5,
124131
config.timeout(),
132+
tofu_store,
133+
)?,
134+
None => RawClient::new_ssl(
135+
url.as_str(),
136+
config.validate_domain(),
137+
config.timeout(),
138+
tofu_store,
125139
)?,
126-
None => {
127-
RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?
128-
}
129140
};
130141
#[cfg(not(feature = "proxy"))]
131-
let client =
132-
RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?;
142+
let client = RawClient::new_ssl(
143+
url.as_str(),
144+
config.validate_domain(),
145+
config.timeout(),
146+
tofu_store,
147+
)?;
133148

134149
return Ok(ClientType::SSL(client));
135150
}
@@ -142,6 +157,12 @@ impl ClientType {
142157
}
143158

144159
{
160+
if tofu_store.is_some() {
161+
return Err(Error::Message(
162+
"TOFU validation is available only for SSL connections".to_string(),
163+
));
164+
}
165+
145166
let url = url.replacen("tcp://", "", 1);
146167
#[cfg(feature = "proxy")]
147168
let client = match config.socks5() {
@@ -178,12 +199,34 @@ impl Client {
178199
/// Generic constructor that supports multiple backends and allows configuration through
179200
/// the [Config]
180201
pub fn from_config(url: &str, config: Config) -> Result<Self, Error> {
181-
let client_type = RwLock::new(ClientType::from_config(url, &config)?);
202+
let client_type = RwLock::new(ClientType::from_config(url, &config, None)?);
203+
204+
Ok(Client {
205+
client_type,
206+
config,
207+
url: url.to_string(),
208+
tofu_store: None,
209+
})
210+
}
211+
212+
/// Creates a new client with TOFU (Trust On First Use) certificate validation.
213+
/// This constructor creates a SSL client that uses TOFU for certificate validation.
214+
pub fn from_config_with_tofu(
215+
url: &str,
216+
config: Config,
217+
tofu_store: Arc<dyn TofuStore>,
218+
) -> Result<Self, Error> {
219+
let client_type = RwLock::new(ClientType::from_config(
220+
url,
221+
&config,
222+
Some(tofu_store.clone()),
223+
)?);
182224

183225
Ok(Client {
184226
client_type,
185227
config,
186228
url: url.to_string(),
229+
tofu_store: Some(tofu_store),
187230
})
188231
}
189232
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ pub use batch::Batch;
6060
pub use client::*;
6161
pub use config::{Config, ConfigBuilder, Socks5Config};
6262
pub use types::*;
63+
64+
mod tofu;
65+
pub use tofu::TofuStore;

0 commit comments

Comments
 (0)