// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Vault Transit-based KMS backend. use crate::backends::vault_credentials::{VaultClientHandle, VaultConnectionSettings, VaultCredentialProvider, token_source_for}; use crate::backends::{BackendCapabilities, BackendInfo, KmsBackend, KmsClient}; use crate::config::{KmsConfig, VaultTransitConfig}; use crate::encryption::{DataKeyEnvelope, generate_key_material}; use crate::error::{KmsError, Result}; use crate::types::*; use async_trait::async_trait; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use jiff::Zoned; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use vaultrs::{ api::transit::{ KeyType, requests::{ CreateKeyRequestBuilder, DecryptDataRequestBuilder, EncryptDataRequestBuilder, UpdateKeyConfigurationRequestBuilder, }, }, kv2, transit::{data, key}, }; #[derive(Debug, Clone)] struct TransitKeyMetadata { key_usage: KeyUsage, description: Option, tags: HashMap, key_state: KeyState, created_at: Zoned, deletion_date: Option, origin: String, created_by: Option, current_version: u32, } /// Serializable version of TransitKeyMetadata for KV v2 persistence. #[derive(Debug, Clone, Serialize, Deserialize)] struct TransitKeyMetadataPersisted { key_usage: KeyUsage, description: Option, tags: HashMap, key_state: KeyState, created_at: Zoned, deletion_date: Option, origin: String, created_by: Option, current_version: u32, } impl TransitKeyMetadata { fn from_create_request(request: &CreateKeyRequest) -> Self { Self { key_usage: request.key_usage.clone(), description: request.description.clone(), tags: request.tags.clone(), key_state: KeyState::Enabled, created_at: Zoned::now(), deletion_date: None, origin: request.origin.clone().unwrap_or_else(|| "VAULT_TRANSIT".to_string()), created_by: None, current_version: 1, } } fn synthesized() -> Self { Self { key_usage: KeyUsage::EncryptDecrypt, description: None, tags: HashMap::new(), key_state: KeyState::Enabled, created_at: Zoned::now(), deletion_date: None, origin: "VAULT_TRANSIT".to_string(), created_by: None, current_version: 1, } } } impl From for TransitKeyMetadataPersisted { fn from(m: TransitKeyMetadata) -> Self { Self { key_usage: m.key_usage, description: m.description, tags: m.tags, key_state: m.key_state, created_at: m.created_at, deletion_date: m.deletion_date, origin: m.origin, created_by: m.created_by, current_version: m.current_version, } } } impl From for TransitKeyMetadata { fn from(m: TransitKeyMetadataPersisted) -> Self { Self { key_usage: m.key_usage, description: m.description, tags: m.tags, key_state: m.key_state, created_at: m.created_at, deletion_date: m.deletion_date, origin: m.origin, created_by: m.created_by, current_version: m.current_version, } } } pub struct VaultTransitKmsClient { credentials: VaultCredentialProvider, config: VaultTransitConfig, /// KV v2 mount path for persisting transit key metadata metadata_kv_mount: String, /// Path prefix under metadata_kv_mount for storing transit key metadata records metadata_key_prefix: String, metadata_cache: RwLock>, } impl VaultTransitKmsClient { /// Create a new Vault Transit KMS client /// /// `attempt_timeout` caps every HTTP request issued through this client. pub async fn new(config: VaultTransitConfig, attempt_timeout: Duration) -> Result { let source = token_source_for(&config.auth_method)?; let settings = VaultConnectionSettings { address: config.address.clone(), namespace: config.namespace.clone(), attempt_timeout, }; let credentials = VaultCredentialProvider::new(settings, source).await?; Ok(Self { credentials, metadata_kv_mount: config.metadata_kv_mount.clone(), metadata_key_prefix: config.metadata_key_prefix.clone(), config, metadata_cache: RwLock::new(HashMap::new()), }) } /// Snapshot the authenticated Vault client for a single request. /// /// Every Vault call takes its own snapshot so a credential rotation /// applies to subsequent calls without interrupting in-flight ones. fn vault(&self) -> Arc { self.credentials.current() } fn canonicalize_context(encryption_context: &HashMap) -> Result> { if encryption_context.is_empty() { return Ok(None); } let ordered: BTreeMap<_, _> = encryption_context .iter() .map(|(key, value)| (key.clone(), value.clone())) .collect(); let serialized = serde_json::to_vec(&ordered)?; Ok(Some(BASE64.encode(serialized))) } fn map_vault_error(key_id: &str, error: vaultrs::error::ClientError, operation: &str) -> Result { match error { vaultrs::error::ClientError::ResponseWrapError => Err(KmsError::key_not_found(key_id)), vaultrs::error::ClientError::APIError { code: 404, .. } => Err(KmsError::key_not_found(key_id)), other => Err(KmsError::backend_error(format!( "Vault Transit {operation} failed for key {key_id}: {other}" ))), } } async fn read_transit_key(&self, key_id: &str) -> Result { key::read(&self.vault().client, &self.config.mount_path, key_id) .await .or_else(|e| Self::map_vault_error(key_id, e, "read")) } async fn create_transit_key(&self, key_id: &str) -> Result<()> { let mut builder = CreateKeyRequestBuilder::default(); builder.key_type(KeyType::Aes256Gcm96); key::create(&self.vault().client, &self.config.mount_path, key_id, Some(&mut builder)) .await .map_err(|e| KmsError::backend_error(format!("Failed to create Vault Transit key {key_id}: {e}"))) } async fn transit_encrypt( &self, key_id: &str, plaintext: &[u8], encryption_context: &HashMap, ) -> Result { let plaintext_b64 = BASE64.encode(plaintext); let mut builder = EncryptDataRequestBuilder::default(); if let Some(aad) = Self::canonicalize_context(encryption_context)? { builder.associated_data(aad); } let response = data::encrypt(&self.vault().client, &self.config.mount_path, key_id, &plaintext_b64, Some(&mut builder)) .await .map_err(|e| KmsError::backend_error(format!("Failed to encrypt data with Vault Transit key {key_id}: {e}")))?; Ok(response.ciphertext) } async fn transit_decrypt( &self, key_id: &str, ciphertext: &str, encryption_context: &HashMap, ) -> Result> { let mut builder = DecryptDataRequestBuilder::default(); if let Some(aad) = Self::canonicalize_context(encryption_context)? { builder.associated_data(aad); } let response = data::decrypt(&self.vault().client, &self.config.mount_path, key_id, ciphertext, Some(&mut builder)) .await .map_err(|e| KmsError::backend_error(format!("Failed to decrypt data with Vault Transit key {key_id}: {e}")))?; BASE64 .decode(response.plaintext) .map_err(|e| KmsError::cryptographic_error("base64_decode", e.to_string())) } fn metadata_key_path(&self, key_id: &str) -> String { format!("{}/{}", self.metadata_key_prefix, key_id) } async fn read_metadata_from_kv(&self, key_id: &str) -> Result> { let path = self.metadata_key_path(key_id); match kv2::read::(&self.vault().client, &self.metadata_kv_mount, &path).await { Ok(persisted) => Ok(Some(persisted.into())), Err(vaultrs::error::ClientError::ResponseWrapError) | Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => Ok(None), Err(e) => Err(KmsError::backend_error(format!("Failed to read transit key metadata from Vault KV: {e}"))), } } async fn write_metadata_to_kv(&self, key_id: &str, metadata: &TransitKeyMetadata) -> Result<()> { let path = self.metadata_key_path(key_id); let persisted: TransitKeyMetadataPersisted = metadata.clone().into(); kv2::set(&self.vault().client, &self.metadata_kv_mount, &path, &persisted) .await .map(|_| ()) .map_err(|e| KmsError::backend_error(format!("Failed to write transit key metadata to Vault KV: {e}"))) } async fn delete_metadata_from_kv(&self, key_id: &str) -> Result<()> { let path = self.metadata_key_path(key_id); match kv2::delete_metadata(&self.vault().client, &self.metadata_kv_mount, &path).await { Ok(_) => Ok(()), Err(vaultrs::error::ClientError::ResponseWrapError) | Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => Ok(()), Err(e) => Err(KmsError::backend_error(format!( "Failed to delete transit key metadata from Vault KV: {e}" ))), } } async fn get_key_metadata(&self, key_id: &str) -> Result { // Check in-memory cache first. if let Some(metadata) = self.metadata_cache.read().await.get(key_id).cloned() { return Ok(metadata); } // On cache miss, try reading from the persistent KV store. if let Some(persisted) = self.read_metadata_from_kv(key_id).await? { self.metadata_cache .write() .await .insert(key_id.to_string(), persisted.clone()); return Ok(persisted); } // Deliberate exemption from the "read paths never write" rule (rustfs#4256 / // rustfs#4262): transit keys created before metadata persistence existed have no // KV record at all, so failing closed here would brick every pre-existing transit // key. The synthesised record only describes metadata — key material lives solely // inside Vault's transit engine and is never generated or written by this path. // // Verify the transit key actually exists in Vault before synthesising. self.read_transit_key(key_id).await?; let metadata = TransitKeyMetadata::synthesized(); // Persist the synthesised metadata so future cache misses pick it up (best // effort: the KV write failing must not fail the read). let _ = self.write_metadata_to_kv(key_id, &metadata).await; self.metadata_cache.write().await.insert(key_id.to_string(), metadata.clone()); Ok(metadata) } async fn store_key_metadata(&self, key_id: &str, metadata: &TransitKeyMetadata) -> Result<()> { self.write_metadata_to_kv(key_id, metadata).await?; self.metadata_cache.write().await.insert(key_id.to_string(), metadata.clone()); Ok(()) } async fn delete_key_metadata(&self, key_id: &str) -> Result<()> { self.delete_metadata_from_kv(key_id).await?; self.metadata_cache.write().await.remove(key_id); Ok(()) } async fn key_info(&self, key_id: &str) -> Result { self.read_transit_key(key_id).await?; let metadata = self.get_key_metadata(key_id).await?; Ok(KeyInfo { key_id: key_id.to_string(), description: metadata.description.clone(), algorithm: "AES_256".to_string(), usage: metadata.key_usage.clone(), status: match metadata.key_state { KeyState::Enabled => KeyStatus::Active, KeyState::Disabled => KeyStatus::Disabled, KeyState::PendingDeletion => KeyStatus::PendingDeletion, KeyState::PendingImport | KeyState::Unavailable => KeyStatus::Deleted, }, version: metadata.current_version, metadata: metadata.tags.clone(), tags: metadata.tags, created_at: metadata.created_at, rotated_at: None, created_by: metadata.created_by, }) } async fn key_metadata_response(&self, key_id: &str) -> Result { self.read_transit_key(key_id).await?; let metadata = self.get_key_metadata(key_id).await?; Ok(KeyMetadata { key_id: key_id.to_string(), key_state: metadata.key_state, key_usage: metadata.key_usage, description: metadata.description, creation_date: metadata.created_at, deletion_date: metadata.deletion_date, origin: metadata.origin, key_manager: "VAULT_TRANSIT".to_string(), tags: metadata.tags, }) } async fn ensure_key_active(&self, key_id: &str) -> Result { let metadata = self.get_key_metadata(key_id).await?; if metadata.key_state != KeyState::Enabled { return Err(KmsError::invalid_operation(format!( "Key {key_id} is not active (state: {:?})", metadata.key_state ))); } Ok(metadata) } } #[async_trait] impl KmsClient for VaultTransitKmsClient { async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result { self.ensure_key_active(&request.master_key_id).await?; let plaintext_key = generate_key_material(&request.key_spec)?; let encrypted_key = self .transit_encrypt(&request.master_key_id, &plaintext_key, &request.encryption_context) .await?; let envelope = DataKeyEnvelope { key_id: uuid::Uuid::new_v4().to_string(), master_key_id: request.master_key_id.clone(), key_spec: request.key_spec.clone(), encrypted_key: encrypted_key.into_bytes(), 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)?; Ok(DataKeyInfo::new( envelope.key_id, 1, Some(plaintext_key), ciphertext, request.key_spec.clone(), )) } async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result { let metadata = self.ensure_key_active(&request.key_id).await?; let ciphertext = self .transit_encrypt(&request.key_id, &request.plaintext, &request.encryption_context) .await?; Ok(EncryptResponse { ciphertext: ciphertext.into_bytes(), key_id: request.key_id.clone(), key_version: metadata.current_version, algorithm: "vault-transit".to_string(), }) } async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result> { let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext) .map_err(|e| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {e}")))?; for (key, expected_value) in &envelope.encryption_context { if let Some(actual_value) = request.encryption_context.get(key) { if actual_value != expected_value { return Err(KmsError::context_mismatch(format!( "Context mismatch for key '{key}': expected '{expected_value}', got '{actual_value}'" ))); } } else if !request.encryption_context.is_empty() { return Err(KmsError::context_mismatch(format!("Missing context key '{key}'"))); } } let encrypted_key = std::str::from_utf8(&envelope.encrypted_key) .map_err(|e| KmsError::cryptographic_error("utf8", format!("Invalid Transit ciphertext: {e}")))?; self.transit_decrypt(&envelope.master_key_id, encrypted_key, &envelope.encryption_context) .await } async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result { if algorithm != "AES_256" { return Err(KmsError::unsupported_algorithm(algorithm)); } if self.read_transit_key(key_id).await.is_ok() { return Err(KmsError::key_already_exists(key_id)); } self.create_transit_key(key_id).await?; let metadata = TransitKeyMetadata { created_by: Some("vault-transit".to_string()), ..TransitKeyMetadata::from_create_request(&CreateKeyRequest { key_name: Some(key_id.to_string()), ..Default::default() }) }; self.store_key_metadata(key_id, &metadata).await?; Ok(MasterKeyInfo { key_id: key_id.to_string(), version: metadata.current_version, algorithm: algorithm.to_string(), usage: metadata.key_usage, status: KeyStatus::Active, description: metadata.description, metadata: metadata.tags, created_at: metadata.created_at, rotated_at: None, created_by: metadata.created_by, }) } async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { self.key_info(key_id).await } async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result { let all_keys = key::list(&self.vault().client, &self.config.mount_path) .await .map_err(|e| KmsError::backend_error(format!("Failed to list Vault Transit keys: {e}")))? .keys; let mut filtered = Vec::new(); for key_id in all_keys { let key_info = self.key_info(&key_id).await?; let usage_matches = request.usage_filter.as_ref().is_none_or(|usage| usage == &key_info.usage); let status_matches = request.status_filter.as_ref().is_none_or(|status| status == &key_info.status); if usage_matches && status_matches { filtered.push(key_info); } } let start_idx = request .marker .as_ref() .and_then(|marker| filtered.iter().position(|info| &info.key_id == marker)) .map(|idx| idx + 1) .unwrap_or(0); let limit = request.limit.unwrap_or(100) as usize; let end_idx = std::cmp::min(start_idx + limit, filtered.len()); let keys = filtered[start_idx..end_idx].to_vec(); let next_marker = if end_idx < filtered.len() { Some(filtered[end_idx - 1].key_id.clone()) } else { None }; Ok(ListKeysResponse { keys, next_marker, truncated: end_idx < filtered.len(), }) } async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { let mut metadata = self.get_key_metadata(key_id).await?; metadata.key_state = KeyState::Enabled; metadata.deletion_date = None; self.store_key_metadata(key_id, &metadata).await } async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { let mut metadata = self.get_key_metadata(key_id).await?; metadata.key_state = KeyState::Disabled; self.store_key_metadata(key_id, &metadata).await } async fn schedule_key_deletion( &self, key_id: &str, pending_window_days: u32, _context: Option<&OperationContext>, ) -> Result<()> { let mut metadata = self.get_key_metadata(key_id).await?; metadata.key_state = KeyState::PendingDeletion; metadata.deletion_date = Some(Zoned::now() + Duration::from_secs(pending_window_days as u64 * 86400)); self.store_key_metadata(key_id, &metadata).await } async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { let mut metadata = self.get_key_metadata(key_id).await?; metadata.key_state = KeyState::Enabled; metadata.deletion_date = None; self.store_key_metadata(key_id, &metadata).await } async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { key::rotate(&self.vault().client, &self.config.mount_path, key_id) .await .map_err(|e| KmsError::backend_error(format!("Failed to rotate Vault Transit key {key_id}: {e}")))?; let mut metadata = self.get_key_metadata(key_id).await?; metadata.current_version += 1; self.store_key_metadata(key_id, &metadata).await?; Ok(MasterKeyInfo { key_id: key_id.to_string(), version: metadata.current_version, algorithm: "AES_256".to_string(), usage: metadata.key_usage, status: KeyStatus::Active, description: metadata.description, metadata: metadata.tags, created_at: metadata.created_at, rotated_at: Some(Zoned::now()), created_by: metadata.created_by, }) } async fn health_check(&self) -> Result<()> { key::list(&self.vault().client, &self.config.mount_path) .await .map(|_| ()) .map_err(|e| KmsError::backend_error(format!("Vault Transit health check failed: {e}"))) } fn backend_info(&self) -> BackendInfo { BackendInfo::new("vault-transit".to_string(), "0.1.0".to_string(), self.config.address.clone(), true) .with_metadata("mount_path".to_string(), self.config.mount_path.clone()) } } pub struct VaultTransitKmsBackend { client: VaultTransitKmsClient, } impl VaultTransitKmsBackend { pub async fn new(config: KmsConfig) -> Result { config.validate()?; let vault_config = match &config.backend_config { crate::config::BackendConfig::VaultTransit(vault_config) => (**vault_config).clone(), crate::config::BackendConfig::VaultKv2(vault_config) => VaultTransitConfig { address: vault_config.address.clone(), auth_method: vault_config.auth_method.clone(), namespace: vault_config.namespace.clone(), mount_path: vault_config.mount_path.clone(), metadata_kv_mount: vault_config.kv_mount.clone(), metadata_key_prefix: vault_config.key_path_prefix.clone(), tls: vault_config.tls.clone(), }, crate::config::BackendConfig::Local(_) | crate::config::BackendConfig::Static(_) => { return Err(KmsError::configuration_error("Expected Vault Transit backend configuration")); } }; let client = VaultTransitKmsClient::new(vault_config, config.effective_timeout()).await?; Ok(Self { client }) } } #[async_trait] impl KmsBackend for VaultTransitKmsBackend { async fn create_key(&self, request: CreateKeyRequest) -> Result { let key_id = request.key_name.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); if self.client.read_transit_key(&key_id).await.is_ok() { return Err(KmsError::key_already_exists(&key_id)); } self.client.create_transit_key(&key_id).await?; let metadata = TransitKeyMetadata::from_create_request(&request); self.client.store_key_metadata(&key_id, &metadata).await?; Ok(CreateKeyResponse { key_id: key_id.clone(), key_metadata: KeyMetadata { key_id, key_state: metadata.key_state, key_usage: metadata.key_usage, description: metadata.description, creation_date: metadata.created_at, deletion_date: metadata.deletion_date, origin: metadata.origin, key_manager: "VAULT_TRANSIT".to_string(), tags: metadata.tags, }, }) } async fn encrypt(&self, request: EncryptRequest) -> Result { self.client.encrypt(&request, None).await } async fn decrypt(&self, request: DecryptRequest) -> Result { let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)?; let plaintext = self.client.decrypt(&request, None).await?; Ok(DecryptResponse { plaintext, key_id: envelope.master_key_id, encryption_algorithm: Some("vault-transit".to_string()), }) } async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result { let generate_request = GenerateKeyRequest { master_key_id: request.key_id.clone(), key_spec: request.key_spec.as_str().to_string(), key_length: Some(request.key_spec.key_size() as u32), encryption_context: request.encryption_context, grant_tokens: Vec::new(), }; let data_key = self.client.generate_data_key(&generate_request, None).await?; let plaintext_key = data_key.plaintext.clone().unwrap_or_default(); let ciphertext_blob = data_key.ciphertext.clone(); Ok(GenerateDataKeyResponse { key_id: request.key_id, plaintext_key, ciphertext_blob, }) } async fn describe_key(&self, request: DescribeKeyRequest) -> Result { Ok(DescribeKeyResponse { key_metadata: self.client.key_metadata_response(&request.key_id).await?, }) } async fn list_keys(&self, request: ListKeysRequest) -> Result { self.client.list_keys(&request, None).await } async fn delete_key(&self, request: DeleteKeyRequest) -> Result { let key_id = request.key_id; let mut key_metadata = self.client.key_metadata_response(&key_id).await?; let deletion_date = if request.force_immediate.unwrap_or(false) { if key_metadata.key_state == KeyState::PendingDeletion { if !self.client.read_transit_key(&key_id).await?.deletion_allowed { let mut update_builder = UpdateKeyConfigurationRequestBuilder::default(); update_builder.deletion_allowed(true); key::update( &self.client.vault().client, &self.client.config.mount_path, &key_id, Some(&mut update_builder), ) .await .map_err(|e| { KmsError::backend_error(format!("Failed to allow deletion of Vault Transit key {key_id}: {e}")) })?; } key::delete(&self.client.vault().client, &self.client.config.mount_path, &key_id) .await .map_err(|e| KmsError::backend_error(format!("Failed to delete Vault Transit key {key_id}: {e}")))?; self.client.delete_key_metadata(&key_id).await?; None } else { let mut metadata = self.client.get_key_metadata(&key_id).await?; metadata.key_state = KeyState::PendingDeletion; metadata.deletion_date = Some(Zoned::now()); self.client.store_key_metadata(&key_id, &metadata).await?; key_metadata = self.client.key_metadata_response(&key_id).await?; None } } else { let days = request.pending_window_in_days.unwrap_or(30); if !(7..=30).contains(&days) { return Err(KmsError::invalid_parameter("pending_window_in_days must be between 7 and 30")); } let mut metadata = self.client.get_key_metadata(&key_id).await?; let scheduled = Zoned::now() + Duration::from_secs(days as u64 * 86400); metadata.key_state = KeyState::PendingDeletion; metadata.deletion_date = Some(scheduled.clone()); self.client.store_key_metadata(&key_id, &metadata).await?; key_metadata = self.client.key_metadata_response(&key_id).await?; Some(scheduled.to_string()) }; Ok(DeleteKeyResponse { key_id, deletion_date, key_metadata, }) } async fn cancel_key_deletion(&self, request: CancelKeyDeletionRequest) -> Result { let mut metadata = self.client.get_key_metadata(&request.key_id).await?; if metadata.key_state != KeyState::PendingDeletion { return Err(KmsError::invalid_key_state(format!("Key {} is not pending deletion", request.key_id))); } metadata.key_state = KeyState::Enabled; metadata.deletion_date = None; self.client.store_key_metadata(&request.key_id, &metadata).await?; Ok(CancelKeyDeletionResponse { key_id: request.key_id.clone(), key_metadata: self.client.key_metadata_response(&request.key_id).await?, }) } async fn health_check(&self) -> Result { self.client.health_check().await.map(|_| true) } fn capabilities(&self) -> BackendCapabilities { // Vault Transit natively supports version-retaining rotation, keeps // prior versions addressable for decryption, and allows physical // deletion once a key is pending deletion. BackendCapabilities::minimal() .with_rotate(true) .with_enable_disable(true) .with_schedule_deletion(true) .with_versioning(true) .with_physical_delete(true) } } #[cfg(test)] mod tests { use super::*; use crate::config::{ DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX, DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT, VaultAuthMethod, VaultTransitConfig, }; use crate::types::KeyStatus; fn test_vault_transit_config() -> VaultTransitConfig { VaultTransitConfig { address: "http://127.0.0.1:8200".to_string(), auth_method: VaultAuthMethod::Token { token: std::env::var("RUSTFS_KMS_VAULT_TOKEN").unwrap_or_else(|_| "dev-token".to_string()), }, namespace: None, mount_path: "transit".to_string(), metadata_kv_mount: DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT.to_string(), metadata_key_prefix: DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX.to_string(), tls: None, } } /// Regression test for rustfs/backlog#808. /// /// VaultTransit stores key metadata (state, tags, etc.) ONLY in an in-memory /// `metadata_cache`. On a cache miss — including after any server restart — /// `get_key_metadata()` synthesises a fresh record with `key_state: Enabled`. /// This means a disabled/deleted key silently revives as Enabled after restart. #[tokio::test] #[ignore] // Requires a running Vault instance with transit engine enabled async fn test_transit_key_state_lost_after_restart_simulation() { let config = test_vault_transit_config(); // --- First "process": create a key and disable it --- let client1 = VaultTransitKmsClient::new(config.clone(), Duration::from_secs(30)) .await .expect("Failed to create VaultTransit client"); let key_id = format!("regression-808-{}", uuid::Uuid::new_v4()); // Create key → Enabled let created = client1.create_key(&key_id, "AES_256", None).await.expect("create_key"); assert_eq!(created.status, KeyStatus::Active, "newly created key must be Active"); let info = client1 .describe_key(&key_id, None) .await .expect("describe_key before disable"); assert_eq!(info.status, KeyStatus::Active, "key must be Active before disable"); // Disable the key client1.disable_key(&key_id, None).await.expect("disable_key"); let info_after_disable = client1.describe_key(&key_id, None).await.expect("describe_key after disable"); assert_eq!(info_after_disable.status, KeyStatus::Disabled, "key must be Disabled after disable_key"); // --- Simulate restart: create a brand new client with empty cache --- let client2 = VaultTransitKmsClient::new(config, Duration::from_secs(30)) .await .expect("Failed to create second VaultTransit client (restart simulation)"); // After "restart", the key must remain Disabled because KV-persisted metadata // survives across client recreation. let info_after_restart = client2 .describe_key(&key_id, None) .await .expect("describe_key after restart simulation"); assert_eq!( info_after_restart.status, KeyStatus::Disabled, "after restart, a disabled key must remain Disabled" ); // Cleanup: schedule the key for deletion so Vault state is clean for the next run. let _ = client2.schedule_key_deletion(&key_id, 7, None).await; } /// Regression test for rustfs/backlog#808. /// /// PendingDeletion must be persisted outside the process-local metadata cache. /// Otherwise, a restart would synthesize Enabled metadata and allow new key use. #[tokio::test] #[ignore] // Requires a running Vault instance with transit engine enabled async fn test_transit_pending_deletion_survives_restart_simulation() { let config = test_vault_transit_config(); let client1 = VaultTransitKmsClient::new(config.clone(), Duration::from_secs(30)) .await .expect("Failed to create VaultTransit client"); let key_id = format!("regression-808-pending-{}", uuid::Uuid::new_v4()); let created = client1.create_key(&key_id, "AES_256", None).await.expect("create_key"); assert_eq!(created.status, KeyStatus::Active, "newly created key must be Active"); client1 .schedule_key_deletion(&key_id, 7, None) .await .expect("schedule_key_deletion"); let info_after_schedule = client1 .describe_key(&key_id, None) .await .expect("describe_key after schedule_key_deletion"); assert_eq!( info_after_schedule.status, KeyStatus::PendingDeletion, "key must be PendingDeletion after schedule_key_deletion" ); let client2 = VaultTransitKmsClient::new(config, Duration::from_secs(30)) .await .expect("Failed to create second VaultTransit client (restart simulation)"); let info_after_restart = client2 .describe_key(&key_id, None) .await .expect("describe_key after restart simulation"); assert_eq!( info_after_restart.status, KeyStatus::PendingDeletion, "after restart, a pending-deletion key must remain PendingDeletion" ); let generate_result = client2 .generate_data_key( &GenerateKeyRequest { master_key_id: key_id, key_spec: "AES_256".to_string(), key_length: Some(32), encryption_context: HashMap::new(), grant_tokens: Vec::new(), }, None, ) .await; assert!( generate_result.is_err(), "after restart, a pending-deletion key must not be usable for new data keys" ); } /// Contract regression for rustfs/backlog#1565. /// /// Transit rotation is delegated entirely to Vault's own key versioning: the /// ciphertext self-describes the wrapping version ("vault:vN:..."), so historical /// ciphertext must keep decrypting after rotation without any RustFS-side /// version bookkeeping in the envelope. #[tokio::test] #[ignore] // Requires a running Vault instance with transit engine enabled async fn test_transit_old_ciphertext_decrypts_after_rotate() { let client = VaultTransitKmsClient::new(test_vault_transit_config(), Duration::from_secs(30)) .await .expect("Failed to create VaultTransit client"); let key_id = format!("regression-1565-rotate-{}", uuid::Uuid::new_v4()); client.create_key(&key_id, "AES_256", None).await.expect("create_key"); let request = GenerateKeyRequest { master_key_id: key_id.clone(), key_spec: "AES_256".to_string(), key_length: Some(32), encryption_context: HashMap::new(), grant_tokens: Vec::new(), }; let dk_v1 = client.generate_data_key(&request, None).await.expect("generate under v1"); let env_v1: DataKeyEnvelope = serde_json::from_slice(&dk_v1.ciphertext).expect("parse v1 envelope"); assert!( env_v1.encrypted_key.starts_with(b"vault:v1:"), "first-version Transit ciphertext must carry the vault:v1: prefix" ); assert_eq!( env_v1.master_key_version, None, "Transit envelopes must not carry a RustFS-side master key version" ); let rotated = client.rotate_key(&key_id, None).await.expect("rotate_key"); assert_eq!(rotated.version, 2, "rotation must advance the Transit key version"); let dk_v2 = client.generate_data_key(&request, None).await.expect("generate under v2"); let env_v2: DataKeyEnvelope = serde_json::from_slice(&dk_v2.ciphertext).expect("parse v2 envelope"); assert!( env_v2.encrypted_key.starts_with(b"vault:v2:"), "post-rotation Transit ciphertext must carry the vault:v2: prefix" ); // Historical ciphertext keeps decrypting per Vault's version semantics, // interleaved with post-rotation ciphertext. for (data_key, label) in [(&dk_v1, "v1"), (&dk_v2, "v2"), (&dk_v1, "v1 again")] { let plaintext = client .decrypt( &DecryptRequest { ciphertext: data_key.ciphertext.clone(), encryption_context: HashMap::new(), grant_tokens: Vec::new(), }, None, ) .await .unwrap_or_else(|error| panic!("{label} ciphertext must stay decryptable after rotation: {error}")); assert_eq!(Some(plaintext), data_key.plaintext, "{label} plaintext must round-trip"); } // Cleanup so repeated runs against the same Vault do not accumulate keys. let _ = client.schedule_key_deletion(&key_id, 7, None).await; } }