* [pmg-devel] [PATCH acme-rs 1/8] add external account binding
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-11-14 14:14 ` [pmg-devel] [PATCH acme-rs 2/8] add meta fields returned by the directory Folke Gleumes
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
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@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
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] [PATCH acme-rs 2/8] add meta fields returned by the directory
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
  2023-11-14 14:14 ` [pmg-devel] [PATCH acme-rs 1/8] add external account binding Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-12-04 10:56   ` [pmg-devel] applied: " Wolfgang Bumiller
  2023-11-14 14:14 ` [pmg-devel] [PATCH] expand helper function by eab credentials Folke Gleumes
                   ` (7 subsequent siblings)
  9 siblings, 1 reply; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
According to the rfc, the meta field contains additional fields that
weren't covered by the Meta struct. Of the additional fields, only
external_account_required will be used in the near future, but others
were added for completeness and the case that they might be used in the
future.
Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
---
 src/directory.rs | 25 +++++++++++++++++++++++--
 1 file changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/directory.rs b/src/directory.rs
index 755ea8c..a9d31f2 100644
--- a/src/directory.rs
+++ b/src/directory.rs
@@ -47,6 +47,18 @@ pub struct Meta {
     /// The terms of service. This is typically in the form of an URL.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub terms_of_service: Option<String>,
+
+    /// Flag indicating if EAB is required, None is equivalent to false
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub external_account_required: Option<bool>,
+
+    /// Website with information about the ACME Server
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub website: Option<String>,
+
+    /// List of hostnames used by the CA, intended for the use with caa dns records
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub caa_identities: Option<Vec<String>>,
 }
 
 impl Directory {
@@ -64,6 +76,17 @@ impl Directory {
         }
     }
 
+    /// Get if external account binding is required
+    pub fn external_account_binding_required(&self) -> bool {
+        matches!(
+            &self.data.meta,
+            Some(Meta {
+                external_account_required: Some(true),
+                ..
+            })
+        )
+    }
+
     /// Get the "newNonce" URL. Use `HEAD` requests on this to get a new nonce.
     pub fn new_nonce_url(&self) -> &str {
         &self.data.new_nonce
@@ -78,8 +101,6 @@ impl Directory {
     }
 
     /// Access to the in the Acme spec defined metadata structure.
-    /// Currently only contains the ToS URL already exposed via the `terms_of_service_url()`
-    /// method.
     pub fn meta(&self) -> Option<&Meta> {
         self.data.meta.as_ref()
     }
-- 
2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] applied: [PATCH acme-rs 2/8] add meta fields returned by the directory
  2023-11-14 14:14 ` [pmg-devel] [PATCH acme-rs 2/8] add meta fields returned by the directory Folke Gleumes
@ 2023-12-04 10:56   ` Wolfgang Bumiller
  0 siblings, 0 replies; 15+ messages in thread
From: Wolfgang Bumiller @ 2023-12-04 10:56 UTC (permalink / raw)
  To: Folke Gleumes; +Cc: pmg-devel
applied the acme-rs patches
Note: proxmox-acme-rs.git has now been merged into proxmox.git.
Also, the `-rs` suffix has been dropped from the crate name.
On Tue, Nov 14, 2023 at 03:14:01PM +0100, Folke Gleumes wrote:
> According to the rfc, the meta field contains additional fields that
> weren't covered by the Meta struct. Of the additional fields, only
> external_account_required will be used in the near future, but others
> were added for completeness and the case that they might be used in the
> future.
> 
> Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
> ---
>  src/directory.rs | 25 +++++++++++++++++++++++--
>  1 file changed, 23 insertions(+), 2 deletions(-)
> 
> diff --git a/src/directory.rs b/src/directory.rs
> index 755ea8c..a9d31f2 100644
> --- a/src/directory.rs
> +++ b/src/directory.rs
> @@ -47,6 +47,18 @@ pub struct Meta {
>      /// The terms of service. This is typically in the form of an URL.
>      #[serde(skip_serializing_if = "Option::is_none")]
>      pub terms_of_service: Option<String>,
> +
> +    /// Flag indicating if EAB is required, None is equivalent to false
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub external_account_required: Option<bool>,
> +
> +    /// Website with information about the ACME Server
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub website: Option<String>,
> +
> +    /// List of hostnames used by the CA, intended for the use with caa dns records
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub caa_identities: Option<Vec<String>>,
^ I dropped the `Option<>` there and added `#[serde(default)]` so
deserializing works without it.
^ permalink raw reply	[flat|nested] 15+ messages in thread
* [pmg-devel] [PATCH] expand helper function by eab credentials
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
  2023-11-14 14:14 ` [pmg-devel] [PATCH acme-rs 1/8] add external account binding Folke Gleumes
  2023-11-14 14:14 ` [pmg-devel] [PATCH acme-rs 2/8] add meta fields returned by the directory Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-12-04 10:57   ` [pmg-devel] applied: " Wolfgang Bumiller
  2023-11-14 14:14 ` [pmg-devel] [PATCH backup 3/8] acme: api: add eab options to api Folke Gleumes
                   ` (6 subsequent siblings)
  9 siblings, 1 reply; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
---
 src/client.rs | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/client.rs b/src/client.rs
index 78c83a2..53f2688 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -367,10 +367,14 @@ impl Client {
         contact: Vec<String>,
         tos_agreed: bool,
         rsa_bits: Option<u32>,
+        eab_creds: Option<(String, String)>,
     ) -> Result<&Account, Error> {
-        let account = Account::creator()
+        let mut account = Account::creator()
             .set_contacts(contact)
             .agree_to_tos(tos_agreed);
+        if let Some((eab_kid, eab_hmac_key)) = eab_creds {
+            account = account.set_eab_credentials(eab_kid, eab_hmac_key)?;
+        }
         let account = if let Some(bits) = rsa_bits {
             account.generate_rsa_key(bits)?
         } else {
-- 
2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] [PATCH backup 3/8] acme: api: add eab options to api
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
                   ` (2 preceding siblings ...)
  2023-11-14 14:14 ` [pmg-devel] [PATCH] expand helper function by eab credentials Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-11-14 14:14 ` [pmg-devel] [PATCH backup 4/8] cli: acme: add possibility to set eab via the cli Folke Gleumes
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
Optionally allow for setting external account binding credentials at the
account registration endpoint.
Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
---
 src/acme/client.rs                     |  7 +++++-
 src/api2/config/acme.rs                | 35 +++++++++++++++++++++++---
 src/bin/proxmox_backup_manager/acme.rs | 12 ++++++---
 3 files changed, 47 insertions(+), 7 deletions(-)
diff --git a/src/acme/client.rs b/src/acme/client.rs
index 46566210..1396eb2c 100644
--- a/src/acme/client.rs
+++ b/src/acme/client.rs
@@ -116,6 +116,7 @@ impl AcmeClient {
         tos_agreed: bool,
         contact: Vec<String>,
         rsa_bits: Option<u32>,
+        eab_creds: Option<(String, String)>,
     ) -> Result<&'a Account, anyhow::Error> {
         self.tos = if tos_agreed {
             self.terms_of_service_url().await?.map(str::to_owned)
@@ -123,10 +124,14 @@ impl AcmeClient {
             None
         };
 
-        let account = Account::creator()
+        let mut account = Account::creator()
             .set_contacts(contact)
             .agree_to_tos(tos_agreed);
 
+        if let Some((eab_kid, eab_hmac_key)) = eab_creds {
+            account = account.set_eab_credentials(eab_kid, eab_hmac_key)?;
+        }
+
         let account = if let Some(bits) = rsa_bits {
             account.generate_rsa_key(bits)?
         } else {
diff --git a/src/api2/config/acme.rs b/src/api2/config/acme.rs
index 1954318b..8f010027 100644
--- a/src/api2/config/acme.rs
+++ b/src/api2/config/acme.rs
@@ -182,6 +182,16 @@ fn account_contact_from_string(s: &str) -> Vec<String> {
                 description: "The ACME Directory.",
                 optional: true,
             },
+            eab_kid: {
+                type: String,
+                description: "Key Identifier for External Account Binding.",
+                optional: true,
+            },
+            eab_hmac_key: {
+                type: String,
+                description: "HMAC Key for External Account Binding.",
+                optional: true,
+            }
         },
     },
     access: {
@@ -196,6 +206,8 @@ fn register_account(
     contact: String,
     tos_url: Option<String>,
     directory: Option<String>,
+    eab_kid: Option<String>,
+    eab_hmac_key: Option<String>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<String, Error> {
     let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
@@ -204,6 +216,15 @@ fn register_account(
         AcmeAccountName::from_string_unchecked("default".to_string())
     });
 
+    // TODO: this should be done via the api definition, but
+    // the api macro currently lacks this ability (2023-11-06)
+    if eab_kid.is_some() ^ eab_hmac_key.is_some() {
+        http_bail!(
+            BAD_REQUEST,
+            "either both or none of 'eab_kid' and 'eab_hmac_key' have to be set."
+        );
+    }
+
     if Path::new(&crate::config::acme::account_path(&name)).exists() {
         http_bail!(BAD_REQUEST, "account {} already exists", name);
     }
@@ -224,8 +245,15 @@ fn register_account(
 
             task_log!(worker, "Registering ACME account '{}'...", &name);
 
-            let account =
-                do_register_account(&mut client, &name, tos_url.is_some(), contact, None).await?;
+            let account = do_register_account(
+                &mut client,
+                &name,
+                tos_url.is_some(),
+                contact,
+                None,
+                eab_kid.zip(eab_hmac_key),
+            )
+            .await?;
 
             task_log!(
                 worker,
@@ -244,10 +272,11 @@ pub async fn do_register_account<'a>(
     agree_to_tos: bool,
     contact: String,
     rsa_bits: Option<u32>,
+    eab_creds: Option<(String, String)>,
 ) -> Result<&'a Account, Error> {
     let contact = account_contact_from_string(&contact);
     client
-        .new_account(name, agree_to_tos, contact, rsa_bits)
+        .new_account(name, agree_to_tos, contact, rsa_bits, eab_creds)
         .await
 }
 
diff --git a/src/bin/proxmox_backup_manager/acme.rs b/src/bin/proxmox_backup_manager/acme.rs
index de48a420..17ca5958 100644
--- a/src/bin/proxmox_backup_manager/acme.rs
+++ b/src/bin/proxmox_backup_manager/acme.rs
@@ -156,9 +156,15 @@ async fn register_account(
 
     println!("Attempting to register account with {:?}...", directory);
 
-    let account =
-        api2::config::acme::do_register_account(&mut client, &name, tos_agreed, contact, None)
-            .await?;
+    let account = api2::config::acme::do_register_account(
+        &mut client,
+        &name,
+        tos_agreed,
+        contact,
+        None,
+        None,
+    )
+    .await?;
 
     println!("Registration successful, account URL: {}", account.location);
 
-- 
2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] [PATCH backup 4/8] cli: acme: add possibility to set eab via the cli
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
                   ` (3 preceding siblings ...)
  2023-11-14 14:14 ` [pmg-devel] [PATCH backup 3/8] acme: api: add eab options to api Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-11-14 14:14 ` [pmg-devel] [PATCH perl-rs 5/8] acme: add eab fields for pmg Folke Gleumes
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
If the ca demands external account binding credentials, the user will be
asked for them. If a custom directory is used, the user will be asked if
eab should be used.
Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
---
 src/acme/client.rs                     |  2 +-
 src/bin/proxmox_backup_manager/acme.rs | 51 +++++++++++++++++++++-----
 2 files changed, 43 insertions(+), 10 deletions(-)
diff --git a/src/acme/client.rs b/src/acme/client.rs
index 1396eb2c..6130748b 100644
--- a/src/acme/client.rs
+++ b/src/acme/client.rs
@@ -577,7 +577,7 @@ impl AcmeClient {
         Self::execute(&mut self.http_client, request, &mut self.nonce).await
     }
 
-    async fn directory(&mut self) -> Result<&Directory, Error> {
+    pub async fn directory(&mut self) -> Result<&Directory, Error> {
         Ok(Self::get_directory(
             &mut self.http_client,
             &self.directory_url,
diff --git a/src/bin/proxmox_backup_manager/acme.rs b/src/bin/proxmox_backup_manager/acme.rs
index 17ca5958..f3e62115 100644
--- a/src/bin/proxmox_backup_manager/acme.rs
+++ b/src/bin/proxmox_backup_manager/acme.rs
@@ -103,8 +103,8 @@ async fn register_account(
     contact: String,
     directory: Option<String>,
 ) -> Result<(), Error> {
-    let directory = match directory {
-        Some(directory) => directory,
+    let (directory_url, custom_directory) = match directory {
+        Some(directory) => (directory, true),
         None => {
             println!("Directory endpoints:");
             for (i, dir) in KNOWN_ACME_DIRECTORIES.iter().enumerate() {
@@ -122,12 +122,12 @@ async fn register_account(
 
                 match input.trim().parse::<usize>() {
                     Ok(n) if n < KNOWN_ACME_DIRECTORIES.len() => {
-                        break KNOWN_ACME_DIRECTORIES[n].url.to_owned();
+                        break (KNOWN_ACME_DIRECTORIES[n].url.to_owned(), false);
                     }
                     Ok(n) if n == KNOWN_ACME_DIRECTORIES.len() => {
                         input.clear();
                         std::io::stdin().read_line(&mut input)?;
-                        break input.trim().to_owned();
+                        break (input.trim().to_owned(), true);
                     }
                     _ => eprintln!("Invalid selection."),
                 }
@@ -140,9 +140,13 @@ async fn register_account(
         }
     };
 
-    println!("Attempting to fetch Terms of Service from {:?}", directory);
-    let mut client = AcmeClient::new(directory.clone());
-    let tos_agreed = if let Some(tos_url) = client.terms_of_service_url().await? {
+    println!(
+        "Attempting to fetch Terms of Service from {:?}",
+        directory_url
+    );
+    let mut client = AcmeClient::new(directory_url.clone());
+    let directory = client.directory().await?;
+    let tos_agreed = if let Some(tos_url) = directory.terms_of_service_url() {
         println!("Terms of Service: {}", tos_url);
         print!("Do you agree to the above terms? [y|N]: ");
         std::io::stdout().flush()?;
@@ -154,7 +158,36 @@ async fn register_account(
         true
     };
 
-    println!("Attempting to register account with {:?}...", directory);
+    let mut eab_enabled = directory.external_account_binding_required();
+    if !eab_enabled && custom_directory {
+        print!("Do you want to use external account binding? [y|N]: ");
+        std::io::stdout().flush()?;
+        let mut input = String::new();
+        std::io::stdin().read_line(&mut input)?;
+        eab_enabled = input.trim().eq_ignore_ascii_case("y");
+    } else if eab_enabled {
+        println!("The CA requires external account binding.");
+    }
+
+    let eab_creds = if eab_enabled {
+        println!("You should have received a key id and a key from your CA.");
+
+        print!("Enter EAB key id: ");
+        std::io::stdout().flush()?;
+        let mut eab_kid = String::new();
+        std::io::stdin().read_line(&mut eab_kid)?;
+
+        print!("Enter EAB key: ");
+        std::io::stdout().flush()?;
+        let mut eab_hmac_key = String::new();
+        std::io::stdin().read_line(&mut eab_hmac_key)?;
+
+        Some((eab_kid.trim().to_owned(), eab_hmac_key.trim().to_owned()))
+    } else {
+        None
+    };
+
+    println!("Attempting to register account with {:?}...", directory_url);
 
     let account = api2::config::acme::do_register_account(
         &mut client,
@@ -162,7 +195,7 @@ async fn register_account(
         tos_agreed,
         contact,
         None,
-        None,
+        eab_creds,
     )
     .await?;
 
-- 
2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] [PATCH perl-rs 5/8] acme: add eab fields for pmg
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
                   ` (4 preceding siblings ...)
  2023-11-14 14:14 ` [pmg-devel] [PATCH backup 4/8] cli: acme: add possibility to set eab via the cli Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-12-06 11:37   ` [pmg-devel] applied: " Wolfgang Bumiller
  2023-11-14 14:14 ` [pmg-devel] [PATCH pmg-api 6/8] api: acme: add eab parameters Folke Gleumes
                   ` (3 subsequent siblings)
  9 siblings, 1 reply; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
---
 pmg-rs/src/acme.rs | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/pmg-rs/src/acme.rs b/pmg-rs/src/acme.rs
index b38e1ea..fe1e465 100644
--- a/pmg-rs/src/acme.rs
+++ b/pmg-rs/src/acme.rs
@@ -79,6 +79,7 @@ impl Inner {
         tos_agreed: bool,
         contact: Vec<String>,
         rsa_bits: Option<u32>,
+        eab_creds: Option<(String, String)>,
     ) -> Result<(), Error> {
         self.tos = if tos_agreed {
             self.client.terms_of_service_url()?.map(str::to_owned)
@@ -86,7 +87,9 @@ impl Inner {
             None
         };
 
-        let _account = self.client.new_account(contact, tos_agreed, rsa_bits)?;
+        let _account = self
+            .client
+            .new_account(contact, tos_agreed, rsa_bits, eab_creds)?;
         let file = OpenOptions::new()
             .write(true)
             .create(true)
@@ -238,11 +241,16 @@ pub mod export {
         tos_agreed: bool,
         contact: Vec<String>,
         rsa_bits: Option<u32>,
+        eab_kid: Option<String>,
+        eab_hmac_key: Option<String>,
     ) -> Result<(), Error> {
-        this.inner
-            .lock()
-            .unwrap()
-            .new_account(account_path, tos_agreed, contact, rsa_bits)
+        this.inner.lock().unwrap().new_account(
+            account_path,
+            tos_agreed,
+            contact,
+            rsa_bits,
+            eab_kid.zip(eab_hmac_key),
+        )
     }
 
     /// Get the directory's meta information.
-- 
2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] applied: [PATCH perl-rs 5/8] acme: add eab fields for pmg
  2023-11-14 14:14 ` [pmg-devel] [PATCH perl-rs 5/8] acme: add eab fields for pmg Folke Gleumes
@ 2023-12-06 11:37   ` Wolfgang Bumiller
  0 siblings, 0 replies; 15+ messages in thread
From: Wolfgang Bumiller @ 2023-12-06 11:37 UTC (permalink / raw)
  To: Folke Gleumes; +Cc: pmg-devel
applied
On Tue, Nov 14, 2023 at 03:14:05PM +0100, Folke Gleumes wrote:
> Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
> ---
>  pmg-rs/src/acme.rs | 18 +++++++++++++-----
>  1 file changed, 13 insertions(+), 5 deletions(-)
> 
> diff --git a/pmg-rs/src/acme.rs b/pmg-rs/src/acme.rs
> index b38e1ea..fe1e465 100644
> --- a/pmg-rs/src/acme.rs
> +++ b/pmg-rs/src/acme.rs
> @@ -79,6 +79,7 @@ impl Inner {
>          tos_agreed: bool,
>          contact: Vec<String>,
>          rsa_bits: Option<u32>,
> +        eab_creds: Option<(String, String)>,
>      ) -> Result<(), Error> {
>          self.tos = if tos_agreed {
>              self.client.terms_of_service_url()?.map(str::to_owned)
> @@ -86,7 +87,9 @@ impl Inner {
>              None
>          };
>  
> -        let _account = self.client.new_account(contact, tos_agreed, rsa_bits)?;
> +        let _account = self
> +            .client
> +            .new_account(contact, tos_agreed, rsa_bits, eab_creds)?;
>          let file = OpenOptions::new()
>              .write(true)
>              .create(true)
> @@ -238,11 +241,16 @@ pub mod export {
>          tos_agreed: bool,
>          contact: Vec<String>,
>          rsa_bits: Option<u32>,
> +        eab_kid: Option<String>,
> +        eab_hmac_key: Option<String>,
>      ) -> Result<(), Error> {
> -        this.inner
> -            .lock()
> -            .unwrap()
> -            .new_account(account_path, tos_agreed, contact, rsa_bits)
> +        this.inner.lock().unwrap().new_account(
> +            account_path,
> +            tos_agreed,
> +            contact,
> +            rsa_bits,
> +            eab_kid.zip(eab_hmac_key),
> +        )
>      }
>  
>      /// Get the directory's meta information.
> -- 
> 2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread
* [pmg-devel] [PATCH pmg-api 6/8] api: acme: add eab parameters
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
                   ` (5 preceding siblings ...)
  2023-11-14 14:14 ` [pmg-devel] [PATCH perl-rs 5/8] acme: add eab fields for pmg Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-11-14 14:14 ` [pmg-devel] [PATCH pmg-api 7/8] api: acme: deprecate tos endpoint in favor of new meta endpoint Folke Gleumes
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
---
 src/PMG/API2/ACME.pm | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/PMG/API2/ACME.pm b/src/PMG/API2/ACME.pm
index 42c9f4e..9e3eb8d 100644
--- a/src/PMG/API2/ACME.pm
+++ b/src/PMG/API2/ACME.pm
@@ -132,6 +132,18 @@ __PACKAGE__->register_method ({
 		default => $acme_default_directory_url,
 		optional => 1,
 	    }),
+	    'eab-kid' => {
+		type => 'string',
+		description => 'Key Identifier for External Account Binding.',
+		requires => 'eab-hmac-key',
+		optional => 1,
+	    },
+	    'eab-hmac-key' => {
+		type => 'string',
+		description => 'HMAC key for External Account Binding.',
+		requires => 'eab-kid',
+		optional => 1,
+	    },
 	},
     },
     returns => {
@@ -151,6 +163,8 @@ __PACKAGE__->register_method ({
 
 	my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
 	my $contact = $account_contact_from_param->($param);
+	my $eab_kid = extract_param($param, 'eab-kid');
+	my $eab_hmac_key = extract_param($param, 'eab-hmac-key');
 
 	my $realcmd = sub {
 	    PMG::CertHelpers::lock_acme($account_name, 10, sub {
@@ -160,7 +174,7 @@ __PACKAGE__->register_method ({
 		print "Registering new ACME account..\n";
 		my $acme = PMG::RS::Acme->new($directory);
 		eval {
-		    $acme->new_account($account_file, defined($param->{tos_url}), $contact, undef);
+		    $acme->new_account($account_file, defined($param->{tos_url}), $contact, undef, $eab_kid, $eab_hmac_key);
 		};
 		if (my $err = $@) {
 		    unlink $account_file;
-- 
2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] [PATCH pmg-api 7/8] api: acme: deprecate tos endpoint in favor of new meta endpoint
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
                   ` (6 preceding siblings ...)
  2023-11-14 14:14 ` [pmg-devel] [PATCH pmg-api 6/8] api: acme: add eab parameters Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-11-14 14:14 ` [pmg-devel] [PATCH pmg-api 8/8] cli: acme: expose acme eab options on the cli Folke Gleumes
  2023-12-06 11:59 ` [pmg-devel] applied-series: [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Wolfgang Bumiller
  9 siblings, 0 replies; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
The ToS endpoint ignored data that is needed to detect if EAB needs to
be used. Instead of adding a new endpoint that does the same request,
the tos endpoint is deprecated and replaced by the meta endpoint,
that returns all information returned by the directory.
Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
---
 src/PMG/API2/ACME.pm | 59 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 58 insertions(+), 1 deletion(-)
diff --git a/src/PMG/API2/ACME.pm b/src/PMG/API2/ACME.pm
index 9e3eb8d..8795c33 100644
--- a/src/PMG/API2/ACME.pm
+++ b/src/PMG/API2/ACME.pm
@@ -67,6 +67,7 @@ __PACKAGE__->register_method ({
 	return [
 	    { name => 'account' },
 	    { name => 'tos' },
+	    { name => 'meta' },
 	    { name => 'directories' },
 	    { name => 'plugins' },
 	    { name => 'challenge-schema' },
@@ -353,11 +354,12 @@ __PACKAGE__->register_method ({
 	return $update_account->($param, 'deactivate', $force_deactivate, status => 'deactivated');
     }});
 
+# TODO: deprecated, remove with pmg 9
 __PACKAGE__->register_method ({
     name => 'get_tos',
     path => 'tos',
     method => 'GET',
-    description => "Retrieve ACME TermsOfService URL from CA.",
+    description => "Retrieve ACME TermsOfService URL from CA. Deprecated, please use /config/acme/meta.",
     permissions => { user => 'all' },
     parameters => {
 	additionalProperties => 0,
@@ -384,6 +386,61 @@ __PACKAGE__->register_method ({
 	return $meta ? $meta->{termsOfService} : undef;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'get_meta',
+    path => 'meta',
+    method => 'GET',
+    description => "Retrieve ACME Directory Meta Information",
+    permissions => { user => 'all' },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    directory => get_standard_option('pmg-acme-directory-url', {
+		default => $acme_default_directory_url,
+		optional => 1,
+	    }),
+	},
+    },
+    returns => {
+	type => 'object',
+	additionalProperties => 1,
+	properties => {
+	    termsOfService => {
+		description => 'ACME TermsOfService URL.',
+		type => 'string',
+		optional => 1,
+	    },
+	    externalAccountRequired => {
+		description => 'EAB Required',
+		type => 'boolean',
+		optional => 1,
+	    },
+	    website => {
+		description => 'URL to more information about the ACME server.',
+		type => 'string',
+		optional => 1,
+	    },
+	    caaIdentities => {
+		description => 'Hostnames referring to the ACME servers.',
+		type => 'array',
+		items => {
+		    type => 'string',
+		},
+		optional => 1,
+	    },
+	},
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
+
+	my $acme = PVE::ACME->new(undef, $directory);
+	my $meta = $acme->get_meta();
+
+	return $meta;
+    }});
+
 __PACKAGE__->register_method ({
     name => 'get_directories',
     path => 'directories',
-- 
2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] [PATCH pmg-api 8/8] cli: acme: expose acme eab options on the cli
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
                   ` (7 preceding siblings ...)
  2023-11-14 14:14 ` [pmg-devel] [PATCH pmg-api 7/8] api: acme: deprecate tos endpoint in favor of new meta endpoint Folke Gleumes
@ 2023-11-14 14:14 ` Folke Gleumes
  2023-12-06 11:41   ` [pmg-devel] applied: " Wolfgang Bumiller
  2023-12-06 11:59 ` [pmg-devel] applied-series: [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Wolfgang Bumiller
  9 siblings, 1 reply; 15+ messages in thread
From: Folke Gleumes @ 2023-11-14 14:14 UTC (permalink / raw)
  To: pmg-devel
interactively ask for external account binding credentials if either:
* the ca requests it
* a custom ca is used
Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
---
 src/PMG/CLI/pmgconfig.pm | 29 ++++++++++++++++++++++++++---
 1 file changed, 26 insertions(+), 3 deletions(-)
diff --git a/src/PMG/CLI/pmgconfig.pm b/src/PMG/CLI/pmgconfig.pm
index cc036fa..2a7a7a1 100644
--- a/src/PMG/CLI/pmgconfig.pm
+++ b/src/PMG/CLI/pmgconfig.pm
@@ -244,6 +244,7 @@ __PACKAGE__->register_method({
     code => sub {
 	my ($param) = @_;
 
+	my $custom_directory = 1;
 	if (!$param->{directory}) {
 	    my $directories = PMG::API2::ACME->get_directories({});
 	    print "Directory endpoints:\n";
@@ -264,6 +265,7 @@ __PACKAGE__->register_method({
 			return;
 		    } elsif ($selection < $i && $selection >= 0) {
 			$param->{directory} = $directories->[$selection]->{url};
+			$custom_directory = 0;
 			return;
 		    }
 		}
@@ -277,11 +279,13 @@ __PACKAGE__->register_method({
 		$attempts++;
 	    }
 	}
+
 	print "\nAttempting to fetch Terms of Service from '$param->{directory}'..\n";
-	my $tos = PMG::API2::ACME->get_tos({ directory => $param->{directory} });
-	if ($tos) {
+	my $meta = PMG::API2::ACME->get_meta({ directory => $param->{directory} });
+	if ($meta->{termsOfService}) {
+	    my $tos = $meta->{termsOfService};
 	    print "Terms of Service: $tos\n";
-	    my $term = Term::ReadLine->new('pvenode');
+	    my $term = Term::ReadLine->new('pmgconfig');
 	    my $agreed = $term->readline('Do you agree to the above terms? [y|N]: ');
 	    die "Cannot continue without agreeing to ToS, aborting.\n"
 		if ($agreed !~ /^y$/i);
@@ -290,6 +294,25 @@ __PACKAGE__->register_method({
 	} else {
 	    print "No Terms of Service found, proceeding.\n";
 	}
+
+	my $eab_enabled = $meta->{externalAccountRequired};
+	if (!$eab_enabled && $custom_directory) {
+	    my $term = Term::ReadLine->new('pmgconfig');
+	    my $agreed = $term->readline('Do you want to use external account binding? [y|N]: ');
+	    $eab_enabled = ($agreed =~ /^y$/i);
+	} elsif ($eab_enabled) {
+	    print "The CA requires external account binding.\n";
+	}
+	if ($eab_enabled) {
+	    print "You should have received a key id and a key from your CA.\n";
+	    my $term = Term::ReadLine->new('pmgconfig');
+	    my $eab_kid = $term->readline('Enter EAB key id: ');
+	    my $eab_hmac_key = $term->readline('Enter EAB key: ');
+
+	    $param->{'eab-kid'} = $eab_kid;
+	    $param->{'eab-hmac-key'} = $eab_hmac_key;
+	}
+
 	print "\nAttempting to register account with '$param->{directory}'..\n";
 
 	$upid_exit->(PMG::API2::ACME->register_account($param));
-- 
2.39.2
^ permalink raw reply	[flat|nested] 15+ messages in thread* [pmg-devel] applied-series: [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs
  2023-11-14 14:13 [pmg-devel] [PATCH acme-rs/backup/perl-rs/pmg-api 0/8] add external account binding to pmg and pbs Folke Gleumes
                   ` (8 preceding siblings ...)
  2023-11-14 14:14 ` [pmg-devel] [PATCH pmg-api 8/8] cli: acme: expose acme eab options on the cli Folke Gleumes
@ 2023-12-06 11:59 ` Wolfgang Bumiller
  9 siblings, 0 replies; 15+ messages in thread
From: Wolfgang Bumiller @ 2023-12-06 11:59 UTC (permalink / raw)
  To: Folke Gleumes; +Cc: pmg-devel
applied the remaining patches, thanks
^ permalink raw reply	[flat|nested] 15+ messages in thread