all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [RFC proxmox 2/6] proxmox-subscription: add new machine-id based serverid
Date: Fri, 10 Apr 2026 12:02:19 +0200	[thread overview]
Message-ID: <20260410100326.3199377-3-f.gruenbichler@proxmox.com> (raw)
In-Reply-To: <20260410100326.3199377-1-f.gruenbichler@proxmox.com>

and adapt the code to allow querying all possible serverids, and accepting the
existing one if it matches one of the candidates.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---

Notes:
    requires bumped librust-proxmox-systemd-dev

 proxmox-subscription/Cargo.toml               |   3 +-
 proxmox-subscription/debian/control           |   2 +
 proxmox-subscription/src/lib.rs               |   2 +-
 proxmox-subscription/src/subscription_info.rs | 105 ++++++++++++++++--
 4 files changed, 98 insertions(+), 14 deletions(-)

diff --git a/proxmox-subscription/Cargo.toml b/proxmox-subscription/Cargo.toml
index dda31a69..4db7ca71 100644
--- a/proxmox-subscription/Cargo.toml
+++ b/proxmox-subscription/Cargo.toml
@@ -23,11 +23,12 @@ proxmox-base64 = { workspace = true, optional = true }
 proxmox-http = { workspace = true, optional = true, features = ["client-trait", "http-helpers"] }
 proxmox-serde.workspace = true
 proxmox-sys = { workspace = true, optional = true }
+proxmox-systemd = { workspace = true, optional = true }
 proxmox-time = { workspace = true, optional = true }
 
 proxmox-schema = { workspace = true, features = ["api-macro"], optional = true }
 
 [features]
 default = ["impl"]
-impl = [ "dep:proxmox-base64", "dep:hex", "dep:openssl", "dep:proxmox-http", "dep:proxmox-sys", "dep:proxmox-time"]
+impl = [ "dep:proxmox-base64", "dep:hex", "dep:openssl", "dep:proxmox-http", "dep:proxmox-sys", "dep:proxmox-systemd", "dep:proxmox-time"]
 api-types = ["dep:proxmox-schema"]
diff --git a/proxmox-subscription/debian/control b/proxmox-subscription/debian/control
index 5fbfcccb..5584d0c1 100644
--- a/proxmox-subscription/debian/control
+++ b/proxmox-subscription/debian/control
@@ -16,6 +16,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
  librust-proxmox-serde-1+default-dev <!nocheck>,
  librust-proxmox-serde-1+serde-json-dev <!nocheck>,
  librust-proxmox-sys-1+default-dev <!nocheck>,
+ librust-proxmox-systemd-1+default-dev <!nocheck>,
  librust-proxmox-time-2+default-dev (>= 2.1.0-~~) <!nocheck>,
  librust-regex-1+default-dev (>= 1.5-~~) <!nocheck>,
  librust-serde-1+default-dev <!nocheck>,
@@ -78,6 +79,7 @@ Depends:
  librust-proxmox-http-1+default-dev (>= 1.0.5-~~),
  librust-proxmox-http-1+http-helpers-dev (>= 1.0.5-~~),
  librust-proxmox-sys-1+default-dev,
+ librust-proxmox-systemd-1+default-dev,
  librust-proxmox-time-2+default-dev (>= 2.1.0-~~)
 Provides:
  librust-proxmox-subscription+default-dev (= ${binary:Version}),
diff --git a/proxmox-subscription/src/lib.rs b/proxmox-subscription/src/lib.rs
index 2ed96903..eb1573e6 100644
--- a/proxmox-subscription/src/lib.rs
+++ b/proxmox-subscription/src/lib.rs
@@ -3,7 +3,7 @@
 mod subscription_info;
 #[cfg(feature = "impl")]
 pub use subscription_info::{
-    get_hardware_address, ProductType, SubscriptionInfo, SubscriptionStatus,
+    get_hardware_address_candidates, ProductType, ServerId, SubscriptionInfo, SubscriptionStatus,
 };
 
 #[cfg(not(feature = "impl"))]
diff --git a/proxmox-subscription/src/subscription_info.rs b/proxmox-subscription/src/subscription_info.rs
index f53b3ce3..f0daa51f 100644
--- a/proxmox-subscription/src/subscription_info.rs
+++ b/proxmox-subscription/src/subscription_info.rs
@@ -47,6 +47,43 @@ impl std::fmt::Display for SubscriptionStatus {
     }
 }
 
+/// Variant discriminator for `ServerId`
+#[derive(Clone, Copy, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ServerIdType {
+    /// Legacy variant tied to SSH host key, for backwards compatibility
+    SshMd5,
+    /// Tied to /etc/machine-id
+    MachineId,
+}
+
+impl Display for ServerIdType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let txt = match self {
+            ServerIdType::SshMd5 => "SSH MD5",
+            ServerIdType::MachineId => "machine-id",
+        };
+        f.write_str(txt)
+    }
+}
+
+/// Serverid used to bind subscription key to system
+pub struct ServerId {
+    ty: ServerIdType,
+    id: String,
+}
+
+impl ServerId {
+    pub fn kind(&self) -> ServerIdType {
+        self.ty
+    }
+}
+
+impl Display for ServerId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(&self.id)
+    }
+}
+
 #[cfg_attr(feature = "api-types", api())]
 #[cfg_attr(feature = "api-types", derive(Updater))]
 #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
