[pmg-devel] [PATCH acme-rs 1/8] add external account binding
Folke Gleumes
f.gleumes at proxmox.com
Tue Nov 14 15:14:00 CET 2023
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 <f.gleumes at proxmox.com>
---
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<bool>,
- /// 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<Value>,
+ pub external_account_binding: Option<ExternalAccountBinding>,
/// 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<String>,
terms_of_service_agreed: bool,
key: Option<PKey<Private>>,
+ eab_credentials: Option<(String, PKey<Private>)>,
}
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<Self, Error> {
+ 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<Self, Error> {
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<Request, Error> {
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<P>(
+ eab_kid: &str,
+ eab_hmac_key: &PKeyRef<P>,
+ jwk: Jwk,
+ url: String,
+ ) -> Result<Self, Error>
+ 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<P>(key: &PKeyRef<P>, protected: &[u8], payload: &[u8]) -> Result<Vec<u8>, 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<crate::request::ErrorResponse> for Error {
Error::Api(e)
}
}
+
+impl From<base64::DecodeError> 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
More information about the pmg-devel
mailing list