mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 19:42:17 +00:00
fix(kms): bound persisted format parsing (#5652)
fix(kms): harden persisted format compatibility
This commit is contained in:
@@ -49,8 +49,8 @@
|
||||
//! manifest, which decodes as an incomplete bundle and can never be restored.
|
||||
|
||||
use crate::backends::local::{
|
||||
LocalKmsClient, STORED_MASTER_KEY_FORMAT_VERSION, StoredKeyProtection, stored_master_key_format_version,
|
||||
unknown_protection_marker,
|
||||
LocalKmsClient, STORED_MASTER_KEY_FORMAT_VERSION, StoredKeyProtection, UNKNOWN_STORED_KEY_PROTECTION,
|
||||
has_unknown_protection_marker, stored_master_key_format_version,
|
||||
};
|
||||
use crate::backup::capability::{AtRestProtection, BackupBackendKind, BackupResponsibility};
|
||||
use crate::backup::error::BackupError;
|
||||
@@ -208,8 +208,7 @@ pub async fn export_local_backup(
|
||||
request: &LocalBackupExportRequest,
|
||||
) -> Result<BackupManifest> {
|
||||
request.validate()?;
|
||||
prepare_destination(&request.destination).await?;
|
||||
|
||||
validate_destination(&request.destination).await?;
|
||||
let snapshot = collect_snapshot(client).await?;
|
||||
if snapshot.records.is_empty() {
|
||||
return Err(KmsError::invalid_operation(
|
||||
@@ -236,6 +235,7 @@ pub async fn export_local_backup(
|
||||
None => None,
|
||||
};
|
||||
|
||||
prepare_destination(&request.destination).await?;
|
||||
let manifest = build_and_write_bundle(kek, request, &snapshot, master_key_verifier).await?;
|
||||
Ok(manifest)
|
||||
}
|
||||
@@ -389,11 +389,11 @@ async fn collect_snapshot(client: &LocalKmsClient) -> Result<CollectedSnapshot>
|
||||
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| {
|
||||
let has_unknown_marker = has_unknown_protection_marker(&raw).map_err(|error| {
|
||||
KmsError::material_corrupt(&stem, format!("stored key record is not a readable JSON object: {error}"))
|
||||
})?;
|
||||
if let Some(version) = unknown_marker {
|
||||
return Err(KmsError::unsupported_format_version(&stem, version));
|
||||
if has_unknown_marker {
|
||||
return Err(KmsError::unsupported_format_version(&stem, UNKNOWN_STORED_KEY_PROTECTION));
|
||||
}
|
||||
let probe: StoredRecordProbe = serde_json::from_slice(&raw)
|
||||
.map_err(|error| KmsError::material_corrupt(&stem, format!("stored key record does not deserialize: {error}")))?;
|
||||
@@ -619,7 +619,7 @@ fn local_kdf_descriptor(snapshot: &CollectedSnapshot, master_key_verifier: Optio
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare_destination(destination: &Path) -> Result<()> {
|
||||
async fn validate_destination(destination: &Path) -> Result<()> {
|
||||
if fs::try_exists(destination).await? {
|
||||
let mut entries = fs::read_dir(destination)
|
||||
.await
|
||||
@@ -630,6 +630,11 @@ async fn prepare_destination(destination: &Path) -> Result<()> {
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prepare_destination(destination: &Path) -> Result<()> {
|
||||
validate_destination(destination).await?;
|
||||
fs::create_dir_all(destination.join(KEYS_DIR)).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1146,7 +1151,7 @@ mod tests {
|
||||
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["at_rest_protection"] = serde_json::json!("post-quantum-v2");
|
||||
record["at_rest_protection"] = serde_json::json!("secret-marker-value-must-not-leak");
|
||||
std::fs::write(&record_path, serde_json::to_vec_pretty(&record).expect("encode record")).expect("write record");
|
||||
|
||||
let bundle = TempDir::new().expect("bundle dir");
|
||||
@@ -1155,9 +1160,10 @@ mod tests {
|
||||
.expect_err("an uninterpretable record must abort the export");
|
||||
assert!(
|
||||
matches!(&error, KmsError::UnsupportedFormatVersion { key_id, version }
|
||||
if key_id == "alpha" && version == "post-quantum-v2"),
|
||||
if key_id == "alpha" && version == UNKNOWN_STORED_KEY_PROTECTION),
|
||||
"got {error:?}"
|
||||
);
|
||||
assert!(!error.to_string().contains("secret-marker-value-must-not-leak"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1172,7 +1178,8 @@ mod tests {
|
||||
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")))
|
||||
let destination = bundle.path().join("bundle");
|
||||
let error = export_local_backup(&client, &test_kek(), &export_request(destination.clone()))
|
||||
.await
|
||||
.expect_err("a newer record format must abort the export");
|
||||
assert!(
|
||||
@@ -1180,5 +1187,17 @@ mod tests {
|
||||
if key_id == "alpha" && version == "99"),
|
||||
"got {error:?}"
|
||||
);
|
||||
assert!(
|
||||
!destination.exists(),
|
||||
"format validation must finish before the export creates its destination"
|
||||
);
|
||||
|
||||
record["format_version"] = serde_json::json!(STORED_MASTER_KEY_FORMAT_VERSION);
|
||||
std::fs::write(&record_path, serde_json::to_vec_pretty(&record).expect("encode supported record"))
|
||||
.expect("write supported record");
|
||||
export_local_backup(&client, &test_kek(), &export_request(destination.clone()))
|
||||
.await
|
||||
.expect("the same destination must remain usable after validation fails");
|
||||
assert!(destination.join(LOCAL_BUNDLE_MANIFEST_FILE).exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
|
||||
use crate::backends::local::{
|
||||
LOCAL_KMS_MASTER_KEY_SALT_FILE, LOCAL_KMS_MASTER_KEY_SALT_LEN, LOCAL_RESTORE_COMMIT_MARKER_FILE, LocalKmsClient,
|
||||
STORED_MASTER_KEY_FORMAT_VERSION, StoredKeyProtection, durable_file, is_orphan_commit_temp_name,
|
||||
stored_master_key_format_version, unknown_protection_marker, validate_key_id,
|
||||
STORED_MASTER_KEY_FORMAT_VERSION, StoredKeyProtection, UNKNOWN_STORED_KEY_PROTECTION, durable_file,
|
||||
has_unknown_protection_marker, is_orphan_commit_temp_name, stored_master_key_format_version, validate_key_id,
|
||||
};
|
||||
use crate::backup::capability::AtRestProtection;
|
||||
use crate::backup::dry_run::{
|
||||
@@ -618,10 +618,14 @@ fn decode_key_record(
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let unknown_marker = unknown_protection_marker(&plaintext)
|
||||
let has_unknown_marker = has_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 {
|
||||
return Err(BackupError::UnsupportedRecordVersion { key_id: stem, version }.into());
|
||||
if has_unknown_marker {
|
||||
return Err(BackupError::UnsupportedRecordVersion {
|
||||
key_id: stem,
|
||||
version: UNKNOWN_STORED_KEY_PROTECTION.to_owned(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let probe: RestoredRecordProbe = serde_json::from_slice(&plaintext)
|
||||
.map_err(|error| BackupError::corrupted(format!("bundled key record '{stem}' does not deserialize: {error}")))?;
|
||||
@@ -2084,7 +2088,7 @@ mod tests {
|
||||
fn bundled_record_from_a_newer_build_is_not_reported_as_corruption() {
|
||||
let record = serde_json::json!({
|
||||
"key_id": "alpha",
|
||||
"at_rest_protection": "post-quantum-v2",
|
||||
"at_rest_protection": "secret-marker-value-must-not-leak",
|
||||
"encrypted_key_material": "AAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"nonce": vec![0u8; 12],
|
||||
});
|
||||
@@ -2102,9 +2106,10 @@ mod tests {
|
||||
};
|
||||
assert!(
|
||||
matches!(inner, BackupError::UnsupportedRecordVersion { key_id, version }
|
||||
if key_id == "alpha" && version == "post-quantum-v2"),
|
||||
if key_id == "alpha" && version == UNKNOWN_STORED_KEY_PROTECTION),
|
||||
"got {inner:?}"
|
||||
);
|
||||
assert!(!error.to_string().contains("secret-marker-value-must-not-leak"));
|
||||
assert_eq!(
|
||||
RestoreBlocker::from(inner).code,
|
||||
RestoreBlockerCode::UnknownFormatVersion,
|
||||
|
||||
Reference in New Issue
Block a user