@@ -133,7 +170,7 @@ pub struct SubscriptionInfo {
 }
 
 #[cfg(feature = "impl")]
-pub use _impl::get_hardware_address;
+pub use _impl::get_hardware_address_candidates;
 
 #[cfg(feature = "impl")]
 pub(crate) use _impl::{md5sum, SHARED_KEY_DATA};
@@ -151,6 +188,10 @@ mod _impl {
 
     use crate::sign::Verifier;
 
+    // Generated using `systemd-sd128 new`
+    pub(crate) const PMX_APPLICATION_ID: [u8; 16] =
+        0x0e6b456a63fe4892997e9f42ebfaf980_u128.to_le_bytes();
+
     pub(crate) const SHARED_KEY_DATA: &str = "kjfdlskfhiuewhfk947368";
 
     /// How long the local key is valid for in between remote checks
@@ -245,7 +286,7 @@ mod _impl {
         /// `status` is set to [SubscriptionStatus::Invalid] and `message` to a human-readable
         ///  message in case it does not.
         pub fn check_server_id(&mut self) {
-            match (self.serverid.as_ref(), get_hardware_address()) {
+            match (self.serverid.as_ref(), get_hardware_address_candidates()) {
                 (_, Err(err)) => {
                     self.status = SubscriptionStatus::Invalid;
                     self.message = Some(format!("Failed to obtain server ID - {err}."));
@@ -256,7 +297,9 @@ mod _impl {
                     self.message = Some("Missing server ID.".to_string());
                     self.signature = None;
                 }
-                (Some(contained), Ok(expected)) if &expected != contained => {
+                (Some(contained), Ok(expected))
+                    if !expected.iter().any(|serverid| serverid.id == *contained) =>
+                {
                     self.status = SubscriptionStatus::Invalid;
                     self.message = Some("Server ID mismatch.".to_string());
                     self.signature = None;
@@ -316,16 +359,54 @@ mod _impl {
         hash(MessageDigest::md5(), data).map_err(Error::from)
     }
 
+    fn get_hardware_address(ty: super::ServerIdType) -> Result<super::ServerId, Error> {
+        fn get_ssh_key() -> Result<Vec<u8>, Error> {
+            static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub";
+
+            proxmox_sys::fs::file_get_contents(FILENAME)
+                .map_err(|e| format_err!("Error getting host key - {}", e))
+        }
+
+        let id = match ty {
+            crate::subscription_info::ServerIdType::SshMd5 => {
+                let digest = md5sum(&get_ssh_key()?)
+                    .map_err(|e| format_err!("Error digesting host key - {}", e))?;
+
+                hex::encode(digest).to_uppercase()
+            }
+            crate::subscription_info::ServerIdType::MachineId => {
+                let machine_id =
+                    proxmox_systemd::sd_id128::get_app_specific_id(PMX_APPLICATION_ID)?;
+                hex::encode(machine_id).to_uppercase()
+            }
+        };
+        Ok(super::ServerId { ty, id })
+    }
+
     /// Generate the current system's "server ID".
-    pub fn get_hardware_address() -> Result<String, Error> {
-        static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub";
-
-        let contents = proxmox_sys::fs::file_get_contents(FILENAME)
-            .map_err(|e| format_err!("Error getting host key - {}", e))?;
-        let digest =
-            md5sum(&contents).map_err(|e| format_err!("Error digesting host key - {}", e))?;
-
-        Ok(hex::encode(digest).to_uppercase())
+    pub fn get_hardware_address_candidates() -> Result<Vec<super::ServerId>, Error> {
+        let mut res = Vec::new();
+        let mut errors = Vec::new();
+        let variants = [super::ServerIdType::MachineId, super::ServerIdType::SshMd5];
+        for ty in variants {
+            match get_hardware_address(ty) {
+                Ok(id) => res.push(id),
+                Err(err) => errors.push((ty, err)),
+            }
+        }
+        if res.is_empty() {
+            let error_strings: Vec<String> = errors
+                .into_iter()
+                .map(|(ty, err)| format!("{ty}: {err}"))
+                .collect();
+            let msg = if error_strings.is_empty() {
+                "unknown error".to_string()
+            } else {
+                error_strings.join(", ")
+            };
+            bail!("Failed to get any hardware address candidate: {msg}",);
+        }
+        Ok(res)
     }
 
     fn parse_next_due(value: &str) -> Result<i64, Error> {
-- 
2.47.3





  parent reply	other threads:[~2026-04-10 10:03 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-10 10:02 [RFC manager/proxmox{,-backup,-perl-rs} 0/6] adapt subscription handling to alternative server IDs Fabian Grünbichler
2026-04-10 10:02 ` [PATCH proxmox 1/6] systemd: add support for machine-id generation Fabian Grünbichler
2026-04-10 10:02 ` Fabian Grünbichler [this message]
2026-04-10 10:02 ` [RFC proxmox-backup 3/6] subscription: adapt to multiple server ID variants Fabian Grünbichler
2026-04-10 10:02 ` [RFC proxmox-perl-rs 4/6] common: subscription: expose server ID candidates Fabian Grünbichler
2026-04-10 10:02 ` [RFC manager 5/6] subscription: adapt to multiple server ID variants Fabian Grünbichler
2026-04-10 10:02 ` [RFC manager 6/6] api2tools: remove unused get_hwaddress Fabian Grünbichler

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260410100326.3199377-3-f.gruenbichler@proxmox.com \
    --to=f.gruenbichler@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal