From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id B2BC31FF292 for ; Fri, 10 Apr 2026 12:03:08 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 2C15F19CDC; Fri, 10 Apr 2026 12:03:36 +0200 (CEST) From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= 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 Message-ID: <20260410100326.3199377-3-f.gruenbichler@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260410100326.3199377-1-f.gruenbichler@proxmox.com> References: <20260410100326.3199377-1-f.gruenbichler@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1775815339682 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.054 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 Message-ID-Hash: XZOHTVWIYZ6OS5YMSOXTAGXZJG5RD6UP X-Message-ID-Hash: XZOHTVWIYZ6OS5YMSOXTAGXZJG5RD6UP X-MailFrom: f.gruenbichler@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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 , librust-proxmox-serde-1+default-dev , librust-proxmox-serde-1+serde-json-dev , librust-proxmox-sys-1+default-dev , + librust-proxmox-systemd-1+default-dev , librust-proxmox-time-2+default-dev (>= 2.1.0-~~) , librust-regex-1+default-dev (>= 1.5-~~) , librust-serde-1+default-dev , @@ -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 { + fn get_ssh_key() -> Result, 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 { - 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, 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 = 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 { -- 2.47.3