From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 2704499EFC for ; Tue, 14 Nov 2023 15:07:48 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id DD2441C759 for ; Tue, 14 Nov 2023 15:07:47 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Tue, 14 Nov 2023 15:07:46 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id CC14D42995 for ; Tue, 14 Nov 2023 15:07:45 +0100 (CET) From: Folke Gleumes To: pve-devel@lists.proxmox.com Date: Tue, 14 Nov 2023 15:07:11 +0100 Message-Id: <20231114140719.225971-2-f.gleumes@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20231114140719.225971-1-f.gleumes@proxmox.com> References: <20231114140719.225971-1-f.gleumes@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.018 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH acme-rs 1/8] add external account binding X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 14 Nov 2023 14:07:48 -0000 Functionality was added as a additional setter function, which hopefully prevents any breakages. Since a placeholder Option an the AccountData was already present, but has never been used, replacing the field with an Option of a fully defined type should also be minimally intrusive. Signed-off-by: Folke Gleumes --- src/account.rs | 28 ++++++++++++++++----- src/eab.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 10 ++++++++ src/lib.rs | 1 + 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 src/eab.rs diff --git a/src/account.rs b/src/account.rs index 8144d39..9f3af26 100644 --- a/src/account.rs +++ b/src/account.rs @@ -11,8 +11,9 @@ use serde_json::Value; use crate::authorization::{Authorization, GetAuthorization}; use crate::b64u; use crate::directory::Directory; +use crate::eab::ExternalAccountBinding; use crate::jws::Jws; -use crate::key::PublicKey; +use crate::key::{Jwk, PublicKey}; use crate::order::{NewOrder, Order, OrderData}; use crate::request::Request; use crate::Error; @@ -336,10 +337,9 @@ pub struct AccountData { #[serde(skip_serializing_if = "Option::is_none")] pub terms_of_service_agreed: Option, - /// External account information. This is currently not directly supported in any way and only - /// stored to completeness. + /// External account information. #[serde(skip_serializing_if = "Option::is_none")] - pub external_account_binding: Option, + pub external_account_binding: Option, /// This is only used by the client when querying an account. #[serde(default = "default_true", skip_serializing_if = "is_false")] @@ -375,6 +375,7 @@ pub struct AccountCreator { contact: Vec, terms_of_service_agreed: bool, key: Option>, + eab_credentials: Option<(String, PKey)>, } impl AccountCreator { @@ -402,6 +403,13 @@ impl AccountCreator { self } + /// Set the EAB credentials for the account registration + pub fn set_eab_credentials(mut self, kid: String, hmac_key: String) -> Result { + let hmac_key = PKey::hmac(&base64::decode(hmac_key)?)?; + self.eab_credentials = Some((kid, hmac_key)); + Ok(self) + } + /// Generate a new RSA key of the specified key size. pub fn generate_rsa_key(self, bits: u32) -> Result { let key = openssl::rsa::Rsa::generate(bits)?; @@ -431,6 +439,15 @@ impl AccountCreator { /// [`response`](AccountCreator::response()) will render the account unusable! pub fn request(&self, directory: &Directory, nonce: &str) -> Result { let key = self.key.as_deref().ok_or(Error::MissingKey)?; + let url = directory.new_account_url(); + + let external_account_binding = self + .eab_credentials + .as_ref() + .map(|cred| { + ExternalAccountBinding::new(&cred.0, &cred.1, Jwk::try_from(key)?, url.to_string()) + }) + .transpose()?; let data = AccountData { orders: None, @@ -441,12 +458,11 @@ impl AccountCreator { } else { None }, - external_account_binding: None, + external_account_binding, only_return_existing: false, extra: HashMap::new(), }; - let url = directory.new_account_url(); let body = serde_json::to_string(&Jws::new( key, None, diff --git a/src/eab.rs b/src/eab.rs new file mode 100644 index 0000000..a4c0642 --- /dev/null +++ b/src/eab.rs @@ -0,0 +1,66 @@ +use openssl::hash::MessageDigest; +use openssl::pkey::{HasPrivate, PKeyRef}; +use openssl::sign::Signer; +use serde::{Deserialize, Serialize}; + +use crate::key::Jwk; +use crate::{b64u, Error}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct Protected { + alg: &'static str, + url: String, + kid: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ExternalAccountBinding { + protected: String, + payload: String, + signature: String, +} + +impl ExternalAccountBinding { + pub fn new

( + eab_kid: &str, + eab_hmac_key: &PKeyRef

, + jwk: Jwk, + url: String, + ) -> Result + where + P: HasPrivate, + { + let protected = Protected { + alg: "HS256", + kid: eab_kid.to_string(), + url, + }; + let payload = b64u::encode(serde_json::to_string(&jwk)?.as_bytes()); + let protected_data = b64u::encode(serde_json::to_string(&protected)?.as_bytes()); + let signature = { + let protected = protected_data.as_bytes(); + let payload = payload.as_bytes(); + Self::sign_hmac(eab_hmac_key, protected, payload)? + }; + + let signature = b64u::encode(&signature); + Ok(ExternalAccountBinding { + protected: protected_data, + payload, + signature, + }) + } + + fn sign_hmac

(key: &PKeyRef

, protected: &[u8], payload: &[u8]) -> Result, Error> + where + P: HasPrivate, + { + let mut signer = Signer::new(MessageDigest::sha256(), key)?; + signer.update(protected)?; + signer.update(b".")?; + signer.update(payload)?; + Ok(signer.sign_to_vec()?) + } +} diff --git a/src/error.rs b/src/error.rs index bcfaed0..59da3ea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,9 @@ pub enum Error { /// An otherwise uncaught serde error happened. Json(serde_json::Error), + /// Failed to parse + BadBase64(base64::DecodeError), + /// Can be used by the user for textual error messages without having to downcast to regular /// acme errors. Custom(String), @@ -121,6 +124,7 @@ impl fmt::Display for Error { Error::HttpClient(err) => fmt::Display::fmt(err, f), Error::Client(err) => fmt::Display::fmt(err, f), Error::Csr(err) => fmt::Display::fmt(err, f), + Error::BadBase64(err) => fmt::Display::fmt(err, f), } } } @@ -142,3 +146,9 @@ impl From for Error { Error::Api(e) } } + +impl From for Error { + fn from(e: base64::DecodeError) -> Self { + Error::BadBase64(e) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3533b29..98ad04e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ #![deny(missing_docs)] mod b64u; +mod eab; mod json; mod jws; mod key; -- 2.39.2