mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 19:16:17 +00:00
fix(kms): version local key records safely (#5638)
This commit is contained in:
@@ -72,6 +72,7 @@ impl From<&BackupError> for RestoreBlocker {
|
||||
BackupError::Corrupted { .. } => RestoreBlockerCode::BundleCorrupted,
|
||||
BackupError::Truncated { .. } => RestoreBlockerCode::BundleTruncated,
|
||||
BackupError::UnknownVersion { .. } => RestoreBlockerCode::UnknownFormatVersion,
|
||||
BackupError::UnsupportedFormatVersion { .. } => RestoreBlockerCode::UnknownFormatVersion,
|
||||
BackupError::WrongKek { .. } => RestoreBlockerCode::WrongBackupKek,
|
||||
BackupError::MissingArtifact { .. } => RestoreBlockerCode::MissingArtifact,
|
||||
BackupError::IncompleteBundle { .. } => RestoreBlockerCode::IncompleteBundle,
|
||||
|
||||
@@ -51,6 +51,11 @@ pub enum BackupError {
|
||||
supplied_kek_version: u32,
|
||||
},
|
||||
|
||||
/// A bundled key record declares a format version this build does not
|
||||
/// understand.
|
||||
#[error("bundled key record '{key_id}' declares unsupported format version {version}; this build cannot restore it")]
|
||||
UnsupportedFormatVersion { key_id: String, version: String },
|
||||
|
||||
/// Manifest requires an artifact that is not present in the bundle.
|
||||
#[error("backup bundle is missing a required artifact: {artifact}")]
|
||||
MissingArtifact { artifact: String },
|
||||
@@ -128,6 +133,15 @@ mod tests {
|
||||
"unknown backup manifest format version 9 (this build supports version 1)"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
BackupError::UnsupportedFormatVersion {
|
||||
key_id: "alpha".to_string(),
|
||||
version: "9".to_string(),
|
||||
}
|
||||
.to_string(),
|
||||
"bundled key record 'alpha' declares unsupported format version 9; this build cannot restore it"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
BackupError::missing_artifact("key-material").to_string(),
|
||||
"backup bundle is missing a required artifact: key-material"
|
||||
|
||||
@@ -48,7 +48,10 @@
|
||||
//! published. A crash at any earlier point leaves a bundle without a
|
||||
//! manifest, which decodes as an incomplete bundle and can never be restored.
|
||||
|
||||
use crate::backends::local::{LocalKmsClient, StoredKeyProtection, unknown_protection_marker};
|
||||
use crate::backends::local::{
|
||||
LocalKmsClient, STORED_MASTER_KEY_FORMAT_VERSION, StoredKeyProtection, stored_master_key_format_version,
|
||||
unknown_protection_marker,
|
||||
};
|
||||
use crate::backup::capability::{AtRestProtection, BackupBackendKind, BackupResponsibility};
|
||||
use crate::backup::error::BackupError;
|
||||
use crate::backup::manifest::{
|
||||
@@ -380,6 +383,12 @@ async fn collect_snapshot(client: &LocalKmsClient) -> Result<CollectedSnapshot>
|
||||
// classified first so a record from a newer build keeps its own
|
||||
// verdict — an operator who reads "material corrupt" starts a
|
||||
// disaster recovery for what is only a version mismatch.
|
||||
let format_version = stored_master_key_format_version(&raw).map_err(|error| {
|
||||
KmsError::material_corrupt(&stem, format!("stored key record is not a readable JSON object: {error}"))
|
||||
})?;
|
||||
if format_version > STORED_MASTER_KEY_FORMAT_VERSION {
|
||||
return Err(KmsError::unsupported_format_version(&stem, format_version.to_string()));
|
||||
}
|
||||
let unknown_marker = unknown_protection_marker(&raw).map_err(|error| {
|
||||
KmsError::material_corrupt(&stem, format!("stored key record is not a readable JSON object: {error}"))
|
||||
})?;
|
||||
@@ -1150,4 +1159,26 @@ mod tests {
|
||||
"got {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn numeric_record_format_version_from_a_newer_build_aborts_export_as_unsupported_format() {
|
||||
let (client, _key_dir) = encrypted_client().await;
|
||||
client.create_key("alpha", "AES_256", None).await.expect("create key");
|
||||
|
||||
let record_path = client.key_directory().join("alpha.key");
|
||||
let mut record: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(&record_path).expect("read record")).expect("decode record");
|
||||
record["format_version"] = serde_json::json!(99);
|
||||
std::fs::write(&record_path, serde_json::to_vec_pretty(&record).expect("encode record")).expect("write record");
|
||||
|
||||
let bundle = TempDir::new().expect("bundle dir");
|
||||
let error = export_local_backup(&client, &test_kek(), &export_request(bundle.path().join("bundle")))
|
||||
.await
|
||||
.expect_err("a newer record format must abort the export");
|
||||
assert!(
|
||||
matches!(&error, KmsError::UnsupportedFormatVersion { key_id, version }
|
||||
if key_id == "alpha" && version == "99"),
|
||||
"got {error:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
|
||||
use crate::backends::local::{
|
||||
LOCAL_KMS_MASTER_KEY_SALT_FILE, LOCAL_KMS_MASTER_KEY_SALT_LEN, LOCAL_RESTORE_COMMIT_MARKER_FILE, LocalKmsClient,
|
||||
StoredKeyProtection, durable_file, is_orphan_commit_temp_name, unknown_protection_marker, validate_key_id,
|
||||
STORED_MASTER_KEY_FORMAT_VERSION, StoredKeyProtection, durable_file, is_orphan_commit_temp_name,
|
||||
stored_master_key_format_version, unknown_protection_marker, validate_key_id,
|
||||
};
|
||||
use crate::backup::capability::AtRestProtection;
|
||||
use crate::backup::dry_run::{
|
||||
@@ -608,6 +609,15 @@ fn decode_key_record(
|
||||
// Classify the protection marker before the schema parse: a record from a
|
||||
// newer build is not a damaged bundle, and reporting it as corruption
|
||||
// sends the operator into disaster recovery instead of a version change.
|
||||
let format_version = stored_master_key_format_version(&plaintext)
|
||||
.map_err(|error| BackupError::corrupted(format!("bundled key record '{stem}' is not a readable JSON object: {error}")))?;
|
||||
if format_version > STORED_MASTER_KEY_FORMAT_VERSION {
|
||||
return Err(BackupError::UnsupportedFormatVersion {
|
||||
key_id: stem,
|
||||
version: format_version.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let unknown_marker = unknown_protection_marker(&plaintext)
|
||||
.map_err(|error| BackupError::corrupted(format!("bundled key record '{stem}' is not a readable JSON object: {error}")))?;
|
||||
if let Some(version) = unknown_marker {
|
||||
@@ -2102,6 +2112,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_record_format_version_from_a_newer_build_is_not_reported_as_corruption() {
|
||||
let record = serde_json::json!({"format_version": 99});
|
||||
let error = match decode_key_record(
|
||||
"artifacts/keys/alpha.key.enc",
|
||||
Zeroizing::new(serde_json::to_vec(&record).expect("encode record")),
|
||||
&[AtRestProtection::EncryptedMasterKey],
|
||||
) {
|
||||
Ok(_) => panic!("a record this build cannot interpret must be rejected"),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
let KmsError::Backup(inner) = &error else {
|
||||
panic!("expected a backup error, got {error:?}");
|
||||
};
|
||||
assert!(
|
||||
matches!(inner, BackupError::UnsupportedFormatVersion { key_id, version }
|
||||
if key_id == "alpha" && version == "99"),
|
||||
"got {inner:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
RestoreBlocker::from(inner).code,
|
||||
RestoreBlockerCode::UnknownFormatVersion,
|
||||
"a dry run must report a version blocker, not a corruption blocker"
|
||||
);
|
||||
}
|
||||
|
||||
/// The commit marker's format-version branch, symmetric with the Vault
|
||||
/// restore marker's: an unknown version is refused outright everywhere the
|
||||
/// marker is read, and the backend refuses to start while it is present.
|
||||
|
||||
Reference in New Issue
Block a user