Skip to content

Conversation

@lucasdbr05
Copy link

What does this merge request do?

This feature (see the reference issue #176) adds SSL certificate validation based on Trust On First Use (TOFU), storing the certificate on the first connection and verifying its consistency on subsequent connections.
It is implemented via the TofuStore trait, which allows customizable certificate persistence.

  • Introduces a refactored Trust On First Use (TOFU) implementation for SSL certificate verification.
  • Adds the TofuStore trait, allowing library consumers to provide their own certificate persistence mechanism.
  • Updates the Client configuration SSL client initialization flow to accept a TofuStore via ConfigBuilder.
  • Adds examples in examples directory and scripts to demonstrate how TOFU can be integrated and used in practice.

- 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
- verifies initial certificate storage on first use
- ensures stored certificates are retrieved correctly
- tests certificate updates and replacement
- confirms data persistence across store instances
- tests handling of large certificates and empty certificates
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
This allows users to use the TOFU flow directly through the high-level  API by providing a store implementation in the

- Update  to automatically establish TOFU-validated SSL connections when a store is provided in the configuration
- Add the  field to the  struct to support Trust On First Use (TOFU) validation. Also change  to allow injecting custom store implementations through the builder pattern
Add examples/tofu.rs, demonstrating Trust On First Use (TOFU)
certificate validation for SSL connections.

The example shows how to implement a custom TofuStore and how to
configure a client to use TOFU for secure certificate verification.
It uses a simple in-memory TofuStore implementation for
demonstration purposes only.
@lucasdbr05 lucasdbr05 changed the title Feat/tofu certificate validation feat:tofu certificate validation Jan 14, 2026
Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took an initial quick look and added some comments (though have yet to do a full review). Though before I proceed, please rewrite the commit history to avoid changing the approach mid-history. Basically, you should try to avoid to touch the same code paths in following commits as far as possible.

Given the code structure and verbosity I'm also suspecting that some form of AI agent was involved here. If this is indeed the case, please note that it is best practice to disclose such use in the PR and commit descriptions.


/// A trait for storing and retrieving TOFU (Trust On First Use) certificate data.
/// Implementors of this trait are responsible for persisting certificate data and retrieving it based on the host.
pub trait TofuStore: Send + Sync + Debug {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be Send + Sync?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it this way to ensure that the Client remains thread-safe for concurrent applications, so I designed it to allow the store to be shared and safely accessed across multiple threads via an Arc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess? Though the Rust compiler should usually infer the bounds when needed.

/// when ssl, validate the domain, default true
validate_domain: bool,
/// TOFU store for certificate validation
tofu_store: Option<Arc<dyn TofuStore>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned over at the issue, it would likely be preferable to add this via a separate constructor. Having an Arc<dyn ..> in a config is rather odd, as it mixes code and data, basically.

.connect(&domain, stream)
.map_err(Error::SslHandshakeError)?;

if !validate_domain {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure why we here error out? Care to explain?

builder.set_verify(SslVerifyMode::NONE);
let connector = builder.build();

let domain = socket_addrs.domain().unwrap_or("NONE").to_string();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, if we don't have a domain we should just abort rather than try to connect to NONE?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, I’ll update it.

@lucasdbr05
Copy link
Author

Took an initial quick look and added some comments (though have yet to do a full review). Though before I proceed, please rewrite the commit history to avoid changing the approach mid-history. Basically, you should try to avoid to touch the same code paths in following commits as far as possible.

Given the code structure and verbosity I'm also suspecting that some form of AI agent was involved here. If this is indeed the case, please note that it is best practice to disclose such use in the PR and commit descriptions.

First of all, thanks for the feedback. When rewriting the history, the best approach would be to avoid keeping commits that record my refactor from persistence-based usage to the trait-based approach, right?

@tnull
Copy link
Contributor

tnull commented Jan 15, 2026

When rewriting the history, the best approach would be to avoid keeping commits that record my refactor from persistence-based usage to the trait-based approach, right?

Yes, this would be preferable. Basically, you could just do a git reset and then commit the individual pieces again.

@luisschwab luisschwab self-requested a review January 17, 2026 20:32
@oleonardolima oleonardolima self-requested a review January 19, 2026 17:54
@oleonardolima oleonardolima added the new feature New feature or request label Jan 19, 2026
@oleonardolima oleonardolima moved this to Discussion in BDK Chain Jan 19, 2026
Copy link
Contributor

@oleonardolima oleonardolima left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with tnull's comments above, you should narrow it down to few commits, for example: feat(tofu): add tofu store mod; feat(client): add new tofu method/feature, docs(example): add new tofu example.

Also, it's best if it's added under a new feature, and by a separate constructor. I don't think many changes to already-existing methods are needed, maybe it's something remaining from previous changes you did.

It's failing in CI, please make sure that everything is building successfully and passing CI too.


let builder = ClientConfig::builder();

let domain = socket_addr.domain().unwrap_or("NONE").to_string();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as tnull's previous comment, it should error-out and it's been handled below.

Comment on lines +616 to +623
let error_msg = format!("{}", e);
if error_msg.contains("TLS certificate changed") {
Error::TlsCertificateChanged(domain.clone())
} else if error_msg.contains("TOFU") {
Error::TofuPersistError(error_msg)
} else {
Error::CouldNotCreateConnection(e)
}
Copy link
Contributor

@oleonardolima oleonardolima Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO it's not ideal to handle the error variants by checking if it contains certain strings on it, you should properly return an map a new error variant where needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

Status: Discussion

Development

Successfully merging this pull request may close these issues.

3 participants