* [PATCH proxmox-backup 0/3] fixup for server side decryption
@ 2026-04-24 10:36 Christian Ebner
2026-04-24 10:36 ` [PATCH proxmox-backup 1/3] sync: pull decrypt: check if contents only signed or fully encrypted Christian Ebner
` (4 more replies)
0 siblings, 5 replies; 8+ messages in thread
From: Christian Ebner @ 2026-04-24 10:36 UTC (permalink / raw)
To: pbs-devel
When decryption keys are set for a pull job, currently it is not
correctly distinguished whether the snapshot to be pulled is trueley
encrypted or just signed, for both cases the key fingerprint being
present on the manifest. Fix this by explicitley checking each files
blob crypt mode as registered in the manifest. Further, extend error
messages on key mismatch with pre-existing local manifests to also
include the signed-only case.
Path 1 is a bugfix for a missing check of snapshots being encrypted
or signed only on decrypting pull syncs. Patch 2 and 3 are related
code cleanups only.
Christian Ebner (3):
sync: pull decrypt: check if contents only signed or fully encrypted
sync: pull: refactor decryption key loading checks
api: encryption keys: refactor associated keys check
src/api2/config/encryption_keys.rs | 18 +--
src/server/pull.rs | 211 +++++++++++++++++++----------
2 files changed, 147 insertions(+), 82 deletions(-)
--
2.47.3
^ permalink raw reply [flat|nested] 8+ messages in thread* [PATCH proxmox-backup 1/3] sync: pull decrypt: check if contents only signed or fully encrypted 2026-04-24 10:36 [PATCH proxmox-backup 0/3] fixup for server side decryption Christian Ebner @ 2026-04-24 10:36 ` Christian Ebner 2026-04-24 10:36 ` [PATCH proxmox-backup 2/3] sync: pull: refactor decryption key loading checks Christian Ebner ` (3 subsequent siblings) 4 siblings, 0 replies; 8+ messages in thread From: Christian Ebner @ 2026-04-24 10:36 UTC (permalink / raw) To: pbs-devel 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 ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH proxmox-backup 2/3] sync: pull: refactor decryption key loading checks 2026-04-24 10:36 [PATCH proxmox-backup 0/3] fixup for server side decryption Christian Ebner 2026-04-24 10:36 ` [PATCH proxmox-backup 1/3] sync: pull decrypt: check if contents only signed or fully encrypted Christian Ebner @ 2026-04-24 10:36 ` 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 ` (2 subsequent siblings) 4 siblings, 1 reply; 8+ messages in thread From: Christian Ebner @ 2026-04-24 10:36 UTC (permalink / raw) To: pbs-devel In an effort to improve code readability and maintanablity. By pulling out the logic into a dedicated helper, nested branches can be switched over to early return statements instead, greatly reducing the required nesting level. Signed-off-by: Christian Ebner <c.ebner@proxmox.com> --- src/server/pull.rs | 225 +++++++++++++++++++++++++++------------------ 1 file changed, 137 insertions(+), 88 deletions(-) diff --git a/src/server/pull.rs b/src/server/pull.rs index 051405ae7..d0f886cb5 100644 --- a/src/server/pull.rs +++ b/src/server/pull.rs @@ -755,95 +755,28 @@ async fn pull_snapshot<'a>( return Ok(sync_stats); } - let mut crypt_config = None; - let mut new_manifest = None; - if let Some(key_fp) = manifest.fingerprint().with_context(|| prefix.clone())? { - // 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()) - { - // check if source is encrypted or contents signed - if !manifest - .files() - .iter() - .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt) - { - log_sender - .log( - Level::WARN, - format!("Snapshot not fully encrypted, sync as is despite matching key '{key_id}' with fingerprint {key_fp}"), - ) - .await?; - } 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 - .fingerprint() - .with_context(|| prefix.clone())? - { - if existing_fingerprint != key_fp { - // pre-existing local manifest for encrypted snapshot with key mismatch - bail!("Local encrypted or signed snapshot with different key detected, refuse to sync"); - } - } else { - // pre-existing local manifest without key-fingerprint was previously decrypted, - // never overwrite with encrypted - bail!( - "local snapshot was previously decrypted but no matching decryption key is configured, refuse to sync" - ); - } - } else if !params.crypt_configs.is_empty() { - log_sender - .log( - Level::INFO, - format!("{prefix}: No matching key found, sync without decryption"), - ) - .await?; + let (crypt_config, new_manifest) = match optionally_use_decryption_key( + Arc::clone(¶ms), + &manifest, + existing_target_manifest.as_ref(), + prefix.clone(), + Arc::clone(&log_sender), + ) + .await? + { + (None, false) => (None, None), // regular pull without decryption + (Some(crypt_config), false) => { + // decrypt while pull + let new_manifest = Arc::new(Mutex::new(BackupManifest::new(snapshot.into()))); + (Some(crypt_config), Some(new_manifest)) } - } + (_, true) => { + // nothing changed + fetch_log().await?; + cleanup().await?; + return Ok(sync_stats); + } + }; for item in manifest.files() { let mut path = snapshot.full_path(); @@ -979,6 +912,122 @@ async fn pull_snapshot<'a>( Ok(sync_stats) } +/// Check if the decryption key should be used to decrypt the snapshot during +/// pull based on given pull parameter, source and optionally already present +/// target manifest. +/// +/// The boolean flag in the returned tuple indicates whether the pull can be +/// skipped altogether, since the already existing target is unchanged. +/// If decryption should happen, the matching decryption key is returned. +async fn optionally_use_decryption_key( + params: Arc<PullParameters>, + manifest: &BackupManifest, + existing_target_manifest: Option<&BackupManifest>, + prefix: String, + log_sender: Arc<LogLineSender>, +) -> Result<(Option<Arc<CryptConfig>>, bool), Error> { + let key_fp = match manifest.fingerprint().with_context(|| prefix.clone())? { + Some(key_fp) => key_fp, + None => return Ok((None, false)), // no fingerprint on source, regular pull + }; + + // source got key fingerprint, expect contents to be signed or encrypted + let (key_id, config) = match params + .crypt_configs + .iter() + .find(|(_id, crypt_conf)| crypt_conf.fingerprint() == *key_fp.bytes()) + { + Some(key_id_and_config) => key_id_and_config, + None => { + // no matching key found in list of configured ones + if let Some(existing_target_manifest) = existing_target_manifest { + if let Some(existing_fingerprint) = existing_target_manifest + .fingerprint() + .with_context(|| prefix.clone())? + { + if existing_fingerprint != key_fp { + // pre-existing local manifest for encrypted snapshot with key mismatch + bail!( + "Local encrypted or signed snapshot with different key detected, refuse to sync" + ); + } + } else { + // pre-existing local manifest without key-fingerprint was previously decrypted, + // never overwrite with encrypted + bail!( + "local snapshot was previously decrypted but no matching decryption key is configured, refuse to sync" + ); + } + } else if !params.crypt_configs.is_empty() { + log_sender + .log( + Level::INFO, + format!("{prefix}: No matching key found, sync without decryption"), + ) + .await?; + } + return Ok((None, false)); + } + }; + + // check if source is encrypted or contents signed + if !manifest + .files() + .iter() + .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt) + { + log_sender + .log( + Level::WARN, + format!("Snapshot not fully encrypted, sync as is despite matching key '{key_id}' with fingerprint {key_fp}"), + ) + .await?; + return Ok((None, false)); + } + + manifest + .check_signature(config) + .context("failed to check source manifest signature") + .with_context(|| prefix.clone())?; + + // avoid overwriting pre-existing target manifest + 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() { + return Ok((None, true)); + } + + bail!("Change detection fingerprint mismatch, refuse to continue!"); + } + return Ok((None, false)); + } + + log_sender + .log( + Level::INFO, + format!("Found matching key '{key_id}' with fingerprint {key_fp}, decrypt on pull"), + ) + .await?; + + Ok((Some(Arc::clone(config)), false)) +} + /// Pulls a `snapshot`, removing newly created ones on error, but keeping existing ones in any case. /// /// The `reader` is configured to read from the source backup directory, while the -- 2.47.3 ^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH proxmox-backup 2/3] sync: pull: refactor decryption key loading checks 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 0 siblings, 0 replies; 8+ messages in thread From: Fabian Grünbichler @ 2026-04-24 12:46 UTC (permalink / raw) To: Christian Ebner, pbs-devel On April 24, 2026 12:36 pm, Christian Ebner wrote: > In an effort to improve code readability and maintanablity. > > By pulling out the logic into a dedicated helper, nested branches can > be switched over to early return statements instead, greatly reducing > the required nesting level. > > Signed-off-by: Christian Ebner <c.ebner@proxmox.com> > --- > src/server/pull.rs | 225 +++++++++++++++++++++++++++------------------ > 1 file changed, 137 insertions(+), 88 deletions(-) > > diff --git a/src/server/pull.rs b/src/server/pull.rs > index 051405ae7..d0f886cb5 100644 > --- a/src/server/pull.rs > +++ b/src/server/pull.rs > @@ -755,95 +755,28 @@ async fn pull_snapshot<'a>( > return Ok(sync_stats); > } > > - let mut crypt_config = None; > - let mut new_manifest = None; > - if let Some(key_fp) = manifest.fingerprint().with_context(|| prefix.clone())? { > - // 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()) > - { > - // check if source is encrypted or contents signed > - if !manifest > - .files() > - .iter() > - .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt) > - { > - log_sender > - .log( > - Level::WARN, > - format!("Snapshot not fully encrypted, sync as is despite matching key '{key_id}' with fingerprint {key_fp}"), > - ) > - .await?; > - } 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 > - .fingerprint() > - .with_context(|| prefix.clone())? > - { > - if existing_fingerprint != key_fp { > - // pre-existing local manifest for encrypted snapshot with key mismatch > - bail!("Local encrypted or signed snapshot with different key detected, refuse to sync"); > - } > - } else { > - // pre-existing local manifest without key-fingerprint was previously decrypted, > - // never overwrite with encrypted > - bail!( > - "local snapshot was previously decrypted but no matching decryption key is configured, refuse to sync" > - ); > - } > - } else if !params.crypt_configs.is_empty() { > - log_sender > - .log( > - Level::INFO, > - format!("{prefix}: No matching key found, sync without decryption"), > - ) > - .await?; > + let (crypt_config, new_manifest) = match optionally_use_decryption_key( > + Arc::clone(¶ms), > + &manifest, > + existing_target_manifest.as_ref(), > + prefix.clone(), > + Arc::clone(&log_sender), > + ) > + .await? > + { > + (None, false) => (None, None), // regular pull without decryption > + (Some(crypt_config), false) => { > + // decrypt while pull > + let new_manifest = Arc::new(Mutex::new(BackupManifest::new(snapshot.into()))); > + (Some(crypt_config), Some(new_manifest)) > } > - } > + (_, true) => { > + // nothing changed > + fetch_log().await?; > + cleanup().await?; > + return Ok(sync_stats); > + } > + }; > > for item in manifest.files() { > let mut path = snapshot.full_path(); > @@ -979,6 +912,122 @@ async fn pull_snapshot<'a>( > Ok(sync_stats) > } > > +/// Check if the decryption key should be used to decrypt the snapshot during > +/// pull based on given pull parameter, source and optionally already present > +/// target manifest. > +/// > +/// The boolean flag in the returned tuple indicates whether the pull can be > +/// skipped altogether, since the already existing target is unchanged. > +/// If decryption should happen, the matching decryption key is returned. > +async fn optionally_use_decryption_key( > + params: Arc<PullParameters>, > + manifest: &BackupManifest, > + existing_target_manifest: Option<&BackupManifest>, > + prefix: String, > + log_sender: Arc<LogLineSender>, > +) -> Result<(Option<Arc<CryptConfig>>, bool), Error> { > + let key_fp = match manifest.fingerprint().with_context(|| prefix.clone())? { > + Some(key_fp) => key_fp, > + None => return Ok((None, false)), // no fingerprint on source, regular pull > + }; this can be a let else isntead > + > + // source got key fingerprint, expect contents to be signed or encrypted > + let (key_id, config) = match params > + .crypt_configs > + .iter() > + .find(|(_id, crypt_conf)| crypt_conf.fingerprint() == *key_fp.bytes()) > + { > + Some(key_id_and_config) => key_id_and_config, this as well > + None => { > + // no matching key found in list of configured ones > + if let Some(existing_target_manifest) = existing_target_manifest { > + if let Some(existing_fingerprint) = existing_target_manifest > + .fingerprint() > + .with_context(|| prefix.clone())? > + { > + if existing_fingerprint != key_fp { > + // pre-existing local manifest for encrypted snapshot with key mismatch > + bail!( > + "Local encrypted or signed snapshot with different key detected, refuse to sync" > + ); > + } > + } else { > + // pre-existing local manifest without key-fingerprint was previously decrypted, > + // never overwrite with encrypted > + bail!( > + "local snapshot was previously decrypted but no matching decryption key is configured, refuse to sync" > + ); > + } > + } else if !params.crypt_configs.is_empty() { > + log_sender > + .log( > + Level::INFO, > + format!("{prefix}: No matching key found, sync without decryption"), > + ) > + .await?; > + } > + return Ok((None, false)); > + } > + }; > + > + // check if source is encrypted or contents signed > + if !manifest > + .files() > + .iter() > + .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt) > + { > + log_sender > + .log( > + Level::WARN, > + format!("Snapshot not fully encrypted, sync as is despite matching key '{key_id}' with fingerprint {key_fp}"), > + ) > + .await?; > + return Ok((None, false)); > + } > + > + manifest > + .check_signature(config) > + .context("failed to check source manifest signature") > + .with_context(|| prefix.clone())?; so at this point we are if - the source is encrypted, we found a matching key, and the signature is okay > + > + // avoid overwriting pre-existing target manifest > + 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!"); > + } if it is signed we don't check the change detection fingerprint.. > + } 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() { > + return Ok((None, true)); > + } > + > + bail!("Change detection fingerprint mismatch, refuse to continue!"); > + } > + return Ok((None, false)); isn't this None here wrong? we know the source is encrypted, and the target is not, so we should either abort, or return the key we found. I sent some follow-ups, feel free to fold them in with either of these two variants (they currently abort, which seems like the safe choice to me). > + } > + > + log_sender > + .log( > + Level::INFO, > + format!("Found matching key '{key_id}' with fingerprint {key_fp}, decrypt on pull"), > + ) > + .await?; > + > + Ok((Some(Arc::clone(config)), false)) > +} > + > /// Pulls a `snapshot`, removing newly created ones on error, but keeping existing ones in any case. > /// > /// The `reader` is configured to read from the source backup directory, while the > -- > 2.47.3 > > > > > > ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH proxmox-backup 3/3] api: encryption keys: refactor associated keys check 2026-04-24 10:36 [PATCH proxmox-backup 0/3] fixup for server side decryption Christian Ebner 2026-04-24 10:36 ` [PATCH proxmox-backup 1/3] sync: pull decrypt: check if contents only signed or fully encrypted Christian Ebner 2026-04-24 10:36 ` [PATCH proxmox-backup 2/3] sync: pull: refactor decryption key loading checks Christian Ebner @ 2026-04-24 10:36 ` 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 19:06 ` applied: [PATCH proxmox-backup 0/3] fixup for server side decryption Thomas Lamprecht 4 siblings, 0 replies; 8+ messages in thread From: Christian Ebner @ 2026-04-24 10:36 UTC (permalink / raw) To: pbs-devel Fixes an if_same_then_else clippy warning and improves the readability for the check of sync jobs having a key assigned as associated key. Both branches have the same push logic, so combine into a common if statement. Since the if statement would then however be hard to parse, move the associated key check logic into a closue instead. Signed-off-by: Christian Ebner <c.ebner@proxmox.com> --- src/api2/config/encryption_keys.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/api2/config/encryption_keys.rs b/src/api2/config/encryption_keys.rs index 81483302e..5c0c08e52 100644 --- a/src/api2/config/encryption_keys.rs +++ b/src/api2/config/encryption_keys.rs @@ -195,15 +195,17 @@ fn check_encryption_key_in_use(id: &str, include_associated: bool) -> Result<(), let mut used_by_jobs = Vec::new(); let job_list: Vec<SyncJobConfig> = config.convert_to_typed_array("sync")?; + + let contains_associated_key = |job: &SyncJobConfig| { + job.associated_key + .as_deref() + .unwrap_or(&[]) + .contains(&id.to_string()) + }; + for job in job_list { - if job.active_encryption_key.as_deref() == Some(id) { - used_by_jobs.push(job.id.clone()); - } else if include_associated - && job - .associated_key - .as_deref() - .unwrap_or(&[]) - .contains(&id.to_string()) + if job.active_encryption_key.as_deref() == Some(id) + || (include_associated && contains_associated_key(&job)) { used_by_jobs.push(job.id.clone()); } -- 2.47.3 ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH proxmox-backup 1/2] pull: decrypt: use let else to reduce indentation 2026-04-24 10:36 [PATCH proxmox-backup 0/3] fixup for server side decryption Christian Ebner ` (2 preceding siblings ...) 2026-04-24 10:36 ` [PATCH proxmox-backup 3/3] api: encryption keys: refactor associated keys check Christian Ebner @ 2026-04-24 12:46 ` 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 4 siblings, 1 reply; 8+ messages in thread From: Fabian Grünbichler @ 2026-04-24 12:46 UTC (permalink / raw) To: pbs-devel Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com> --- best viewed with -w src/server/pull.rs | 56 +++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/server/pull.rs b/src/server/pull.rs index d0f886cb5..3dc5770f9 100644 --- a/src/server/pull.rs +++ b/src/server/pull.rs @@ -926,48 +926,44 @@ async fn optionally_use_decryption_key( prefix: String, log_sender: Arc<LogLineSender>, ) -> Result<(Option<Arc<CryptConfig>>, bool), Error> { - let key_fp = match manifest.fingerprint().with_context(|| prefix.clone())? { - Some(key_fp) => key_fp, - None => return Ok((None, false)), // no fingerprint on source, regular pull + let Some(key_fp) = manifest.fingerprint().with_context(|| prefix.clone())? else { + return Ok((None, false)); // no fingerprint on source, regular pull }; // source got key fingerprint, expect contents to be signed or encrypted - let (key_id, config) = match params + let Some((key_id, config)) = params .crypt_configs .iter() .find(|(_id, crypt_conf)| crypt_conf.fingerprint() == *key_fp.bytes()) - { - Some(key_id_and_config) => key_id_and_config, - None => { - // no matching key found in list of configured ones - if let Some(existing_target_manifest) = existing_target_manifest { - if let Some(existing_fingerprint) = existing_target_manifest - .fingerprint() - .with_context(|| prefix.clone())? - { - if existing_fingerprint != key_fp { - // pre-existing local manifest for encrypted snapshot with key mismatch - bail!( + else { + // no matching key found in list of configured ones + if let Some(existing_target_manifest) = existing_target_manifest { + if let Some(existing_fingerprint) = existing_target_manifest + .fingerprint() + .with_context(|| prefix.clone())? + { + if existing_fingerprint != key_fp { + // pre-existing local manifest for encrypted snapshot with key mismatch + bail!( "Local encrypted or signed snapshot with different key detected, refuse to sync" ); - } - } else { - // pre-existing local manifest without key-fingerprint was previously decrypted, - // never overwrite with encrypted - bail!( + } + } else { + // pre-existing local manifest without key-fingerprint was previously decrypted, + // never overwrite with encrypted + bail!( "local snapshot was previously decrypted but no matching decryption key is configured, refuse to sync" ); - } - } else if !params.crypt_configs.is_empty() { - log_sender - .log( - Level::INFO, - format!("{prefix}: No matching key found, sync without decryption"), - ) - .await?; } - return Ok((None, false)); + } else if !params.crypt_configs.is_empty() { + log_sender + .log( + Level::INFO, + format!("{prefix}: No matching key found, sync without decryption"), + ) + .await?; } + return Ok((None, false)); }; // check if source is encrypted or contents signed -- 2.47.3 ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH proxmox-backup 2/2] pull: decrypt: re-order encryption key/state checks 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 ` Fabian Grünbichler 0 siblings, 0 replies; 8+ messages in thread From: Fabian Grünbichler @ 2026-04-24 12:46 UTC (permalink / raw) To: pbs-devel make the full sequence of checks: - is source encrypted? if not -> regular pull - if a local snapshot exists -- is it encrypted or signed using the same key -> regular pull -- is it encrypted using a different key -> abort -- if it is signed, we either abort later or log + regular pull - find decryption key -- if none is found, but a local snapshot exists and the source is full encrypted -> abort -- if none is found, and no local snapshot exists -> regular pull -- only proceed for plain snapshots or signed-with-same key, since those could be the result of a decrypting pull - check signature -> abort if it doesn't match - if a local snapshot exists, check change-detection fingerprint, and abort if it doesn't match or doesn't exist, otherwise assume we can proceed with a decrypting sync Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com> --- still not super happy with this, but AFAICT this should at least handle one case better (the final bail) and deduplicate some code.. IMHO the encrypted = part should move to BackupManifest itself, since we have it in quite a few places already, but unfortunately ENOTIME right now ;) src/server/pull.rs | 80 ++++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/server/pull.rs b/src/server/pull.rs index 3dc5770f9..b4882718e 100644 --- a/src/server/pull.rs +++ b/src/server/pull.rs @@ -930,32 +930,43 @@ async fn optionally_use_decryption_key( return Ok((None, false)); // no fingerprint on source, regular pull }; + // check if source is encrypted or contents signed + let encrypted = manifest + .files() + .iter() + .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt); + + if let Some(existing_manifest) = existing_target_manifest { + if let Some(existing_fingerprint) = existing_manifest.fingerprint()? { + if existing_fingerprint == key_fp { + let target_encrypted = existing_manifest + .files() + .iter() + .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt); + if encrypted == target_encrypted { + // both sides are signed or encrypted with the same key, just resync + return Ok((None, false)); + } + } else { + // pre-existing local manifest for encrypted snapshot with key mismatch + bail!("Local encrypted or signed snapshot with different key detected, refuse to sync"); + } + } + }; + // source got key fingerprint, expect contents to be signed or encrypted let Some((key_id, config)) = params .crypt_configs .iter() .find(|(_id, crypt_conf)| crypt_conf.fingerprint() == *key_fp.bytes()) else { - // no matching key found in list of configured ones - if let Some(existing_target_manifest) = existing_target_manifest { - if let Some(existing_fingerprint) = existing_target_manifest - .fingerprint() - .with_context(|| prefix.clone())? - { - if existing_fingerprint != key_fp { - // pre-existing local manifest for encrypted snapshot with key mismatch - bail!( - "Local encrypted or signed snapshot with different key detected, refuse to sync" - ); - } - } else { - // pre-existing local manifest without key-fingerprint was previously decrypted, - // never overwrite with encrypted - bail!( - "local snapshot was previously decrypted but no matching decryption key is configured, refuse to sync" - ); - } - } else if !params.crypt_configs.is_empty() { + // all the other cases are handled above + if encrypted && existing_target_manifest.is_some() { + bail!("No matching key found, refusing sync"); + } + + // regular sync + if !params.crypt_configs.is_empty() { log_sender .log( Level::INFO, @@ -963,15 +974,12 @@ async fn optionally_use_decryption_key( ) .await?; } + return Ok((None, false)); }; // check if source is encrypted or contents signed - if !manifest - .files() - .iter() - .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt) - { + if !encrypted { log_sender .log( Level::WARN, @@ -986,16 +994,11 @@ async fn optionally_use_decryption_key( .context("failed to check source manifest signature") .with_context(|| prefix.clone())?; + let mut skip_resync = false; + // avoid overwriting pre-existing target manifest 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 + if let Some(source_fp) = manifest .get_change_detection_fingerprint() .context("failed to parse change detection fingerprint of source manifest") .with_context(|| prefix.clone())? @@ -1006,12 +1009,13 @@ async fn optionally_use_decryption_key( .signature(config) .with_context(|| prefix.clone())?; if target_fp == *source_fp.bytes() { - return Ok((None, true)); + skip_resync = true; + } else { + bail!("Change detection fingerprint mismatch, refuse to continue!"); } - - bail!("Change detection fingerprint mismatch, refuse to continue!"); + } else { + bail!("No change detection fingerprint found, refuse to continue!"); } - return Ok((None, false)); } log_sender @@ -1021,7 +1025,7 @@ async fn optionally_use_decryption_key( ) .await?; - Ok((Some(Arc::clone(config)), false)) + Ok((Some(Arc::clone(config)), skip_resync)) } /// Pulls a `snapshot`, removing newly created ones on error, but keeping existing ones in any case. -- 2.47.3 ^ permalink raw reply [flat|nested] 8+ messages in thread
* applied: [PATCH proxmox-backup 0/3] fixup for server side decryption 2026-04-24 10:36 [PATCH proxmox-backup 0/3] fixup for server side decryption Christian Ebner ` (3 preceding siblings ...) 2026-04-24 12:46 ` [PATCH proxmox-backup 1/2] pull: decrypt: use let else to reduce indentation Fabian Grünbichler @ 2026-04-24 19:06 ` Thomas Lamprecht 4 siblings, 0 replies; 8+ messages in thread From: Thomas Lamprecht @ 2026-04-24 19:06 UTC (permalink / raw) To: pbs-devel, Christian Ebner On Fri, 24 Apr 2026 12:36:04 +0200, Christian Ebner wrote: > When decryption keys are set for a pull job, currently it is not > correctly distinguished whether the snapshot to be pulled is trueley > encrypted or just signed, for both cases the key fingerprint being > present on the manifest. Fix this by explicitley checking each files > blob crypt mode as registered in the manifest. Further, extend error > messages on key mismatch with pre-existing local manifests to also > include the signed-only case. > > [...] Applied, thanks! [1/3] sync: pull decrypt: check if contents only signed or fully encrypted commit: f515516448250afc96f3227d1323e0323ded0dda [2/3] sync: pull: refactor decryption key loading checks commit: 14b580739d9f0e3ad2098812a3a9b251ae5e48c1 [3/3] api: encryption keys: refactor associated keys check commit: a6dfa6abdea8045b82209bb357e21862d88b0abc ^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-04-24 19:10 UTC | newest] Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-04-24 10:36 [PATCH proxmox-backup 0/3] fixup for server side decryption Christian Ebner 2026-04-24 10:36 ` [PATCH proxmox-backup 1/3] sync: pull decrypt: check if contents only signed or fully encrypted Christian Ebner 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
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox