Skip to content

Support client side tls certificates reload #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

kezhuw
Copy link
Owner

@kezhuw kezhuw commented Jul 7, 2025

This allows creating a client with dynamic tls certificates. When created this way, on reconnection the client will use latest tls certificates.

This allows us to have auto-reloading of refreshed certificates stored anywhere in client side.

Resolves #59.

@kezhuw kezhuw force-pushed the tls-certs-reload branch 2 times, most recently from 2bc7594 to a7a17be Compare July 7, 2025 11:07
@kezhuw
Copy link
Owner Author

kezhuw commented Jul 7, 2025

@behos Would you mind take a look ? Does it solve your concerns ? I included a file-based certs in test.

struct FileBasedDynamicCerts {
ca: Ca,
dir: TempDir,
certs: TlsDynamicCerts,
feedback: UnboundedReceiver<()>,
_task: TaskHandle<()>,
}
struct EventSender {
sender: UnboundedSender<Event>,
}
impl notify::EventHandler for EventSender {
fn handle_event(&mut self, event: Result<Event, notify::Error>) {
if let Ok(event) = event {
self.sender.unbounded_send(event).unwrap();
}
}
}
impl FileBasedDynamicCerts {
pub fn new(ca: Ca, client_name: &str) -> Self {
let dir = TempDir::new().unwrap();
Self::generate_cert(&ca, dir.path(), client_name);
let (certs, feedback, _task) = Self::load_dynamic_certs(dir.path());
Self { ca, dir, certs, feedback, _task }
}
fn load_dynamic_certs(dir: &Path) -> (TlsDynamicCerts, UnboundedReceiver<()>, TaskHandle<()>) {
let cert_path = dir.join("cert.pem").to_path_buf();
let key_path = dir.join("cert.key.pem").to_path_buf();
let mut cert_modified = std::fs::metadata(&cert_path).unwrap().modified().unwrap();
let mut key_modified = std::fs::metadata(&key_path).unwrap().modified().unwrap();
let client_cert = std::fs::read_to_string(&cert_path).unwrap();
let client_key = std::fs::read_to_string(&key_path).unwrap();
let dynamic_certs =
TlsDynamicCerts::new(TlsCerts::default().with_pem_identity(&client_cert, &client_key).unwrap());
let dynamic_certs_updator = dynamic_certs.clone();
let (feedback_sender, feedback_receiver) = mpsc::unbounded();
let task = asyncs::spawn(async move {
let (tx, mut rx) = mpsc::unbounded();
let mut watcher = notify::recommended_watcher(EventSender { sender: tx }).unwrap();
watcher.watch(&cert_path, RecursiveMode::NonRecursive).unwrap();
watcher.watch(&key_path, RecursiveMode::NonRecursive).unwrap();
while rx.next().await.is_some() {
let updated_cert_modified = std::fs::metadata(&cert_path).unwrap().modified().unwrap();
let updated_key_modified = std::fs::metadata(&key_path).unwrap().modified().unwrap();
if updated_cert_modified <= cert_modified || updated_key_modified <= key_modified {
continue;
}
cert_modified = updated_cert_modified;
key_modified = updated_key_modified;
let client_cert = std::fs::read_to_string(&cert_path).unwrap();
let client_key = std::fs::read_to_string(&key_path).unwrap();
dynamic_certs_updator
.update(TlsCerts::default().with_pem_identity(&client_cert, &client_key).unwrap());
feedback_sender.unbounded_send(()).unwrap();
}
})
.attach();
(dynamic_certs, feedback_receiver, task)
}
fn generate_cert(ca: &Ca, dir: &Path, name: &str) {
let client_cert = generate_client_cert(name, &ca.issuer);
let file = AtomicWriteFile::open(dir.join("cert.pem")).unwrap();
write!(&file, "{}", client_cert.cert.pem()).unwrap();
file.commit().unwrap();
let file = AtomicWriteFile::open(dir.join("cert.key.pem")).unwrap();
write!(&file, "{}", client_cert.signing_key.serialize_pem()).unwrap();
file.commit().unwrap();
}
pub async fn regenerate_cert(&mut self, client_name: &str) {
Self::generate_cert(&self.ca, self.dir.path(), client_name);
self.feedback.next().await;
}
}
#[asyncs::test]
async fn with_file_based_dynamic_certs() {
let (ca, listener) = listen().await;
let options = TlsOptions::new().with_pem_ca_certs(&ca.cert.pem()).unwrap();
let mut certs = FileBasedDynamicCerts::new(ca, "client1");
let options = options.with_certs(certs.certs.clone());
let client = options.into_client().unwrap();
assert_client_name(&listener, &client, "client1").await;
certs.regenerate_cert("client2").await;
assert_client_name(&listener, &client, "client2").await;
}

@kezhuw kezhuw force-pushed the tls-certs-reload branch from a7a17be to a2cd525 Compare July 7, 2025 18:38
Copy link
Contributor

@behos behos left a comment

Choose a reason for hiding this comment

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

