From d6e95bade5ca311dea83575b26e055d17533bd88 Mon Sep 17 00:00:00 2001 From: Roman Kivalin Date: Tue, 14 Apr 2026 15:24:30 +0200 Subject: [PATCH] Fix account show creating account when credentials are missing Split AcmeClient::new into load (existing only) and create (register new). account show now uses load and errors if no credentials exist, account create always registers a new account. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + src/acme.rs | 123 +++++++++++++++++++++++++++------------------------ src/main.rs | 4 +- 3 files changed, 68 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f65bb..09007d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- Fix `account show` creating a new account instead of erroring when no credentials exist - Fix inline hooks ignoring `--dry-run` flag - Validate certificate name and hook name uniqueness - Validate that every domain has a solver configured (via `solver`, `solvers`, or `default_solver`) diff --git a/src/acme.rs b/src/acme.rs index abd698a..1a8a93b 100644 --- a/src/acme.rs +++ b/src/acme.rs @@ -30,72 +30,79 @@ pub struct ChallengeInfo { } impl AcmeClient { + /// Load existing account or create a new one if credentials don't exist. pub async fn new(config: &AcmeConfig) -> Result { - let cred_data = credentials::load_secret( + match Self::load(config).await { + Ok(client) => Ok(client), + Err(_) => Self::create(config).await, + } + } + + /// Load an existing account from credentials. Errors if no credentials exist. + pub async fn load(config: &AcmeConfig) -> Result { + let data = credentials::load_secret( config.account_key_credential.as_deref(), config.account_key_path.as_deref(), ) - .await; + .await?; - match cred_data { - Ok(data) => { - let creds: AccountCredentials = serde_json::from_slice(&data).map_err(|e| { - Error::Config(format!("invalid account credentials: {e}")) - })?; - let account = Account::builder() - .map_err(Error::Acme)? - .from_credentials(creds) - .await?; - tracing::info!(id = %account.id(), "loaded existing ACME account"); - Ok(Self { account }) + let creds: AccountCredentials = serde_json::from_slice(&data).map_err(|e| { + Error::Config(format!("invalid account credentials: {e}")) + })?; + let account = Account::builder() + .map_err(Error::Acme)? + .from_credentials(creds) + .await?; + tracing::info!(id = %account.id(), "loaded existing ACME account"); + Ok(Self { account }) + } + + /// Create a new ACME account and save credentials. + pub async fn create(config: &AcmeConfig) -> Result { + tracing::info!(directory = %config.directory_url, "creating new ACME account"); + let (account, creds) = Account::builder() + .map_err(Error::Acme)? + .create( + &NewAccount { + contact: &config + .contact + .iter() + .map(|s| s.as_str()) + .collect::>(), + terms_of_service_agreed: true, + only_return_existing: false, + }, + config.directory_url.clone(), + None, + ) + .await?; + + tracing::info!(id = %account.id(), "created new ACME account"); + + let creds_json = serde_json::to_vec(&creds) + .map_err(|e| Error::Config(format!("failed to serialize credentials: {e}")))?; + + if let Some(cred_path) = &config.account_key_credential { + credentials::encrypt_credential(&creds_json, cred_path) + .await?; + tracing::info!(path = %cred_path.display(), "saved account credentials (encrypted)"); + } else if let Some(file_path) = &config.account_key_path { + if let Some(parent) = file_path.parent() { + std::fs::create_dir_all(parent)?; } - Err(_) => { - tracing::info!(directory = %config.directory_url, "creating new ACME account"); - let (account, creds) = Account::builder() - .map_err(Error::Acme)? - .create( - &NewAccount { - contact: &config - .contact - .iter() - .map(|s| s.as_str()) - .collect::>(), - terms_of_service_agreed: true, - only_return_existing: false, - }, - config.directory_url.clone(), - None, - ) - .await?; - - tracing::info!(id = %account.id(), "created new ACME account"); - - let creds_json = serde_json::to_vec(&creds) - .map_err(|e| Error::Config(format!("failed to serialize credentials: {e}")))?; - - if let Some(cred_path) = &config.account_key_credential { - credentials::encrypt_credential(&creds_json, cred_path) - .await?; - tracing::info!(path = %cred_path.display(), "saved account credentials (encrypted)"); - } else if let Some(file_path) = &config.account_key_path { - if let Some(parent) = file_path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(file_path, &creds_json)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions( - file_path, - std::fs::Permissions::from_mode(0o600), - )?; - } - tracing::info!(path = %file_path.display(), "saved account credentials"); - } - - Ok(Self { account }) + std::fs::write(file_path, &creds_json)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions( + file_path, + std::fs::Permissions::from_mode(0o600), + )?; } + tracing::info!(path = %file_path.display(), "saved account credentials"); } + + Ok(Self { account }) } /// Create a new ACME order for the given domains/IPs. diff --git a/src/main.rs b/src/main.rs index b4b32fc..79d9781 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,11 +82,11 @@ async fn main() -> anyhow::Result<()> { let config = config::Config::load(&cli.config)?; match action { AccountAction::Create => { - let client = acme::AcmeClient::new(&config.acme).await?; + let client = acme::AcmeClient::create(&config.acme).await?; println!("Account ID: {}", client.account().id()); } AccountAction::Show => { - let client = acme::AcmeClient::new(&config.acme).await?; + let client = acme::AcmeClient::load(&config.acme).await?; println!("Account ID: {}", client.account().id()); } AccountAction::Deactivate => {