Files
rustfs/crates/kms/src/error.rs
T
Zhengchao An bd834297da feat(kms): add the data key rewrap primitive (#5607)
* feat(kms): add data key rewrap and wrapping inspection primitives

Rewrap re-protects an existing data key envelope with the master key's
current version without touching the data key itself, which is the
precondition for ever retiring an older version: until every envelope a
version wrapped has been moved off it, destroying that version orphans
every object whose data key it wrapped.

Adds KmsBackend::rewrap_data_key and its read-only counterpart
describe_data_key_wrapping, both gated by a new BackendCapabilities::rewrap
flag and defaulting to UnsupportedCapability. Vault KV2 unwraps with the
frozen version record that wrapped the envelope and re-wraps with the
current material; Vault Transit uses the native transit/rewrap endpoint so
the data key never enters this process.

No read or write path changes: nothing calls these yet.

* test(kms): cover the rewrap primitive against a scripted Vault

* fix(kms): resolve both key materials before the data key is unwrapped

Keeps every fallible step out of the window in which the plaintext data
key exists, so no error path can drop it without zeroizing it first.
2026-08-02 05:51:12 +00:00

417 lines
15 KiB
Rust

// 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.
//! KMS error types and result handling
use thiserror::Error;
/// Result type for KMS operations
pub type Result<T> = std::result::Result<T, KmsError>;
/// KMS runtime is unavailable for an encryption operation.
#[derive(Error, Debug, Clone, Copy)]
#[error("KMS encryption service is unavailable")]
pub struct KmsUnavailableError;
/// KMS error types covering all possible failure scenarios
#[derive(Error, Debug, Clone)]
pub enum KmsError {
/// Configuration errors
#[error("Configuration error: {message}")]
ConfigurationError { message: String },
/// Key not found
#[error("Key not found: {key_id}")]
KeyNotFound { key_id: String },
/// Invalid key format or content
#[error("Invalid key: {message}")]
InvalidKey { message: String },
/// Cryptographic operation failed
#[error("Cryptographic error in {operation}: {message}")]
CryptographicError { operation: String, message: String },
/// Backend communication error
#[error("Backend error: {message}")]
BackendError { message: String },
/// Access denied
#[error("Access denied: {message}")]
AccessDenied { message: String },
/// Key already exists
#[error("Key already exists: {key_id}")]
KeyAlreadyExists { key_id: String },
/// Invalid operation state
#[error("Invalid operation: {message}")]
InvalidOperation { message: String },
/// Internal error
#[error("Internal error: {message}")]
InternalError { message: String },
/// Serialization/deserialization error
#[error("Serialization error: {message}")]
SerializationError { message: String },
/// I/O error
#[error("I/O error: {message}")]
IoError { message: String },
/// Cache error
#[error("Cache error: {message}")]
CacheError { message: String },
/// Validation error
#[error("Validation error: {message}")]
ValidationError { message: String },
/// Unsupported algorithm
#[error("Unsupported algorithm: {algorithm}")]
UnsupportedAlgorithm { algorithm: String },
/// Invalid key size
#[error("Invalid key size: expected {expected}, got {actual}")]
InvalidKeySize { expected: usize, actual: usize },
/// Encryption context mismatch
#[error("Encryption context mismatch: {message}")]
ContextMismatch { message: String },
/// Backend operation exceeded its per-attempt timeout or total deadline
#[error("Operation timed out: {message}")]
OperationTimedOut { message: String },
/// Backend operation aborted by cancellation or shutdown
#[error("Operation cancelled: {message}")]
OperationCancelled { message: String },
// New variants must be appended below (never inserted above) so that
// concurrent additions rebase without conflicts.
/// Persisted key material is absent from an otherwise readable key record
#[error(
"Key material missing for key {key_id}: the stored record has no key material; restore it from backup or repair the key explicitly"
)]
MaterialMissing { key_id: String },
/// Persisted key material exists but cannot be decoded
#[error("Key material corrupt for key {key_id}: {message}")]
MaterialCorrupt { key_id: String, message: String },
/// Persisted key material failed authenticated decryption
#[error(
"Key material authentication failed for key {key_id}: the stored material cannot be decrypted with the configured master key"
)]
MaterialAuthenticationFailed { key_id: String },
/// Persisted key record uses a format version unknown to this build
#[error("Unsupported key format version {version:?} for key {key_id}")]
UnsupportedFormatVersion { key_id: String, version: String },
/// Requested master key version has no persisted material for the key
#[error("Key version {version} not found for key {key_id}")]
KeyVersionNotFound { key_id: String, version: u32 },
/// Backup/restore bundle contract violation; see [`crate::backup::BackupError`]
#[error(transparent)]
Backup(#[from] crate::backup::BackupError),
/// Operation is not supported by the active KMS backend
#[error("Operation '{operation}' is not supported by KMS backend '{backend}'")]
UnsupportedCapability { backend: String, operation: String },
/// Backend credentials expired or could not be refreshed in time; requests
/// fail closed instead of being sent with credentials that may lapse mid-flight
#[error("KMS credentials unavailable: {message}")]
CredentialsUnavailable { message: String },
/// Key has master key version records but no baseline version, so envelopes
/// written before versioned rotation can no longer be resolved
#[error(
"Baseline version lost for key {key_id}: master key version records exist (oldest {oldest_version}) but the key record carries no baseline version, so data keys written before versioned rotation can no longer be resolved to the master key version that wrapped them. A node older than versioned rotation rewrote the key record and dropped the field. Finish upgrading every node, restore baseline_version to {oldest_version} on the key record, then retry"
)]
BaselineVersionLost { key_id: String, oldest_version: u32 },
/// Configuration still points at the key, so its material must not be
/// destroyed. Distinct from the generic invalid-operation errors so that
/// callers can tell "this key is still wired into the deployment" apart
/// from a malformed request and act on the listed references.
#[error(
"Key {key_id} is still referenced by configuration and its material must not be destroyed: {}. Remove or repoint the listed configuration, then retry",
.references.join(", ")
)]
KeyStillReferenced { key_id: String, references: Vec<String> },
/// The only available way to rewrap this envelope would pull the plaintext
/// data key into the RustFS process. Refused rather than performed: the
/// point of a backend-side rewrap is that the data key stays inside the
/// backend, so silently falling back to unwrap-then-rewrap would hand back
/// a correct envelope while quietly dropping the property that justified
/// the operation.
#[error("Cannot rewrap a data key of key {key_id} without exposing its plaintext: {reason}")]
RewrapWouldExposePlaintext { key_id: String, reason: String },
}
impl KmsError {
/// Create a configuration error
pub fn configuration_error<S: Into<String>>(message: S) -> Self {
Self::ConfigurationError { message: message.into() }
}
/// Create a key not found error
pub fn key_not_found<S: Into<String>>(key_id: S) -> Self {
Self::KeyNotFound { key_id: key_id.into() }
}
/// Create an invalid key error
pub fn invalid_key<S: Into<String>>(message: S) -> Self {
Self::InvalidKey { message: message.into() }
}
/// Create a cryptographic error
pub fn cryptographic_error<S1: Into<String>, S2: Into<String>>(operation: S1, message: S2) -> Self {
Self::CryptographicError {
operation: operation.into(),
message: message.into(),
}
}
/// Create a backend error
pub fn backend_error<S: Into<String>>(message: S) -> Self {
Self::BackendError { message: message.into() }
}
/// Create access denied error
pub fn access_denied<S: Into<String>>(message: S) -> Self {
Self::AccessDenied { message: message.into() }
}
/// Create a key already exists error
pub fn key_already_exists<S: Into<String>>(key_id: S) -> Self {
Self::KeyAlreadyExists { key_id: key_id.into() }
}
/// Create an invalid operation error
pub fn invalid_operation<S: Into<String>>(message: S) -> Self {
Self::InvalidOperation { message: message.into() }
}
/// Create an internal error
pub fn internal_error<S: Into<String>>(message: S) -> Self {
Self::InternalError { message: message.into() }
}
/// Create a serialization error
pub fn serialization_error<S: Into<String>>(message: S) -> Self {
Self::SerializationError { message: message.into() }
}
/// Create an I/O error
pub fn io_error<S: Into<String>>(message: S) -> Self {
Self::IoError { message: message.into() }
}
/// Create a cache error
pub fn cache_error<S: Into<String>>(message: S) -> Self {
Self::CacheError { message: message.into() }
}
/// Create a validation error
pub fn validation_error<S: Into<String>>(message: S) -> Self {
Self::ValidationError { message: message.into() }
}
/// Create an invalid parameter error
pub fn invalid_parameter<S: Into<String>>(message: S) -> Self {
Self::InvalidOperation { message: message.into() }
}
/// Create an invalid key state error
pub fn invalid_key_state<S: Into<String>>(message: S) -> Self {
Self::InvalidOperation { message: message.into() }
}
/// Create an unsupported algorithm error
pub fn unsupported_algorithm<S: Into<String>>(algorithm: S) -> Self {
Self::UnsupportedAlgorithm {
algorithm: algorithm.into(),
}
}
/// Create an invalid key size error
pub fn invalid_key_size(expected: usize, actual: usize) -> Self {
Self::InvalidKeySize { expected, actual }
}
/// Create an encryption context mismatch error
pub fn context_mismatch<S: Into<String>>(message: S) -> Self {
Self::ContextMismatch { message: message.into() }
}
/// Create an operation timed out error
pub fn operation_timed_out<S: Into<String>>(message: S) -> Self {
Self::OperationTimedOut { message: message.into() }
}
/// Create an operation cancelled error
pub fn operation_cancelled<S: Into<String>>(message: S) -> Self {
Self::OperationCancelled { message: message.into() }
}
/// Create a still-referenced error
pub fn key_still_referenced<S: Into<String>>(key_id: S, references: Vec<String>) -> Self {
Self::KeyStillReferenced {
key_id: key_id.into(),
references,
}
}
/// Create a material missing error
pub fn material_missing<S: Into<String>>(key_id: S) -> Self {
Self::MaterialMissing { key_id: key_id.into() }
}
/// Create a material corrupt error
pub fn material_corrupt<S1: Into<String>, S2: Into<String>>(key_id: S1, message: S2) -> Self {
Self::MaterialCorrupt {
key_id: key_id.into(),
message: message.into(),
}
}
/// Create a material authentication failed error
pub fn material_authentication_failed<S: Into<String>>(key_id: S) -> Self {
Self::MaterialAuthenticationFailed { key_id: key_id.into() }
}
/// Create an unsupported format version error
pub fn unsupported_format_version<S1: Into<String>, S2: Into<String>>(key_id: S1, version: S2) -> Self {
Self::UnsupportedFormatVersion {
key_id: key_id.into(),
version: version.into(),
}
}
/// Create a key version not found error
pub fn key_version_not_found<S: Into<String>>(key_id: S, version: u32) -> Self {
Self::KeyVersionNotFound {
key_id: key_id.into(),
version,
}
}
/// Create an unsupported capability error
pub fn unsupported_capability<S1: Into<String>, S2: Into<String>>(backend: S1, operation: S2) -> Self {
Self::UnsupportedCapability {
backend: backend.into(),
operation: operation.into(),
}
}
/// Create a credentials unavailable error
pub fn credentials_unavailable<S: Into<String>>(message: S) -> Self {
Self::CredentialsUnavailable { message: message.into() }
}
/// Create a baseline version lost error
///
/// `oldest_version` is the lowest master key version that still has a
/// material record; it is exactly the baseline that was dropped, because
/// version records start at the baseline the first rotation froze.
pub fn baseline_version_lost<S: Into<String>>(key_id: S, oldest_version: u32) -> Self {
Self::BaselineVersionLost {
key_id: key_id.into(),
oldest_version,
}
}
/// Create a rewrap-would-expose-plaintext error
pub fn rewrap_would_expose_plaintext<S1: Into<String>, S2: Into<String>>(key_id: S1, reason: S2) -> Self {
Self::RewrapWouldExposePlaintext {
key_id: key_id.into(),
reason: reason.into(),
}
}
}
/// Convert from standard library errors
impl From<std::io::Error> for KmsError {
fn from(error: std::io::Error) -> Self {
Self::IoError {
message: error.to_string(),
}
}
}
impl From<serde_json::Error> for KmsError {
fn from(error: serde_json::Error) -> Self {
Self::SerializationError {
message: error.to_string(),
}
}
}
// Note: We can't implement From for both aes_gcm::Error and chacha20poly1305::Error
// because they might be the same type. Instead, we provide helper functions.
impl KmsError {
/// Create a KMS error from AES-GCM error
///
/// #Arguments
/// * `error` - The AES-GCM error to convert
///
/// #Returns
/// * `KmsError` - The corresponding KMS error
///
pub fn from_aes_gcm_error(error: aes_gcm::Error) -> Self {
Self::CryptographicError {
operation: "AES-GCM".to_string(),
message: error.to_string(),
}
}
/// Create a KMS error from ChaCha20-Poly1305 error
///
/// #Arguments
/// * `error` - The ChaCha20-Poly1305 error to convert
///
/// #Returns
/// * `KmsError` - The corresponding KMS error
///
pub fn from_chacha20_error(error: chacha20poly1305::Error) -> Self {
Self::CryptographicError {
operation: "ChaCha20-Poly1305".to_string(),
message: error.to_string(),
}
}
}
impl From<url::ParseError> for KmsError {
fn from(error: url::ParseError) -> Self {
Self::ConfigurationError {
message: format!("Invalid URL: {error}"),
}
}
}
impl From<reqwest::Error> for KmsError {
fn from(error: reqwest::Error) -> Self {
Self::BackendError {
message: format!("HTTP request failed: {error}"),
}
}
}