From: Christian Ebner <c.ebner@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox-backup 1/3] sync: pull decrypt: check if contents only signed or fully encrypted
Date: Fri, 24 Apr 2026 12:36:05 +0200 [thread overview]
Message-ID: <20260424103607.531400-2-c.ebner@proxmox.com> (raw)
In-Reply-To: <20260424103607.531400-1-c.ebner@proxmox.com>
The key-fingerprint stored on the source manifest might be present if
the snapshot contents are encrypted or signed.
Checking the key fingerprints presence on the source manifest is
therefore not enough to determine whether the snapshot is fully
encrypted, rather each of the registered files chunk crypt mode has
to be checked.
Only decrypt on pull with a matching key if the snapshots contains
only contains encrypted files, warn and fallback to regular pull if
not. Adapt error messages on key mismatch to reflect that snapshot
might be signed but not encrypted.
Reported-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
src/server/pull.rs | 90 ++++++++++++++++++++++++++--------------------
1 file changed, 52 insertions(+), 38 deletions(-)
diff --git a/src/server/pull.rs b/src/server/pull.rs
index 7fa273edb..051405ae7 100644
--- a/src/server/pull.rs
+++ b/src/server/pull.rs
@@ -758,52 +758,66 @@ async fn pull_snapshot<'a>(
let mut crypt_config = None;
let mut new_manifest = None;
if let Some(key_fp) = manifest.fingerprint().with_context(|| prefix.clone())? {
- // source is encrypted, find matching key
+ // source got key fingerprint, expect contents to be signed or encrypted
if let Some((key_id, config)) = params
.crypt_configs
.iter()
.find(|(_id, crypt_conf)| crypt_conf.fingerprint() == *key_fp.bytes())
{
- manifest
- .check_signature(config)
- .context("failed to check source manifest signature")
- .with_context(|| prefix.clone())?;
-
- if let Some(existing_manifest) = existing_target_manifest {
- if let Some(existing_fingerprint) = existing_manifest
- .fingerprint()
- .with_context(|| prefix.clone())?
- {
- if existing_fingerprint != key_fp {
- bail!("Detected local encrypted snapshot with encryption key mismatch!");
- }
- } else if let Some(source_fp) = manifest
- .get_change_detection_fingerprint()
- .context("failed to parse change detection fingerprint of source manifest")
- .with_context(|| prefix.clone())?
- {
- // Stored fp is HMAC over the unencrypted source's protected fields; recompute
- // over the locally decrypted manifest, not the fresh encrypted remote one.
- let target_fp = existing_manifest
- .signature(config)
- .with_context(|| prefix.clone())?;
- if target_fp == *source_fp.bytes() {
- fetch_log().await?;
- cleanup().await?;
- return Ok(sync_stats); // nothing changed
- }
-
- bail!("Change detection fingerprint mismatch, refuse to continue");
- }
- } else {
+ // check if source is encrypted or contents signed
+ if !manifest
+ .files()
+ .iter()
+ .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt)
+ {
log_sender
.log(
- Level::INFO,
- format!("Found matching key '{key_id}' with fingerprint {key_fp}, decrypt on pull"),
+ Level::WARN,
+ format!("Snapshot not fully encrypted, sync as is despite matching key '{key_id}' with fingerprint {key_fp}"),
)
.await?;
- crypt_config = Some(Arc::clone(config));
- new_manifest = Some(Arc::new(Mutex::new(BackupManifest::new(snapshot.into()))));
+ } else {
+ manifest
+ .check_signature(config)
+ .context("failed to check source manifest signature")
+ .with_context(|| prefix.clone())?;
+
+ if let Some(existing_manifest) = existing_target_manifest {
+ if let Some(existing_fingerprint) = existing_manifest
+ .fingerprint()
+ .with_context(|| prefix.clone())?
+ {
+ if existing_fingerprint != key_fp {
+ bail!("Detected local encrypted or signed snapshot with encryption key mismatch!");
+ }
+ } else if let Some(source_fp) = manifest
+ .get_change_detection_fingerprint()
+ .context("failed to parse change detection fingerprint of source manifest")
+ .with_context(|| prefix.clone())?
+ {
+ // Stored fp is HMAC over the unencrypted source's protected fields; recompute
+ // over the locally decrypted manifest, not the fresh encrypted remote one.
+ let target_fp = existing_manifest
+ .signature(config)
+ .with_context(|| prefix.clone())?;
+ if target_fp == *source_fp.bytes() {
+ fetch_log().await?;
+ cleanup().await?;
+ return Ok(sync_stats); // nothing changed
+ }
+
+ bail!("Change detection fingerprint mismatch, refuse to continue");
+ }
+ } else {
+ log_sender
+ .log(
+ Level::INFO,
+ format!("Found matching key '{key_id}' with fingerprint {key_fp}, decrypt on pull"),
+ )
+ .await?;
+ crypt_config = Some(Arc::clone(config));
+ new_manifest = Some(Arc::new(Mutex::new(BackupManifest::new(snapshot.into()))));
+ }
}
} else if let Some(existing_target_manifest) = existing_target_manifest {
if let Some(existing_fingerprint) = existing_target_manifest
@@ -812,7 +826,7 @@ async fn pull_snapshot<'a>(
{
if existing_fingerprint != key_fp {
// pre-existing local manifest for encrypted snapshot with key mismatch
- bail!("Local encrypted snapshot with different key detected, refuse to sync");
+ bail!("Local encrypted or signed snapshot with different key detected, refuse to sync");
}
} else {
// pre-existing local manifest without key-fingerprint was previously decrypted,
--
2.47.3
next prev parent reply other threads:[~2026-04-24 10:36 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-24 10:36 [PATCH proxmox-backup 0/3] fixup for server side decryption Christian Ebner
2026-04-24 10:36 ` Christian Ebner [this message]
2026-04-24 10:36 ` [PATCH proxmox-backup 2/3] sync: pull: refactor decryption key loading checks Christian Ebner
2026-04-24 12:46 ` Fabian Grünbichler
2026-04-24 10:36 ` [PATCH proxmox-backup 3/3] api: encryption keys: refactor associated keys check Christian Ebner
2026-04-24 12:46 ` [PATCH proxmox-backup 1/2] pull: decrypt: use let else to reduce indentation Fabian Grünbichler
2026-04-24 12:46 ` [PATCH proxmox-backup 2/2] pull: decrypt: re-order encryption key/state checks Fabian Grünbichler
2026-04-24 19:06 ` applied: [PATCH proxmox-backup 0/3] fixup for server side decryption Thomas Lamprecht
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=20260424103607.531400-2-c.ebner@proxmox.com \
--to=c.ebner@proxmox.com \
--cc=pbs-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox