feat(kms): add master key version to data key envelope contract (#5480)

* fix(kms): restore vault backend test compilation after timeout parameter

PR #5472 added an attempt_timeout parameter to VaultKmsClient::new while
PR #5474 landed tests still using the one-argument form, leaving
'cargo test -p rustfs-kms' unable to compile on main. Pass the same
30-second timeout the surrounding integration tests already use.

* feat(kms): add master key version to data key envelope contract

DataKeyEnvelope gains an optional master_key_version field recording
which KEK version wrapped the DEK, so rotation-aware backends can load
the matching historical material on decrypt. The field is skipped when
None, keeping envelopes from non-rotating backends byte-identical to
the historical seven-field JSON shape, and legacy envelopes without the
field deserialize to None. The envelope discriminator marker is
untouched, so mixed-format routing is unchanged in both directions.

Adds the KeyVersionNotFound typed error for version-addressed material
lookups that must fail closed instead of falling back to the current
version.

Refs rustfs/backlog#1565
This commit is contained in:
Zhengchao An
2026-07-31 00:43:10 +08:00
committed by GitHub
parent 7662b2436a
commit 35a20622f1
6 changed files with 100 additions and 5 deletions
+3 -1
View File
@@ -942,7 +942,8 @@ impl KmsClient for LocalKmsClient {
// Encrypt the data key with the master key
let (encrypted_key, nonce) = self.encrypt_with_master_key(&request.master_key_id, &plaintext_key).await?;
// Create data key envelope with master key version for rotation support
// Local rotation is rejected, so every envelope is wrapped by the key's sole
// material and needs no master key version.
let envelope = DataKeyEnvelope {
key_id: uuid::Uuid::new_v4().to_string(),
master_key_id: request.master_key_id.clone(),
@@ -951,6 +952,7 @@ impl KmsClient for LocalKmsClient {
nonce,
encryption_context: request.encryption_context.clone(),
created_at: Zoned::now(),
master_key_version: None,
};
// Serialize the envelope as the ciphertext
+4
View File
@@ -137,6 +137,8 @@ impl KmsClient for StaticKmsBackend {
nonce: nonce_bytes.to_vec(),
encryption_context: request.encryption_context.clone(),
created_at: Zoned::now(),
// The static backend has a single fixed key with no rotation.
master_key_version: None,
};
let ciphertext = serde_json::to_vec(&envelope)?;
@@ -181,6 +183,8 @@ impl KmsClient for StaticKmsBackend {
nonce: nonce_bytes.to_vec(),
encryption_context: request.encryption_context.clone(),
created_at: Zoned::now(),
// The static backend has a single fixed key with no rotation.
master_key_version: None,
};
let ciphertext = serde_json::to_vec(&envelope)?;
+10 -3
View File
@@ -311,6 +311,7 @@ impl KmsClient for VaultKmsClient {
nonce,
encryption_context: request.encryption_context.clone(),
created_at: Zoned::now(),
master_key_version: None,
};
// Serialize the envelope as the ciphertext
@@ -938,7 +939,9 @@ mod tests {
async fn test_vault_kv2_rotate_key_rejected_without_touching_storage() {
// No Vault instance needed: rotation must be rejected before any storage access,
// so the call cannot read or overwrite key material.
let client = VaultKmsClient::new(integration_vault_config()).await.expect("client");
let client = VaultKmsClient::new(integration_vault_config(), Duration::from_secs(30))
.await
.expect("client");
let err = client
.rotate_key("any-key", None)
@@ -950,7 +953,9 @@ mod tests {
#[tokio::test]
async fn test_vault_kv2_backend_info_reports_at_rest_protection() {
let client = VaultKmsClient::new(integration_vault_config()).await.expect("client");
let client = VaultKmsClient::new(integration_vault_config(), Duration::from_secs(30))
.await
.expect("client");
let info = client.backend_info();
assert_eq!(info.backend_type, "vault-kv2");
@@ -962,7 +967,9 @@ mod tests {
#[tokio::test]
#[ignore] // Requires a running Vault instance (dev mode)
async fn test_vault_kv2_rotate_rejected_and_material_untouched() {
let client = VaultKmsClient::new(integration_vault_config()).await.expect("client");
let client = VaultKmsClient::new(integration_vault_config(), Duration::from_secs(30))
.await
.expect("client");
let key_id = format!("rotate-{}", uuid::Uuid::new_v4());
client.create_key(&key_id, "AES_256", None).await.expect("create");
+3
View File
@@ -406,6 +406,9 @@ impl KmsClient for VaultTransitKmsClient {
nonce: Vec::new(),
encryption_context: request.encryption_context.clone(),
created_at: Zoned::now(),
// Transit ciphertext already self-describes its key version
// ("vault:vN:..."), so the envelope never carries one.
master_key_version: None,
};
let ciphertext = serde_json::to_vec(&envelope)?;