@@ -11,6 +11,8 @@ use std::sync::atomic::{AtomicUsize, Ordering};
1111use std:: sync:: mpsc:: { channel, Receiver , Sender } ;
1212use std:: sync:: { Arc , Mutex , TryLockError } ;
1313use std:: time:: Duration ;
14+ use std:: convert:: TryFrom ;
15+
1416
1517#[ allow( unused_imports) ]
1618use 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) ) ]
330360mod 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