From: Nicolas Frey <n.frey@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Thomas Lamprecht <t.lamprech@proxmox.com>
Subject: [pve-devel] [PATCH proxmox v5 1/4] add proxmox-pgp subcrate, move POM verifier code to it
Date: Thu, 23 Oct 2025 12:39:50 +0200 [thread overview]
Message-ID: <20251023103953.305810-2-n.frey@proxmox.com> (raw)
In-Reply-To: <20251023103953.305810-1-n.frey@proxmox.com>
Add new micro-crate `proxmox-pgp` for use in `proxmox-apt` to verify
repository signatures. Code is derived from `proxmox-offline-mirror`.
If this patch is applied, then `proxmox-offline-mirror` should use
the `proxmox-pgp` crate.
For the original development history, see commits related to [0].
[0] https://git.proxmox.com/?p=proxmox-offline-mirror.git;a=blob;f=src/helpers/verifier.rs;h=15195a6ec3413ad3c016fffc38f9eee3cefc6827;hb=cd56fab
Suggested-by: Thomas Lamprecht <t.lamprech@proxmox.com>
Signed-off-by: Nicolas Frey <n.frey@proxmox.com>
---
Cargo.toml | 2 +
proxmox-pgp/Cargo.toml | 17 +++
proxmox-pgp/debian/changelog | 5 +
proxmox-pgp/debian/control | 40 +++++++
proxmox-pgp/debian/copyright | 18 +++
proxmox-pgp/debian/debcargo.toml | 7 ++
proxmox-pgp/src/lib.rs | 5 +
proxmox-pgp/src/verifier.rs | 200 +++++++++++++++++++++++++++++++
8 files changed, 294 insertions(+)
create mode 100644 proxmox-pgp/Cargo.toml
create mode 100644 proxmox-pgp/debian/changelog
create mode 100644 proxmox-pgp/debian/control
create mode 100644 proxmox-pgp/debian/copyright
create mode 100644 proxmox-pgp/debian/debcargo.toml
create mode 100644 proxmox-pgp/src/lib.rs
create mode 100644 proxmox-pgp/src/verifier.rs
diff --git a/Cargo.toml b/Cargo.toml
index 8091bf70..7d35c9fd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,6 +28,7 @@ members = [
"proxmox-network-types",
"proxmox-notify",
"proxmox-openid",
+ "proxmox-pgp",
"proxmox-product-config",
"proxmox-resource-scheduling",
"proxmox-rest-server",
@@ -154,6 +155,7 @@ proxmox-lang = { version = "1.5", path = "proxmox-lang" }
proxmox-log = { version = "1.0.0", path = "proxmox-log" }
proxmox-login = { version = "1.0.0", path = "proxmox-login" }
proxmox-network-types = { version = "0.1.0", path = "proxmox-network-types" }
+proxmox-pgp = { version = "1.0.0", path = "proxmox-pgp" }
proxmox-product-config = { version = "1.0.0", path = "proxmox-product-config" }
proxmox-config-digest = { version = "1.0.0", path = "proxmox-config-digest" }
proxmox-rest-server = { version = "1.0.0", path = "proxmox-rest-server" }
diff --git a/proxmox-pgp/Cargo.toml b/proxmox-pgp/Cargo.toml
new file mode 100644
index 00000000..66cac444
--- /dev/null
+++ b/proxmox-pgp/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "proxmox-pgp"
+version = "1.0.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+exclude.workspace = true
+description = "Proxmox wrapper for verifying pgp signatures using sequoia-openpgp"
+
+[dependencies]
+anyhow.workspace = true
+serde.workspace = true
+sequoia-openpgp = "2"
+
+proxmox-api-macro.workspace = true
+proxmox-schema.workspace = true
diff --git a/proxmox-pgp/debian/changelog b/proxmox-pgp/debian/changelog
new file mode 100644
index 00000000..3a5a947c
--- /dev/null
+++ b/proxmox-pgp/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-pgp (1.0.0-1) stable; urgency=medium
+
+ * initial split out of proxmox offline mirror.
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 22 Oct 2025 14:06:52 +0200
diff --git a/proxmox-pgp/debian/control b/proxmox-pgp/debian/control
new file mode 100644
index 00000000..23412bab
--- /dev/null
+++ b/proxmox-pgp/debian/control
@@ -0,0 +1,40 @@
+Source: rust-proxmox-pgp
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-proxmox-api-macro-1+default-dev (>= 1.4.1-~~) <!nocheck>,
+ librust-proxmox-schema-5+default-dev <!nocheck>,
+ librust-sequoia-openpgp-2+default-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://git.proxmox.com/?p=proxmox.git
+X-Cargo-Crate: proxmox-pgp
+
+Package: librust-proxmox-pgp-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-proxmox-api-macro-1+default-dev (>= 1.4.1-~~),
+ librust-proxmox-schema-5+default-dev,
+ librust-sequoia-openpgp-2+default-dev,
+ librust-serde-1+default-dev
+Provides:
+ librust-proxmox-pgp+default-dev (= ${binary:Version}),
+ librust-proxmox-pgp-1-dev (= ${binary:Version}),
+ librust-proxmox-pgp-1+default-dev (= ${binary:Version}),
+ librust-proxmox-pgp-1.0-dev (= ${binary:Version}),
+ librust-proxmox-pgp-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-pgp-1.0.0-dev (= ${binary:Version}),
+ librust-proxmox-pgp-1.0.0+default-dev (= ${binary:Version})
+Description: Proxmox wrapper for verifying pgp signatures using sequoia-openpgp - Rust source code
+ Source code for Debianized Rust crate "proxmox-pgp"
diff --git a/proxmox-pgp/debian/copyright b/proxmox-pgp/debian/copyright
new file mode 100644
index 00000000..d6e3c304
--- /dev/null
+++ b/proxmox-pgp/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-pgp/debian/debcargo.toml b/proxmox-pgp/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-pgp/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-pgp/src/lib.rs b/proxmox-pgp/src/lib.rs
new file mode 100644
index 00000000..08a873fd
--- /dev/null
+++ b/proxmox-pgp/src/lib.rs
@@ -0,0 +1,5 @@
+#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
+
+mod verifier;
+
+pub use verifier::{verify_signature, WeakCryptoConfig, WeakCryptoConfigUpdater};
diff --git a/proxmox-pgp/src/verifier.rs b/proxmox-pgp/src/verifier.rs
new file mode 100644
index 00000000..84a15c4c
--- /dev/null
+++ b/proxmox-pgp/src/verifier.rs
@@ -0,0 +1,200 @@
+use anyhow::{bail, format_err, Error};
+
+use proxmox_api_macro::api;
+use proxmox_schema::Updater;
+use sequoia_openpgp::{
+ cert::CertParser,
+ parse::{
+ stream::{
+ DetachedVerifierBuilder, MessageLayer, MessageStructure, VerificationError,
+ VerificationHelper, VerifierBuilder,
+ },
+ PacketParser, PacketParserResult, Parse,
+ },
+ policy::StandardPolicy,
+ types::HashAlgorithm,
+ Cert, KeyHandle,
+};
+use serde::{Deserialize, Serialize};
+use std::io;
+
+#[api(
+ properties: {
+ "allow-sha1": {
+ type: bool,
+ default: false,
+ optional: true,
+ },
+ "min-dsa-key-size": {
+ type: u64,
+ optional: true,
+ },
+ "min-rsa-key-size": {
+ type: u64,
+ optional: true,
+ },
+ },
+)]
+#[derive(Default, Serialize, Deserialize, Updater, Clone, Debug)]
+#[serde(rename_all = "kebab-case")]
+/// Weak Cryptography Configuration
+pub struct WeakCryptoConfig {
+ /// Whether to allow SHA-1 based signatures
+ #[serde(default)]
+ pub allow_sha1: bool,
+ /// Whether to lower the key size cutoff for DSA-based signatures
+ #[serde(default)]
+ pub min_dsa_key_size: Option<u64>,
+ /// Whether to lower the key size cutoff for RSA-based signatures
+ #[serde(default)]
+ pub min_rsa_key_size: Option<u64>,
+}
+
+struct Helper<'a> {
+ cert: &'a Cert,
+}
+
+impl VerificationHelper for Helper<'_> {
+ fn get_certs(&mut self, _ids: &[KeyHandle]) -> sequoia_openpgp::Result<Vec<Cert>> {
+ // Return public keys for signature verification here.
+ Ok(vec![self.cert.clone()])
+ }
+
+ fn check(&mut self, structure: MessageStructure) -> sequoia_openpgp::Result<()> {
+ // In this function, we implement our signature verification policy.
+
+ let mut good = false;
+
+ // we don't want compression and/or encryption
+ let layers: Vec<_> = structure.iter().collect();
+ if layers.len() > 1 || layers.is_empty() {
+ bail!(
+ "unexpected GPG message structure - expected plain signed data, got {} layers!",
+ layers.len()
+ );
+ }
+ let layer = &layers[0];
+ let mut errors = Vec::new();
+ match layer {
+ MessageLayer::SignatureGroup { results } => {
+ // We possibly have multiple signatures, but not all keys, so `or` all the individual results.
+ for result in results {
+ match result {
+ Ok(_) => good = true,
+ Err(e) => errors.push(e),
+ }
+ }
+ }
+ _ => return Err(anyhow::anyhow!("Unexpected message structure")),
+ }
+
+ if good {
+ Ok(()) // Good signature.
+ } else {
+ if errors.len() > 1 {
+ eprintln!("\nEncountered {} errors:", errors.len());
+ }
+
+ for (n, err) in errors.iter().enumerate() {
+ if errors.len() > 1 {
+ eprintln!("\nSignature #{n}: {err}");
+ } else {
+ eprintln!("\n{err}");
+ }
+ match err {
+ VerificationError::MalformedSignature { error, .. }
+ | VerificationError::UnboundKey { error, .. }
+ | VerificationError::BadKey { error, .. }
+ | VerificationError::BadSignature { error, .. } => {
+ let mut cause = error.chain();
+ if cause.len() > 1 {
+ cause.next(); // already included in `err` above
+ eprintln!("Caused by:");
+ for (n, e) in cause.enumerate() {
+ eprintln!("\t{n}: {e}");
+ }
+ }
+ }
+ VerificationError::MissingKey { .. }
+ | VerificationError::UnknownSignature { .. } => {} // doesn't contain a cause
+ _ => {} // we already print the error above in full
+ };
+ }
+ eprintln!();
+ Err(anyhow::anyhow!("No valid signature found."))
+ }
+ }
+}
+
+/// Verifies GPG-signed `msg` was signed by `key`, returning the verified data without signature.
+pub fn verify_signature(
+ msg: &[u8],
+ key: &[u8],
+ detached_sig: Option<&[u8]>,
+ weak_crypto: &WeakCryptoConfig,
+) -> Result<Vec<u8>, Error> {
+ let mut policy = StandardPolicy::new();
+ if weak_crypto.allow_sha1 {
+ policy.accept_hash(HashAlgorithm::SHA1);
+ }
+ if let Some(min_dsa) = weak_crypto.min_dsa_key_size {
+ if min_dsa <= 1024 {
+ policy.accept_asymmetric_algo(sequoia_openpgp::policy::AsymmetricAlgorithm::DSA1024);
+ }
+ }
+ if let Some(min_rsa) = weak_crypto.min_rsa_key_size {
+ if min_rsa <= 1024 {
+ policy.accept_asymmetric_algo(sequoia_openpgp::policy::AsymmetricAlgorithm::RSA1024);
+ }
+ }
+
+ let verifier = |cert| {
+ let helper = Helper { cert: &cert };
+
+ if let Some(sig) = detached_sig {
+ let mut verifier =
+ DetachedVerifierBuilder::from_bytes(sig)?.with_policy(&policy, None, helper)?;
+ verifier.verify_bytes(msg)?;
+ Ok(msg.to_vec())
+ } else {
+ let mut verified = Vec::new();
+ let mut verifier =
+ VerifierBuilder::from_bytes(msg)?.with_policy(&policy, None, helper)?;
+ let bytes = io::copy(&mut verifier, &mut verified)?;
+ println!("{bytes} bytes verified");
+ if !verifier.message_processed() {
+ bail!("Failed to verify message!");
+ }
+ Ok(verified)
+ }
+ };
+
+ let mut packed_parser = PacketParser::from_bytes(key)?;
+
+ // parse all packets to see whether this is a simple certificate or a keyring
+ while let PacketParserResult::Some(pp) = packed_parser {
+ packed_parser = pp.recurse()?.1;
+ }
+
+ if let PacketParserResult::EOF(eof) = packed_parser {
+ // verify against a single certificate
+ if eof.is_cert().is_ok() {
+ let cert = Cert::from_bytes(key)?;
+ return verifier(cert);
+ // verify against a keyring
+ } else if eof.is_keyring().is_ok() {
+ let packed_parser = PacketParser::from_bytes(key)?;
+
+ return CertParser::from(packed_parser)
+ // flatten here as we ignore packets that aren't a certificate
+ .flatten()
+ // keep trying to verify the message until the first certificate that succeeds
+ .find_map(|c| verifier(c).ok())
+ // if no certificate verified the message, abort
+ .ok_or_else(|| format_err!("No key in keyring could verify the message!"));
+ }
+ }
+
+ // neither a keyring nor a certificate was detect, so we abort here
+ bail!("'key-path' contains neither a keyring nor a certificate, aborting!");
+}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2025-10-23 10:40 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-10-23 10:39 [pve-devel] [PATCH proxmox v5 0/4] fix #5207: apt: check signage of repos with proxmox-pgp Nicolas Frey
2025-10-23 10:39 ` Nicolas Frey [this message]
2025-10-23 10:39 ` [pve-devel] [PATCH proxmox v5 2/4] " Nicolas Frey
2025-10-23 14:24 ` Nicolas Frey
2025-10-23 10:39 ` [pve-devel] [PATCH proxmox v5 3/4] apt: add tests for POM release filenames Nicolas Frey
2025-10-23 10:39 ` [pve-devel] [PATCH proxmox v5 4/4] apt: check for local POM InRelease as fallback Nicolas Frey
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=20251023103953.305810-2-n.frey@proxmox.com \
--to=n.frey@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
--cc=t.lamprech@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox