diff --git a/acmed/src/account.rs b/acmed/src/account.rs index 3118506..d0995b7 100644 --- a/acmed/src/account.rs +++ b/acmed/src/account.rs @@ -1,4 +1,4 @@ -use crate::acme_proto::account::register_account; +use crate::acme_proto::account::{register_account, update_account_contacts, update_account_key}; use crate::endpoint::Endpoint; use crate::logs::HasLogger; use crate::storage::FileManager; @@ -130,6 +130,17 @@ impl Account { } } + pub fn get_past_key(&self, key_hash: &[u8]) -> Result<&AccountKey, Error> { + let key_hash = key_hash.to_vec(); + for key in &self.past_keys { + let past_key_hash = hash_key(key)?; + if past_key_hash == key_hash { + return Ok(key); + } + } + Err("key not found".into()) + } + pub fn load( file_manager: &FileManager, name: &str, @@ -183,7 +194,21 @@ impl Account { endpoint: &mut Endpoint, root_certs: &[String], ) -> Result<(), Error> { - register_account(endpoint, root_certs, self)?; + let acc_ep = self.get_endpoint(&endpoint.name)?; + if !acc_ep.account_url.is_empty() { + let ct_hash = hash_contacts(&self.contacts); + let key_hash = hash_key(&self.current_key)?; + let contacts_changed = ct_hash != acc_ep.contacts_hash; + let key_changed = key_hash != acc_ep.key_hash; + if contacts_changed { + update_account_contacts(endpoint, root_certs, self)?; + } + if key_changed { + update_account_key(endpoint, root_certs, self)?; + } + } else { + register_account(endpoint, root_certs, self)?; + } Ok(()) } diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index a46ace8..b258a9d 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -58,6 +58,7 @@ impl PartialEq for Challenge { } } +#[macro_export] macro_rules! set_data_builder { ($account: ident, $endpoint_name: ident, $data: expr) => { |n: &str, url: &str| { @@ -138,8 +139,7 @@ pub fn request_certificate( // Tell the server the challenge has been completed let chall_url = challenge.get_url(); let data_builder = set_data_builder!(account, endpoint_name, b"{}"); - let _ = - http::post_challenge_response(endpoint, root_certs, &data_builder, &chall_url)?; + let _ = http::post_no_response(endpoint, root_certs, &data_builder, &chall_url)?; } } diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index dabda80..6a4a785 100644 --- a/acmed/src/acme_proto/account.rs +++ b/acmed/src/acme_proto/account.rs @@ -1,9 +1,10 @@ use crate::account::Account as BaseAccount; use crate::acme_proto::http; -use crate::acme_proto::structs::Account; +use crate::acme_proto::structs::{Account, AccountKeyRollover, AccountUpdate}; use crate::endpoint::Endpoint; -use crate::jws::encode_jwk; +use crate::jws::{encode_jwk, encode_jwk_no_nonce, encode_kid}; use crate::logs::HasLogger; +use crate::set_data_builder; use acme_common::error::Error; pub fn register_account( @@ -36,3 +37,61 @@ pub fn register_account( account.info(&format!("account created on endpoint {}", &endpoint.name)); Ok(()) } + +pub fn update_account_contacts( + endpoint: &mut Endpoint, + root_certs: &[String], + account: &mut BaseAccount, +) -> Result<(), Error> { + let endpoint_name = endpoint.name.clone(); + account.debug(&format!( + "updating account contacts on endpoint {}...", + &endpoint_name + )); + let new_contacts: Vec = account.contacts.iter().map(|c| c.to_string()).collect(); + let acc_up_struct = AccountUpdate::new(&new_contacts); + let acc_up_struct = serde_json::to_string(&acc_up_struct)?; + let data_builder = set_data_builder!(account, endpoint_name, acc_up_struct.as_bytes()); + let url = account.get_endpoint(&endpoint_name)?.account_url.clone(); + http::post_no_response(endpoint, root_certs, &data_builder, &url)?; + account.update_contacts_hash(&endpoint_name)?; + account.save()?; + account.info(&format!( + "account contacts updated on endpoint {}", + &endpoint_name + )); + Ok(()) +} + +pub fn update_account_key( + endpoint: &mut Endpoint, + root_certs: &[String], + account: &mut BaseAccount, +) -> Result<(), Error> { + let endpoint_name = endpoint.name.clone(); + account.debug(&format!( + "updating account key on endpoint {}...", + &endpoint_name + )); + let url = endpoint.dir.key_change.clone(); + let ep = account.get_endpoint(&endpoint_name)?; + let old_account_key = account.get_past_key(&ep.key_hash)?; + let old_key = &old_account_key.key; + let rollover_struct = AccountKeyRollover::new(account, &old_key)?; + let rollover_struct = serde_json::to_string(&rollover_struct)?; + let rollover_payload = encode_jwk_no_nonce( + &old_key, + &old_account_key.signature_algorithm, + rollover_struct.as_bytes(), + &url, + )?; + let data_builder = set_data_builder!(account, endpoint_name, rollover_payload.as_bytes()); + http::post_no_response(endpoint, root_certs, &data_builder, &url)?; + account.update_key_hash(&endpoint_name)?; + account.save()?; + account.info(&format!( + "account key updated on endpoint {}", + &endpoint_name + )); + Ok(()) +} diff --git a/acmed/src/acme_proto/http.rs b/acmed/src/acme_proto/http.rs index 87720ae..250c195 100644 --- a/acmed/src/acme_proto/http.rs +++ b/acmed/src/acme_proto/http.rs @@ -26,6 +26,19 @@ pub fn refresh_directory(endpoint: &mut Endpoint, root_certs: &[String]) -> Resu Ok(()) } +pub fn post_no_response( + endpoint: &mut Endpoint, + root_certs: &[String], + data_builder: &F, + url: &str, +) -> Result<(), Error> +where + F: Fn(&str, &str) -> Result, +{ + let _ = http::post_jose(endpoint, root_certs, &url, data_builder)?; + Ok(()) +} + pub fn new_account( endpoint: &mut Endpoint, root_certs: &[String], @@ -78,19 +91,6 @@ where Ok(auth) } -pub fn post_challenge_response( - endpoint: &mut Endpoint, - root_certs: &[String], - data_builder: &F, - url: &str, -) -> Result<(), Error> -where - F: Fn(&str, &str) -> Result, -{ - let _ = http::post_jose(endpoint, root_certs, &url, data_builder)?; - Ok(()) -} - pub fn pool_authorization( endpoint: &mut Endpoint, root_certs: &[String], diff --git a/acmed/src/acme_proto/structs.rs b/acmed/src/acme_proto/structs.rs index 8a8eedb..ba048bb 100644 --- a/acmed/src/acme_proto/structs.rs +++ b/acmed/src/acme_proto/structs.rs @@ -18,7 +18,9 @@ mod directory; mod error; mod order; -pub use account::{Account, AccountDeactivation, AccountResponse, AccountUpdate}; +pub use account::{ + Account, AccountDeactivation, AccountKeyRollover, AccountResponse, AccountUpdate, +}; pub use authorization::{Authorization, AuthorizationStatus, Challenge}; pub use deserialize_from_str; pub use directory::Directory; diff --git a/acmed/src/acme_proto/structs/account.rs b/acmed/src/acme_proto/structs/account.rs index 6fe7b54..0810439 100644 --- a/acmed/src/acme_proto/structs/account.rs +++ b/acmed/src/acme_proto/structs/account.rs @@ -1,6 +1,8 @@ use crate::endpoint::Endpoint; +use acme_common::crypto::KeyPair; use acme_common::error::Error; use serde::{Deserialize, Serialize}; +use serde_json::value::Value; use std::str::FromStr; #[derive(Serialize)] @@ -33,15 +35,12 @@ pub struct AccountResponse { deserialize_from_str!(AccountResponse); -// TODO: implement account update -#[allow(dead_code)] #[derive(Serialize)] pub struct AccountUpdate { pub contact: Vec, } impl AccountUpdate { - #[allow(dead_code)] pub fn new(contact: &[String]) -> Self { AccountUpdate { contact: contact.into(), @@ -49,6 +48,22 @@ impl AccountUpdate { } } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountKeyRollover { + pub account: String, + pub old_key: Value, +} + +impl AccountKeyRollover { + pub fn new(account: &crate::account::Account, old_key: &KeyPair) -> Result { + Ok(AccountKeyRollover { + account: account.name.clone(), + old_key: old_key.jwk_public_key()?, + }) + } +} + // TODO: implement account deactivation #[allow(dead_code)] #[derive(Serialize)] diff --git a/acmed/src/jws.rs b/acmed/src/jws.rs index 2c5f6b0..62d366f 100644 --- a/acmed/src/jws.rs +++ b/acmed/src/jws.rs @@ -11,6 +11,13 @@ struct JwsData { signature: String, } +#[derive(Serialize)] +struct JwsProtectedHeaderJwkNoNonce { + alg: String, + jwk: Value, + url: String, +} + #[derive(Serialize)] struct JwsProtectedHeaderJwk { alg: String, @@ -64,6 +71,21 @@ pub fn encode_jwk( get_data(key_pair, sign_alg, &protected, payload) } +pub fn encode_jwk_no_nonce( + key_pair: &KeyPair, + sign_alg: &JwsSignatureAlgorithm, + payload: &[u8], + url: &str, +) -> Result { + let protected = JwsProtectedHeaderJwkNoNonce { + alg: sign_alg.to_string(), + jwk: key_pair.jwk_public_key()?, + url: url.into(), + }; + let protected = serde_json::to_string(&protected)?; + get_data(key_pair, sign_alg, &protected, payload) +} + pub fn encode_kid( key_pair: &KeyPair, sign_alg: &JwsSignatureAlgorithm,