public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Christian Ebner <c.ebner@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox-backup v2 5/5] sync: push: gracefully handle previous manifest signature mismatches
Date: Thu,  7 May 2026 15:01:35 +0200	[thread overview]
Message-ID: <20260507130135.589100-6-c.ebner@proxmox.com> (raw)
In-Reply-To: <20260507130135.589100-1-c.ebner@proxmox.com>

During push sync jobs with a given active encryption key, the key is
loaded for the backup writer, used to encrypt the source snapshot on
the fly. As optimization, the backup writer deduplicates chunks
already present in the previous snapshot by loading them from the
index files as stored in the previous manifest.

Reuse of the previous manifest and index must however only happen if:
- The previous manifest is not encrypted, neither will push encrypt.
- The previous manifest is encrypted and passes signature
  verification using the same key to be used during push encryption.

Since these checks are now performed when loading the previous
manifest, drop redundant and now useless checks performed before
loading the chunks from index files.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
 src/server/push.rs | 81 ++++++++++++++++++++++++++++++++++------------
 1 file changed, 61 insertions(+), 20 deletions(-)

diff --git a/src/server/push.rs b/src/server/push.rs
index afa78b751..4495b68ed 100644
--- a/src/server/push.rs
+++ b/src/server/push.rs
@@ -979,25 +979,12 @@ pub(crate) async fn push_group(
 }
 
 async fn load_previous_snapshot_known_chunks(
-    params: &PushParameters,
     upload_options: &UploadOptions,
     backup_writer: &BackupWriter,
     archive_name: &BackupArchiveName,
     known_chunks: Arc<Mutex<HashSet<[u8; 32]>>>,
 ) {
     if let Some(manifest) = upload_options.previous_manifest.as_ref() {
-        if let Some((_id, crypt_config)) = &params.crypt_config {
-            // no fingerprint in previous manifest -> no reuse possible
-            let Ok(Some(fingerprint)) = manifest.fingerprint() else {
-                return;
-            };
-
-            if *fingerprint.bytes() != crypt_config.fingerprint() {
-                // key mismatch -> no reuse possible
-                return;
-            }
-        }
-
         // Add known chunks, ignore errors since archive might not be present and it is better
         // to proceed on unrelated errors than to fail here.
         match archive_name.archive_type() {
@@ -1128,18 +1115,73 @@ pub(crate) async fn push_snapshot(
     .await
     .with_context(|| prefix.to_string())?;
 
-    let mut previous_manifest = None;
     // Use manifest of previous snapshots in group on target for chunk upload deduplication
-    if fetch_previous_manifest {
-        match backup_writer.download_previous_manifest(false).await {
-            Ok(manifest) => previous_manifest = Some(Arc::new(manifest)),
+    let previous_manifest = if !fetch_previous_manifest {
+        None
+    } else {
+        let result = backup_writer
+            .download_previous_manifest(false)
+            .await
+            .map_err(|err| err.context("failed to download"))
+            .and_then(|manifest| {
+                let fingerprint = manifest
+                    .fingerprint()
+                    .context("failed getting fingerprint")?;
+
+                let Some(manifest_key_fp) = fingerprint else {
+                    if encrypt_using_key.is_none() {
+                        return Ok(Arc::new(manifest));
+                    }
+                    bail!("manifest not encrypted");
+                };
+
+                let signed_only = manifest
+                    .files()
+                    .iter()
+                    .all(|f| f.chunk_crypt_mode() == CryptMode::None);
+
+                let Some((_key_id, crypt_config)) = &encrypt_using_key else {
+                    if signed_only {
+                        return Ok(Arc::new(manifest));
+                    }
+                    bail!("manifest encrypted but no encryption key configured");
+                };
+
+                if *manifest_key_fp.bytes() == crypt_config.fingerprint() {
+                    if let Some(expected_signature) = &manifest.signature {
+                        let calculated_signature = manifest
+                            .signature(crypt_config)
+                            .context("signature calculation failed")?;
+                        if hex::encode(calculated_signature) == *expected_signature {
+                            return Ok(Arc::new(manifest));
+                        }
+                        bail!("signature mismatch with configured encryption key");
+                    }
+                    bail!("manifest not signed with configured encryption key");
+                }
+                bail!(
+                    "encryption key of previous snapshot does not match configured encryption key"
+                );
+            });
+
+        match result {
+            Ok(prev) => {
+                log_sender
+                    .log(
+                        Level::INFO,
+                        format!("{prefix}: Reusing previous manifest for deduplication"),
+                    )
+                    .await?;
+                Some(prev)
+            }
             Err(err) => {
                 log_sender
                     .log(
                         Level::INFO,
-                        format!("{prefix}: Could not download previous manifest - {err}"),
+                        format!("{prefix}: Skip reuse of previous manifest - {err:#}"),
                     )
-                    .await?
+                    .await?;
+                None
             }
         }
     };
@@ -1184,7 +1226,6 @@ pub(crate) async fn push_snapshot(
             };
 
             load_previous_snapshot_known_chunks(
-                params,
                 &upload_options,
                 &backup_writer,
                 &archive_name,
-- 
2.47.3





      parent reply	other threads:[~2026-05-07 13:02 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-07 13:01 [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 1/5] datastore: data blob: refactor crypt mode method Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 2/5] datastore: data blob: refactor decoding method Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 3/5] client: backup writer: pass no crypt config to manifest blob decoder Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 4/5] client: allow skipping signature check on previous manifest fetching Christian Ebner
2026-05-07 13:01 ` Christian Ebner [this message]

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=20260507130135.589100-6-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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal