BREAKING CHANGE(smart-proxy): move certificate persistence to an in-memory store and introduce consumer-managed certStore API; add default self-signed fallback cert and change ACME account handling

This commit is contained in:
2026-02-13 16:32:02 +00:00
parent e0af82c1ef
commit 0e058594c9
17 changed files with 296 additions and 397 deletions

View File

@@ -1,16 +1,15 @@
//! ACME (Let's Encrypt) integration using instant-acme.
//!
//! This module handles HTTP-01 challenge creation and certificate provisioning.
//! Supports persisting ACME account credentials to disk for reuse across restarts.
//! Account credentials are ephemeral — the consumer owns all persistence.
use std::path::{Path, PathBuf};
use instant_acme::{
Account, NewAccount, NewOrder, Identifier, ChallengeType, OrderStatus,
AccountCredentials,
};
use rcgen::{CertificateParams, KeyPair};
use thiserror::Error;
use tracing::{debug, info, warn};
use tracing::{debug, info};
#[derive(Debug, Error)]
pub enum AcmeError {
@@ -26,8 +25,6 @@ pub enum AcmeError {
NoHttp01Challenge,
#[error("Timeout waiting for order: {0}")]
Timeout(String),
#[error("Account persistence error: {0}")]
Persistence(String),
}
/// Pending HTTP-01 challenge that needs to be served.
@@ -41,8 +38,6 @@ pub struct PendingChallenge {
pub struct AcmeClient {
use_production: bool,
email: String,
/// Optional directory where account.json is persisted.
account_dir: Option<PathBuf>,
}
impl AcmeClient {
@@ -50,56 +45,15 @@ impl AcmeClient {
Self {
use_production,
email,
account_dir: None,
}
}
/// Create a new client with account persistence at the given directory.
pub fn with_persistence(email: String, use_production: bool, account_dir: impl AsRef<Path>) -> Self {
Self {
use_production,
email,
account_dir: Some(account_dir.as_ref().to_path_buf()),
}
}
/// Get or create an ACME account, persisting credentials if account_dir is set.
/// Create a new ACME account (ephemeral — not persisted).
async fn get_or_create_account(&self) -> Result<Account, AcmeError> {
let directory_url = self.directory_url();
// Try to restore from persisted credentials
if let Some(ref dir) = self.account_dir {
let account_file = dir.join("account.json");
if account_file.exists() {
match std::fs::read_to_string(&account_file) {
Ok(json) => {
match serde_json::from_str::<AccountCredentials>(&json) {
Ok(credentials) => {
match Account::from_credentials(credentials).await {
Ok(account) => {
debug!("Restored ACME account from {}", account_file.display());
return Ok(account);
}
Err(e) => {
warn!("Failed to restore ACME account, creating new: {}", e);
}
}
}
Err(e) => {
warn!("Invalid account.json, creating new account: {}", e);
}
}
}
Err(e) => {
warn!("Could not read account.json: {}", e);
}
}
}
}
// Create a new account
let contact = format!("mailto:{}", self.email);
let (account, credentials) = Account::create(
let (account, _credentials) = Account::create(
&NewAccount {
contact: &[&contact],
terms_of_service_agreed: true,
@@ -113,27 +67,6 @@ impl AcmeClient {
debug!("ACME account created");
// Persist credentials if we have a directory
if let Some(ref dir) = self.account_dir {
if let Err(e) = std::fs::create_dir_all(dir) {
warn!("Failed to create account directory {}: {}", dir.display(), e);
} else {
let account_file = dir.join("account.json");
match serde_json::to_string_pretty(&credentials) {
Ok(json) => {
if let Err(e) = std::fs::write(&account_file, &json) {
warn!("Failed to persist ACME account to {}: {}", account_file.display(), e);
} else {
info!("ACME account credentials persisted to {}", account_file.display());
}
}
Err(e) => {
warn!("Failed to serialize account credentials: {}", e);
}
}
}
}
Ok(account)
}
@@ -158,7 +91,7 @@ impl AcmeClient {
{
info!("Starting ACME provisioning for {} via {}", domain, self.directory_url());
// 1. Get or create ACME account (with persistence)
// 1. Get or create ACME account
let account = self.get_or_create_account().await?;
// 2. Create order
@@ -339,22 +272,4 @@ mod tests {
assert!(!client.directory_url().contains("staging"));
assert!(client.is_production());
}
#[test]
fn test_with_persistence_sets_account_dir() {
let tmp = tempfile::tempdir().unwrap();
let client = AcmeClient::with_persistence(
"test@example.com".to_string(),
false,
tmp.path(),
);
assert!(client.account_dir.is_some());
assert_eq!(client.account_dir.unwrap(), tmp.path());
}
#[test]
fn test_without_persistence_no_account_dir() {
let client = AcmeClient::new("test@example.com".to_string(), false);
assert!(client.account_dir.is_none());
}
}