mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-02 03:19:19 +00:00
test(kms): cover Transit decrypt of pre-rotation data keys offline
Vault owns the transit crypto, so the offline responder cannot prove the round trip - that stays in the #[ignore] live test. What it can pin is the client-side wiring: after a rotation records the version bump, decrypting a data key generated before it must forward the historical vault:v1: ciphertext to Vault byte for byte and return the material Vault hands back.
This commit is contained in:
@@ -2258,4 +2258,89 @@ mod tests {
|
||||
"the sweep must not touch the transit key after backing off: {requests:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The forward half of the rotation contract on the Transit path: a data key
|
||||
/// generated before a rotation must still be decryptable after it.
|
||||
///
|
||||
/// Transit key material never leaves Vault, so an offline responder cannot
|
||||
/// prove the cryptographic round trip — `test_transit_old_ciphertext_decrypts_after_rotate`
|
||||
/// keeps that against a live Vault. What this pins is the wiring the client
|
||||
/// owns and could regress on its own: the historical `vault:v1:` ciphertext
|
||||
/// is forwarded to Vault byte for byte after the rotation bumped the
|
||||
/// recorded version, and nothing on the decrypt path gates on that version.
|
||||
#[tokio::test]
|
||||
async fn wired_transit_pre_rotation_data_key_is_decrypted_unchanged() {
|
||||
const CIPHERTEXT_V1: &str = "vault:v1:scripted-pre-rotation";
|
||||
const RECOVERED_DEK: [u8; 32] = [0x37u8; 32];
|
||||
let metadata = TransitKeyMetadata::from_create_request(&CreateKeyRequest::default());
|
||||
let (vault, client) = scripted_client(vec![
|
||||
// generate_data_key: state gate reads the metadata record, then the
|
||||
// transit encrypt returns first-version ciphertext.
|
||||
ScriptedResponse::ok(metadata_read_data(&metadata)),
|
||||
ScriptedResponse::ok(serde_json::json!({ "ciphertext": CIPHERTEXT_V1 })),
|
||||
// rotate: the state gate hits the metadata cache, the rotation
|
||||
// commits, and the versioned read+write records the version bump.
|
||||
ScriptedResponse::ok(serde_json::json!({})),
|
||||
ScriptedResponse::ok(kv2_metadata_read_data(1)),
|
||||
ScriptedResponse::ok(metadata_read_data(&metadata)),
|
||||
ScriptedResponse::ok(kv2_write_ack()),
|
||||
// decrypt of the pre-rotation envelope; Vault owns the transit
|
||||
// crypto, so the recovered material is the responder's to hand back.
|
||||
ScriptedResponse::ok(serde_json::json!({ "plaintext": BASE64.encode(RECOVERED_DEK) })),
|
||||
])
|
||||
.await;
|
||||
|
||||
let data_key = client
|
||||
.generate_data_key(
|
||||
&GenerateKeyRequest {
|
||||
master_key_id: "wired-key".to_string(),
|
||||
key_spec: "AES_256".to_string(),
|
||||
key_length: Some(32),
|
||||
encryption_context: HashMap::new(),
|
||||
grant_tokens: Vec::new(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("generate_data_key must produce an envelope");
|
||||
let envelope: DataKeyEnvelope = serde_json::from_slice(&data_key.ciphertext).expect("envelope must parse");
|
||||
assert_eq!(envelope.encrypted_key, CIPHERTEXT_V1.as_bytes());
|
||||
|
||||
let rotated = client.rotate_key("wired-key", None).await.expect("rotation must commit");
|
||||
assert_eq!(rotated.version, 2, "the rotation must record the version bump");
|
||||
|
||||
let plaintext = client
|
||||
.decrypt(
|
||||
&DecryptRequest {
|
||||
ciphertext: data_key.ciphertext.clone(),
|
||||
encryption_context: HashMap::new(),
|
||||
grant_tokens: Vec::new(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("a pre-rotation data key must stay decryptable");
|
||||
assert_eq!(
|
||||
plaintext,
|
||||
RECOVERED_DEK.to_vec(),
|
||||
"the decrypt must hand back the recovered material, not merely avoid an error"
|
||||
);
|
||||
|
||||
let requests = vault.requests();
|
||||
assert_eq!(requests.len(), 7, "{requests:?}");
|
||||
assert_eq!(requests[6], "POST /v1/transit/decrypt/wired-key", "{requests:?}");
|
||||
|
||||
// The version the rotation recorded, and the ciphertext the decrypt sent:
|
||||
// the client must forward the historical version verbatim instead of
|
||||
// re-stamping it to (or pinning the request at) the current one.
|
||||
let bodies = vault.request_bodies();
|
||||
let recorded: serde_json::Value = serde_json::from_str(&bodies[5]).expect("metadata write body must be JSON");
|
||||
assert_eq!(recorded["data"]["current_version"], serde_json::json!(2), "{recorded}");
|
||||
let decrypt_body: serde_json::Value = serde_json::from_str(&bodies[6]).expect("decrypt body must be JSON");
|
||||
assert_eq!(
|
||||
decrypt_body["ciphertext"],
|
||||
serde_json::json!(CIPHERTEXT_V1),
|
||||
"the pre-rotation ciphertext must reach Vault unchanged: {decrypt_body}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user