LGTM, the file based implementation can either be exposed here or delegated to external services.

Comment on lines 352 to 533
struct FileBasedDynamicCerts {
ca: Ca,
dir: TempDir,
certs: TlsDynamicCerts,
feedback: UnboundedReceiver<()>,
_task: TaskHandle<()>,
}

struct EventSender {
sender: UnboundedSender<Event>,
}

impl notify::EventHandler for EventSender {
fn handle_event(&mut self, event: Result<Event, notify::Error>) {
if let Ok(event) = event {
self.sender.unbounded_send(event).unwrap();
}
}
}

impl FileBasedDynamicCerts {
pub fn new(ca: Ca, client_name: &str) -> Self {
let dir = TempDir::new().unwrap();
Self::generate_cert(&ca, dir.path(), client_name);
let (certs, feedback, _task) = Self::load_dynamic_certs(dir.path());
Self { ca, dir, certs, feedback, _task }
}

fn load_dynamic_certs(dir: &Path) -> (TlsDynamicCerts, UnboundedReceiver<()>, TaskHandle<()>) {
let cert_path = dir.join("cert.pem").to_path_buf();
let key_path = dir.join("cert.key.pem").to_path_buf();

let mut cert_modified = std::fs::metadata(&cert_path).unwrap().modified().unwrap();
let mut key_modified = std::fs::metadata(&key_path).unwrap().modified().unwrap();
let client_cert = std::fs::read_to_string(&cert_path).unwrap();
let client_key = std::fs::read_to_string(&key_path).unwrap();

let dynamic_certs =
TlsDynamicCerts::new(TlsCerts::default().with_pem_identity(&client_cert, &client_key).unwrap());
let dynamic_certs_updator = dynamic_certs.clone();

let (feedback_sender, feedback_receiver) = mpsc::unbounded();
let task = asyncs::spawn(async move {
let (tx, mut rx) = mpsc::unbounded();
let mut watcher = notify::recommended_watcher(EventSender { sender: tx }).unwrap();
watcher.watch(&cert_path, RecursiveMode::NonRecursive).unwrap();
watcher.watch(&key_path, RecursiveMode::NonRecursive).unwrap();
while rx.next().await.is_some() {
let updated_cert_modified = std::fs::metadata(&cert_path).unwrap().modified().unwrap();
let updated_key_modified = std::fs::metadata(&key_path).unwrap().modified().unwrap();
if updated_cert_modified <= cert_modified || updated_key_modified <= key_modified {
continue;
}
cert_modified = updated_cert_modified;
key_modified = updated_key_modified;
let client_cert = std::fs::read_to_string(&cert_path).unwrap();
let client_key = std::fs::read_to_string(&key_path).unwrap();
dynamic_certs_updator
.update(TlsCerts::default().with_pem_identity(&client_cert, &client_key).unwrap());
feedback_sender.unbounded_send(()).unwrap();
}
})
.attach();
(dynamic_certs, feedback_receiver, task)
}

fn generate_cert(ca: &Ca, dir: &Path, name: &str) {
let client_cert = generate_client_cert(name, &ca.issuer);
let file = AtomicWriteFile::open(dir.join("cert.pem")).unwrap();
write!(&file, "{}", client_cert.cert.pem()).unwrap();
file.commit().unwrap();

let file = AtomicWriteFile::open(dir.join("cert.key.pem")).unwrap();
write!(&file, "{}", client_cert.signing_key.serialize_pem()).unwrap();
file.commit().unwrap();
}

pub async fn regenerate_cert(&mut self, client_name: &str) {
Self::generate_cert(&self.ca, self.dir.path(), client_name);
self.feedback.next().await;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think it's worth having a FileBased implementation into the main package so that it's easily reusable?

Copy link
Owner Author

Choose a reason for hiding this comment

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

I think such an official file based implementation may have biased on the change detection. And, worse, it will propgate this to applications. I think we should avoid such a thing in library.

Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds good to me! The example looks clean enough to use as a basis for an implementation anyway. Thanks

@behos behos mentioned this pull request Jul 8, 2025
Comment on lines +98 to +171
/// Cell to keep up to date [TlsCerts].
#[derive(Clone, Debug)]
pub struct TlsDynamicCerts {
certs: Arc<RwLock<(u64, Arc<TlsCerts>)>>,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm trying to use this from the branch and it seems it's not visible.

Copy link
Owner Author

Choose a reason for hiding this comment

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

My fault! I have referenced it in integration test.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, I was able to create and test a simple file-based reloader that can be plugged into the new structs. It works great

@kezhuw kezhuw force-pushed the tls-certs-reload branch 6 times, most recently from 15be32d to e1a6e06 Compare July 13, 2025 04:14
This allows creating a client with dynamic tls certificates. When
created this way, on reconnection the client will use latest tls
certificates.

This allows us to have auto-reloading of refreshed certificates stored
anywhere in client side.

Resolves #59.
@kezhuw kezhuw force-pushed the tls-certs-reload branch from e1a6e06 to e3adbc4 Compare July 13, 2025 04:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants