From 9b12e88ae1e33dc1a5f1366e00a2f2124054ff41 Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Tue, 1 Sep 2020 16:01:56 +0200 Subject: [PATCH] Allow accounts to be updated The previous strategy for accounts management on endpoints was to send an account creation request every time in order to retrieve the account URL. Although it works on most cases, the contact information or key update wasn't handled correctly. The only drawback of this new way of managing accounts is that, if the endpoints drops the account, ACMEd will fail to renew any certificate for this account on this endpoint. Previously, it would have simply created a new account. This should be fixed soon. --- acmed/src/account.rs | 29 +++++++++++- acmed/src/acme_proto.rs | 4 +- acmed/src/acme_proto/account.rs | 63 ++++++++++++++++++++++++- acmed/src/acme_proto/http.rs | 26 +++++----- acmed/src/acme_proto/structs.rs | 4 +- acmed/src/acme_proto/structs/account.rs | 21 +++++++-- acmed/src/jws.rs | 22 +++++++++ 7 files changed, 146 insertions(+), 23 deletions(-) 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,