From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 938701FF141 for ; Mon, 13 Apr 2026 10:45:23 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7FCE11C58C; Mon, 13 Apr 2026 10:46:11 +0200 (CEST) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Mon, 13 Apr 2026 10:45:32 +0200 Message-Id: Subject: Re: [RFC proxmox 2/6] proxmox-subscription: add new machine-id based serverid To: =?utf-8?q?Fabian_Gr=C3=BCnbichler?= , X-Mailer: aerc 0.20.0 References: <20260410100326.3199377-1-f.gruenbichler@proxmox.com> <20260410100326.3199377-3-f.gruenbichler@proxmox.com> In-Reply-To: <20260410100326.3199377-3-f.gruenbichler@proxmox.com> From: "Shannon Sterz" X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1776069858781 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.123 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 RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [serverid.id,lib.rs] Message-ID-Hash: 4NLLH5MXXFIYPUWPDGGDKPDGNNLCWTTN X-Message-ID-Hash: 4NLLH5MXXFIYPUWPDGGDKPDGNNLCWTTN X-MailFrom: s.sterz@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: On Fri Apr 10, 2026 at 12:02 PM CEST, Fabian Gr=C3=BCnbichler wrote: > and adapt the code to allow querying all possible serverids, and acceptin= g the > existing one if it matches one of the candidates. > > Signed-off-by: Fabian Gr=C3=BCnbichler > --- > > 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 =3D { workspace =3D true, optional =3D= true } > proxmox-http =3D { workspace =3D true, optional =3D true, features =3D [= "client-trait", "http-helpers"] } > proxmox-serde.workspace =3D true > proxmox-sys =3D { workspace =3D true, optional =3D true } > +proxmox-systemd =3D { workspace =3D true, optional =3D true } > proxmox-time =3D { workspace =3D true, optional =3D true } > > proxmox-schema =3D { workspace =3D true, features =3D ["api-macro"], opt= ional =3D true } > > [features] > default =3D ["impl"] > -impl =3D [ "dep:proxmox-base64", "dep:hex", "dep:openssl", "dep:proxmox-= http", "dep:proxmox-sys", "dep:proxmox-time"] > +impl =3D [ "dep:proxmox-base64", "dep:hex", "dep:openssl", "dep:proxmox-= http", "dep:proxmox-sys", "dep:proxmox-systemd", "dep:proxmox-time"] > api-types =3D ["dep:proxmox-schema"] > diff --git a/proxmox-subscription/debian/control b/proxmox-subscription/d= ebian/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 (>=3D 2.1.0-~~) , > librust-regex-1+default-dev (>=3D 1.5-~~) , > librust-serde-1+default-dev , > @@ -78,6 +79,7 @@ Depends: > librust-proxmox-http-1+default-dev (>=3D 1.0.5-~~), > librust-proxmox-http-1+http-helpers-dev (>=3D 1.0.5-~~), > librust-proxmox-sys-1+default-dev, > + librust-proxmox-systemd-1+default-dev, > librust-proxmox-time-2+default-dev (>=3D 2.1.0-~~) > Provides: > librust-proxmox-subscription+default-dev (=3D ${binary:Version}), > diff --git a/proxmox-subscription/src/lib.rs b/proxmox-subscription/src/l= ib.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 =3D "impl")] > pub use subscription_info::{ > - get_hardware_address, ProductType, SubscriptionInfo, SubscriptionSta= tus, > + get_hardware_address_candidates, ProductType, ServerId, Subscription= Info, SubscriptionStatus, > }; > > #[cfg(not(feature =3D "impl"))] > diff --git a/proxmox-subscription/src/subscription_info.rs b/proxmox-subs= cription/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 =3D match self { > + ServerIdType::SshMd5 =3D> "SSH MD5", > + ServerIdType::MachineId =3D> "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 =3D "api-types", api())] > #[cfg_attr(feature =3D "api-types", derive(Updater))] > #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, Part= ialOrd, Ord)] > @@ -133,7 +170,7 @@ pub struct SubscriptionInfo { > } > > #[cfg(feature =3D "impl")] > -pub use _impl::get_hardware_address; > +pub use _impl::get_hardware_address_candidates; > > #[cfg(feature =3D "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] =3D > + 0x0e6b456a63fe4892997e9f42ebfaf980_u128.to_le_bytes(); > + > pub(crate) const SHARED_KEY_DATA: &str =3D "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 `messag= e` 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_candidat= es()) { > (_, Err(err)) =3D> { > self.status =3D SubscriptionStatus::Invalid; > self.message =3D Some(format!("Failed to obtain serv= er ID - {err}.")); > @@ -256,7 +297,9 @@ mod _impl { > self.message =3D Some("Missing server ID.".to_string= ()); > self.signature =3D None; > } > - (Some(contained), Ok(expected)) if &expected !=3D contai= ned =3D> { > + (Some(contained), Ok(expected)) > + if !expected.iter().any(|serverid| serverid.id =3D= =3D *contained) =3D> > + { > self.status =3D SubscriptionStatus::Invalid; > self.message =3D Some("Server ID mismatch.".to_strin= g()); > self.signature =3D 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 =3D "/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 =3D match ty { > + crate::subscription_info::ServerIdType::SshMd5 =3D> { > + let digest =3D md5sum(&get_ssh_key()?) > + .map_err(|e| format_err!("Error digesting host key -= {}", e))?; nit: could inline these errors here. > + > + hex::encode(digest).to_uppercase() > + } > + crate::subscription_info::ServerIdType::MachineId =3D> { > + let machine_id =3D > + proxmox_systemd::sd_id128::get_app_specific_id(PMX_A= PPLICATION_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 =3D "/etc/ssh/ssh_host_rsa_key.pub"; > - > - let contents =3D proxmox_sys::fs::file_get_contents(FILENAME) > - .map_err(|e| format_err!("Error getting host key - {}", e))?= ; > - let digest =3D > - md5sum(&contents).map_err(|e| format_err!("Error digesting h= ost key - {}", e))?; > - > - Ok(hex::encode(digest).to_uppercase()) > + pub fn get_hardware_address_candidates() -> Result, Error> { > + let mut res =3D Vec::new(); > + let mut errors =3D Vec::new(); > + let variants =3D [super::ServerIdType::MachineId, super::ServerI= dType::SshMd5]; > + for ty in variants { > + match get_hardware_address(ty) { > + Ok(id) =3D> res.push(id), > + Err(err) =3D> errors.push((ty, err)), > + } > + } > + if res.is_empty() { > + let error_strings: Vec =3D errors > + .into_iter() > + .map(|(ty, err)| format!("{ty}: {err}")) > + .collect(); > + let msg =3D 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 {