mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-07 22:03:14 +00:00
62cc19e937
* Add black-box behavior tests for KMS resilience and serialization * fix(kms): repair unopenable ciphertext across backends Black-box testing of the KMS crate surfaced several defects that make encrypted data permanently unreadable. Symmetric envelopes. The Local and Vault Transit backends returned raw cipher output from `encrypt` while `decrypt` parsed a JSON envelope, so anything sealed through the master-key path could never be opened again. Local also discarded the AES-GCM nonce. Both now emit the same envelope `decrypt` consumes, matching the Static backend. Deterministic AAD. The object layer derived AEAD additional data by serializing a `HashMap` directly. Iteration order differs per instance, so a context rebuilt from storage produced different AAD bytes than the one used to seal and the object stopped opening. Ordering by key removes that dependency, matching the Static backend's existing `context_aad`. Objects written with the default single-key context are unaffected, since a one-entry map has only one serialization. Cipher in the header projection. `metadata_to_headers` recorded the SSE mode (`AES256` / `aws:kms`), which cannot represent ChaCha20-Poly1305, so a ChaCha-sealed object came back claiming `aws:kms` and was opened with the wrong cipher. The cipher now travels in `x-rustfs-encryption-algorithm` — the header the storage layer already reads but nothing ever wrote. Objects without it fall back as before. Also: the Static backend ignored `key_spec` and always issued 256-bit data keys; Local `list_keys` hardcoded `truncated: false`, ignored `marker`, and paginated over unordered `read_dir`, so a paginating client silently saw a partial key list; and Local and Vault KV2 reported `key_id: "unknown"` from `decrypt` despite the envelope naming the master key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(kms): cover both Vault backends and key rotation The behavior suite ran only against Local and Static, and its own harness documented the gap: the Vault backends had no business-capability coverage at all. Setting `RUSTFS_KMS_VAULT_TOKEN` now adds Vault KV2 and Vault Transit to every `for_each_backend` spec against a live server. That lane is what surfaced the Transit envelope defect fixed in the previous commit. `rotate` and `versioning` are advertised only by the Vault backends, so until now every capability-gated branch for them took the `UnsupportedCapability` side and the working half was never asserted — a rotation that dropped prior key versions would have gone green. The new `behavior_rotation.rs` pins that half: material sealed before a rotation still opens after it, repeated rotations accumulate versions rather than overwriting a single spare, and the history survives a restart. Two test defects fixed. `objects_round_trip_across_sizes_and_algorithms` asserted a 1-byte object differs from its own ciphertext, which collides once every 256 runs; the assertion now applies only where a collision is not realistic, and small objects stay covered by the tag check and the decrypt round-trip. `test_from_env_selects_token_file` depended on `RUSTFS_KMS_VAULT_TOKEN` being absent from the caller's environment and now clears it explicitly. The snapshots directory was also removed from `.gitignore`: insta snapshots are the assertions themselves, so leaving them untracked gives CI nothing to compare against. Only `.snap.new` scratch files are ignored now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(kms): adapt behavior suite to current key APIs Rebasing onto main brought four API changes the suite predates. `DeleteKeyRequest` gained `confirm_key_id`, and immediate deletion is now gated on the server's `allow_immediate_deletion`. Scheduled deletions pass `None`; the four specs that destroy a key outright echo the key id back and opt the harness config in, which is what the gate asks of a real caller. `LocalBackupExportRequest` gained `sanitized_config`. These specs cover the key-material path, so they seal no configuration and pass `None`. `KmsCacheStats` became a named struct with real hit, miss, and eviction counters. `cache_stats_returns_an_entry_count_and_no_hit_or_miss_data` existed to pin the old placeholder behavior — that the second tuple element was always zero — which main has since fixed, so it is now `cache_stats_reports_hits_and_misses_separately` and asserts the counters actually move. Starting the service provisions the reserved probe key, so it shows up in listings and backup bundles. Exact-set assertions filter it through a new `without_probe_key` helper rather than naming it, keeping those specs about the keys they seeded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(kms): bind the AAD to the stored context bytes Review caught that canonicalizing the AAD on decrypt breaks objects sealed before canonicalization existed, and it was right. The AAD is the *serialization* of the encryption context, and `x-rustfs-encryption-context` stores that exact byte sequence: `encrypt_object` fed one `HashMap` to the AEAD and then moved the same map into the metadata the header is written from, so the stored string is byte-identical to the AAD the object was sealed under. Those objects are therefore recoverable — but only while nothing round-trips the value through a `HashMap` and re-serializes it. Recomputing sorted AAD on decrypt would have turned a readable object into a permanently unreadable one. The previous behavior was worse than the first analysis credited: it did not merely fail intermittently, it made the failure deterministic. `EncryptionMetadata` now carries `context_aad`, the bytes the object was actually sealed with. Encryption records what it fed the AEAD, the header projection stores those bytes verbatim (and preserves a legacy ordering across a re-projection rather than rewriting it into sorted form), and `headers_to_metadata` carries the stored string through untouched. Both decrypt paths, SSE-KMS and SSE-C, prefer it and fall back to canonical serialization only when no stored serialization exists. Canonicalization still applies to everything newly sealed, so the original ordering bug cannot recur. Two tests pin this: a legacy record whose sealed bytes are non-canonical must survive a full header round trip unchanged, and a context header rewritten to an equivalent-but-reordered serialization must fail authentication rather than silently re-deriving a working AAD. Both were mutation-checked against the reinstated bug on each side. Also from review: the lifecycle churn test asserted only that every request was accounted for, which holds whether the state gate exists or not, so both branches are now pinned deterministically after the churn (asserting `refused > 0` on the concurrent phase would only trade the hole for a scheduling flake). And the Local and Vault KV2 envelopes compare `encryption_context` without authenticating it — `DekCrypto` seals only the plaintext — which is now documented at both sites; closing it needs a versioned envelope, since existing ciphertext was sealed without AAD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
492 lines
18 KiB
Rust
492 lines
18 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.
|
|
|
|
//! Shared harness for the `rustfs-kms` black-box behavior suite.
|
|
//!
|
|
//! Everything here drives the crate through the same public entry points the
|
|
//! server uses (`KmsServiceManager` -> `KmsManager` / `ObjectEncryptionService`),
|
|
//! so the suite keeps holding after internal refactors. Two rules keep it
|
|
//! black-box:
|
|
//!
|
|
//! * no `pub(crate)` internals, no on-disk key format parsing;
|
|
//! * assertions target observable contract — error *variants*, returned values,
|
|
//! and state that survives a restart — never implementation details.
|
|
//!
|
|
//! Every harness instance owns its own `KmsServiceManager`; the process-global
|
|
//! singleton is deliberately avoided so tests never cross-talk under nextest.
|
|
|
|
#![allow(dead_code)] // each test binary uses a different slice of the harness
|
|
|
|
use std::collections::HashMap;
|
|
use std::fmt::Debug;
|
|
use std::future::Future;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use base64::Engine as _;
|
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
|
use rustfs_kms::backends::BackendCapabilities;
|
|
use rustfs_kms::{
|
|
CreateKeyRequest, KeyUsage, KmsConfig, KmsError, KmsManager, KmsServiceManager, KmsServiceStatus, ObjectEncryptionService,
|
|
Result,
|
|
};
|
|
use tempfile::TempDir;
|
|
|
|
/// Key id configured for the static backend harness.
|
|
pub const STATIC_KEY_ID: &str = "behavior-static-key";
|
|
|
|
/// Deterministic 32-byte secret for the static backend, base64 encoded.
|
|
///
|
|
/// Fixed rather than random so a failure is reproducible; it is test-only
|
|
/// material and never leaves this crate's test binaries.
|
|
pub fn static_secret_key() -> String {
|
|
BASE64.encode([0x5au8; 32])
|
|
}
|
|
|
|
/// Which backend a harness instance is running.
|
|
///
|
|
/// Local and Static always run. The two Vault backends are **opt-in**: they
|
|
/// need a reachable server, so they join the matrix only when
|
|
/// `RUSTFS_KMS_VAULT_TOKEN` is set (see [`live_vault_backends`]).
|
|
///
|
|
/// This matters for how a green run should be read. `rotate` and `versioning`
|
|
/// are advertised *only* by the Vault backends, so without the Vault lane every
|
|
/// capability-gated branch for them in a `for_each_backend` spec runs the
|
|
/// `UnsupportedCapability` side and never the working side — a rotation that
|
|
/// silently dropped prior key versions would pass. An offline-only run says
|
|
/// nothing about whether the Vault backends work.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum BackendKind {
|
|
Local,
|
|
Static,
|
|
VaultKv2,
|
|
VaultTransit,
|
|
}
|
|
|
|
impl BackendKind {
|
|
pub fn name(self) -> &'static str {
|
|
match self {
|
|
Self::Local => "local",
|
|
Self::Static => "static",
|
|
Self::VaultKv2 => "vault-kv2",
|
|
Self::VaultTransit => "vault-transit",
|
|
}
|
|
}
|
|
|
|
/// Whether this backend keeps its state on an external server that outlives
|
|
/// the harness, so key names must not collide between runs.
|
|
pub fn is_vault(self) -> bool {
|
|
matches!(self, Self::VaultKv2 | Self::VaultTransit)
|
|
}
|
|
}
|
|
|
|
/// Address of the live Vault, defaulting to the usual local dev server.
|
|
pub fn vault_address() -> String {
|
|
std::env::var("RUSTFS_KMS_VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string())
|
|
}
|
|
|
|
/// Token for the live Vault, or `None` when the Vault lane is switched off.
|
|
///
|
|
/// Presence of this variable is the single switch that adds the Vault backends
|
|
/// to every `for_each_backend` spec.
|
|
pub fn vault_token() -> Option<String> {
|
|
std::env::var("RUSTFS_KMS_VAULT_TOKEN").ok().filter(|token| !token.is_empty())
|
|
}
|
|
|
|
/// The Vault backends to include in the matrix for this run.
|
|
pub fn live_vault_backends() -> Vec<BackendKind> {
|
|
match vault_token() {
|
|
Some(_) => vec![BackendKind::VaultKv2, BackendKind::VaultTransit],
|
|
None => Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// A key name that cannot collide with another run against the same Vault.
|
|
///
|
|
/// Vault state is persistent and shared, unlike the per-test temp directory the
|
|
/// local backend gets, so a fixed name would make a rerun collide with its own
|
|
/// leftovers and turn every assertion into a function of run order.
|
|
pub fn unique_key_name(prefix: &str) -> String {
|
|
format!("{prefix}-{}", uuid::Uuid::new_v4().simple())
|
|
}
|
|
|
|
/// A running KMS service, reachable only through the crate's public API.
|
|
pub struct TestKms {
|
|
manager: Arc<KmsServiceManager>,
|
|
kind: BackendKind,
|
|
config: KmsConfig,
|
|
/// Held for the harness lifetime so the local key directory outlives a
|
|
/// simulated process restart.
|
|
_dir: Option<TempDir>,
|
|
}
|
|
|
|
impl TestKms {
|
|
/// Local backend with development defaults and no default key id.
|
|
pub async fn local() -> Self {
|
|
Self::local_with(|_| {}).await
|
|
}
|
|
|
|
/// Local backend with `tweak` applied to the configuration before start.
|
|
pub async fn local_with(tweak: impl FnOnce(&mut KmsConfig)) -> Self {
|
|
let dir = TempDir::new().expect("create temp key dir");
|
|
let mut config = KmsConfig::local(dir.path().to_path_buf()).with_insecure_development_defaults();
|
|
tweak(&mut config);
|
|
let manager = start_manager(&config).await;
|
|
Self {
|
|
manager,
|
|
kind: BackendKind::Local,
|
|
config,
|
|
_dir: Some(dir),
|
|
}
|
|
}
|
|
|
|
/// Vault KV v2 backend against the live server.
|
|
///
|
|
/// Panics when the Vault lane is off — callers gate on
|
|
/// [`live_vault_backends`] rather than calling this blind.
|
|
pub async fn vault_kv2() -> Self {
|
|
let token = vault_token().expect("RUSTFS_KMS_VAULT_TOKEN must be set to run the Vault lane");
|
|
let address = vault_address().parse().expect("RUSTFS_KMS_VAULT_ADDR must be a URL");
|
|
// A local dev Vault speaks plain HTTP, which the config guard refuses
|
|
// unless development mode is declared explicitly.
|
|
let config = KmsConfig::vault(address, token).with_insecure_development_defaults();
|
|
let manager = start_manager(&config).await;
|
|
Self {
|
|
manager,
|
|
kind: BackendKind::VaultKv2,
|
|
config,
|
|
_dir: None,
|
|
}
|
|
}
|
|
|
|
/// Vault Transit backend against the live server.
|
|
pub async fn vault_transit() -> Self {
|
|
let token = vault_token().expect("RUSTFS_KMS_VAULT_TOKEN must be set to run the Vault lane");
|
|
let address = vault_address().parse().expect("RUSTFS_KMS_VAULT_ADDR must be a URL");
|
|
let config = KmsConfig::vault_transit(address, token).with_insecure_development_defaults();
|
|
let manager = start_manager(&config).await;
|
|
Self {
|
|
manager,
|
|
kind: BackendKind::VaultTransit,
|
|
config,
|
|
_dir: None,
|
|
}
|
|
}
|
|
|
|
/// Static single-key backend with a fixed key id and secret.
|
|
pub async fn static_backend() -> Self {
|
|
let config = KmsConfig::static_kms(STATIC_KEY_ID.to_string(), static_secret_key());
|
|
let manager = start_manager(&config).await;
|
|
Self {
|
|
manager,
|
|
kind: BackendKind::Static,
|
|
config,
|
|
_dir: None,
|
|
}
|
|
}
|
|
|
|
/// Simulate a process restart: stop the running service and bring a brand
|
|
/// new manager up over the same configuration and key directory.
|
|
///
|
|
/// A fresh manager (rather than `stop` + `start` on the same one) is what
|
|
/// makes this meaningful — it discards every in-memory cache and version
|
|
/// counter, so anything that still holds afterwards came off disk.
|
|
pub async fn restart(&mut self) {
|
|
self.manager.stop().await.expect("stop should succeed");
|
|
self.manager = start_manager(&self.config).await;
|
|
}
|
|
|
|
pub fn manager(&self) -> &Arc<KmsServiceManager> {
|
|
&self.manager
|
|
}
|
|
|
|
pub fn kind(&self) -> BackendKind {
|
|
self.kind
|
|
}
|
|
|
|
pub fn config(&self) -> &KmsConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Key directory of the local backend, for restart-over-same-state setups.
|
|
pub fn key_dir(&self) -> Option<PathBuf> {
|
|
self.config.local_config().map(|local| local.key_dir.clone())
|
|
}
|
|
|
|
pub async fn kms(&self) -> Arc<KmsManager> {
|
|
self.manager.get_manager().await.expect("KMS manager should be running")
|
|
}
|
|
|
|
pub async fn service(&self) -> Arc<ObjectEncryptionService> {
|
|
self.manager
|
|
.get_encryption_service()
|
|
.await
|
|
.expect("encryption service should be running")
|
|
}
|
|
|
|
pub async fn capabilities(&self) -> BackendCapabilities {
|
|
self.kms().await.backend_capabilities()
|
|
}
|
|
|
|
/// Create a key and return its id, failing loudly on backends that cannot.
|
|
pub async fn create_key(&self, name: &str) -> String {
|
|
let response = self
|
|
.kms()
|
|
.await
|
|
.create_key(CreateKeyRequest {
|
|
key_name: Some(name.to_string()),
|
|
key_usage: KeyUsage::EncryptDecrypt,
|
|
description: Some(format!("black-box behavior key {name}")),
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.unwrap_or_else(|error| panic!("create_key({name}) should succeed on {}: {error:?}", self.kind.name()));
|
|
assert_eq!(response.key_id, name, "created key id must be the requested name");
|
|
response.key_id
|
|
}
|
|
}
|
|
|
|
async fn start_manager(config: &KmsConfig) -> Arc<KmsServiceManager> {
|
|
let manager = Arc::new(KmsServiceManager::new());
|
|
manager.configure(config.clone()).await.expect("configure should succeed");
|
|
manager.start().await.expect("start should succeed");
|
|
assert_eq!(
|
|
manager.get_status().await,
|
|
KmsServiceStatus::Running,
|
|
"manager must report Running right after a successful start"
|
|
);
|
|
manager
|
|
}
|
|
|
|
/// One backend under the shared behavior spec, pre-seeded with a usable key.
|
|
pub struct BackendCase {
|
|
pub kms: TestKms,
|
|
/// A key that exists and is Enabled on this backend.
|
|
pub key_id: String,
|
|
}
|
|
|
|
impl BackendCase {
|
|
async fn new(kind: BackendKind) -> Self {
|
|
match kind {
|
|
BackendKind::Local => {
|
|
let kms = TestKms::local().await;
|
|
let key_id = kms.create_key("behavior-local-key").await;
|
|
Self { kms, key_id }
|
|
}
|
|
BackendKind::Static => {
|
|
let kms = TestKms::static_backend().await;
|
|
Self {
|
|
kms,
|
|
key_id: STATIC_KEY_ID.to_string(),
|
|
}
|
|
}
|
|
BackendKind::VaultKv2 => {
|
|
let kms = TestKms::vault_kv2().await;
|
|
let key_id = kms.create_key(&unique_key_name("behavior-kv2")).await;
|
|
Self { kms, key_id }
|
|
}
|
|
BackendKind::VaultTransit => {
|
|
let kms = TestKms::vault_transit().await;
|
|
let key_id = kms.create_key(&unique_key_name("behavior-transit")).await;
|
|
Self { kms, key_id }
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn kind(&self) -> BackendKind {
|
|
self.kms.kind()
|
|
}
|
|
|
|
pub async fn caps(&self) -> BackendCapabilities {
|
|
self.kms.capabilities().await
|
|
}
|
|
}
|
|
|
|
/// Run one behavior spec against every backend in this run's matrix.
|
|
///
|
|
/// Always Local and Static; plus the Vault backends when the Vault lane is on
|
|
/// (see [`live_vault_backends`]).
|
|
///
|
|
/// The spec is expected to branch on `case.caps()`: a capability a backend
|
|
/// advertises must behave correctly, and one it does not advertise must be
|
|
/// rejected with `UnsupportedCapability` (or the backend's documented
|
|
/// read-only refusal). The contract is deliberately two-directional.
|
|
pub async fn for_each_backend<F, Fut>(spec: F)
|
|
where
|
|
F: Fn(BackendCase) -> Fut,
|
|
Fut: Future<Output = ()>,
|
|
{
|
|
let kinds = [BackendKind::Local, BackendKind::Static]
|
|
.into_iter()
|
|
.chain(live_vault_backends());
|
|
for kind in kinds {
|
|
let case = BackendCase::new(kind).await;
|
|
spec(case).await;
|
|
}
|
|
}
|
|
|
|
/// Drop the service's own startup probe key from a listing.
|
|
///
|
|
/// Starting the service provisions the reserved [`rustfs_kms::probe::PROBE_KEY_ID`]
|
|
/// to verify the backend is actually usable, so it exists on every running
|
|
/// service and is not something a spec created. Exact-set assertions filter it
|
|
/// out: it is startup machinery, not behavior under test, and asserting it in
|
|
/// every expected list would couple those specs to the probe's naming.
|
|
pub fn without_probe_key(ids: impl IntoIterator<Item = String>) -> Vec<String> {
|
|
ids.into_iter().filter(|id| id != rustfs_kms::probe::PROBE_KEY_ID).collect()
|
|
}
|
|
|
|
/// Build an encryption context from literal pairs.
|
|
pub fn ctx(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
|
pairs.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect()
|
|
}
|
|
|
|
/// Deterministic pseudo-random payload of `len` bytes.
|
|
///
|
|
/// Avoids a RNG dependency in the assertions while still producing data that a
|
|
/// broken cipher cannot accidentally round-trip (unlike an all-zero buffer).
|
|
pub fn payload(len: usize) -> Vec<u8> {
|
|
(0..len).map(|i| ((i * 31 + 17) % 251) as u8).collect()
|
|
}
|
|
|
|
/// Drop a successful value so a result whose `Ok` type is not `Debug` (an
|
|
/// `AsyncRead` trait object, for instance) can still go through the error
|
|
/// assertions below.
|
|
pub fn discard<T>(result: Result<T>) -> Result<()> {
|
|
result.map(|_| ())
|
|
}
|
|
|
|
/// Flip one bit in the middle of `bytes`, returning the tampered copy.
|
|
pub fn flip_middle_bit(bytes: &[u8]) -> Vec<u8> {
|
|
assert!(!bytes.is_empty(), "cannot tamper with empty bytes");
|
|
let mut tampered = bytes.to_vec();
|
|
let index = tampered.len() / 2;
|
|
tampered[index] ^= 0b0000_1000;
|
|
tampered
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Error-variant assertions
|
|
//
|
|
// Every failure path is pinned to a KmsError *variant*, never to message text:
|
|
// messages are diagnostics and may be reworded, whereas the variant is what
|
|
// callers (admin handlers, ecfs) actually match on.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[track_caller]
|
|
pub fn assert_key_not_found<T: Debug>(result: Result<T>, expected_key_id: &str) {
|
|
match result {
|
|
Err(KmsError::KeyNotFound { key_id }) => assert!(
|
|
key_id.contains(expected_key_id),
|
|
"KeyNotFound should name {expected_key_id:?}, got {key_id:?}"
|
|
),
|
|
other => panic!("expected KeyNotFound({expected_key_id}), got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_key_already_exists<T: Debug>(result: Result<T>, expected_key_id: &str) {
|
|
match result {
|
|
Err(KmsError::KeyAlreadyExists { key_id }) => {
|
|
assert_eq!(key_id, expected_key_id, "KeyAlreadyExists must name the conflicting key")
|
|
}
|
|
other => panic!("expected KeyAlreadyExists({expected_key_id}), got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_invalid_operation<T: Debug>(result: Result<T>, message_fragment: &str) {
|
|
match result {
|
|
Err(KmsError::InvalidOperation { message }) => assert!(
|
|
message.contains(message_fragment),
|
|
"InvalidOperation should mention {message_fragment:?}, got {message:?}"
|
|
),
|
|
other => panic!("expected InvalidOperation containing {message_fragment:?}, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_unsupported_capability<T: Debug>(result: Result<T>, expected_operation: &str) {
|
|
match result {
|
|
Err(KmsError::UnsupportedCapability { operation, .. }) => {
|
|
assert_eq!(operation, expected_operation, "UnsupportedCapability must name the refused operation")
|
|
}
|
|
other => panic!("expected UnsupportedCapability({expected_operation}), got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_context_mismatch<T: Debug>(result: Result<T>) {
|
|
match result {
|
|
Err(KmsError::ContextMismatch { .. }) => {}
|
|
other => panic!("expected ContextMismatch, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_configuration_error<T: Debug>(result: Result<T>, message_fragment: &str) {
|
|
match result {
|
|
Err(KmsError::ConfigurationError { message }) => assert!(
|
|
message.contains(message_fragment),
|
|
"ConfigurationError should mention {message_fragment:?}, got {message:?}"
|
|
),
|
|
other => panic!("expected ConfigurationError containing {message_fragment:?}, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_validation_error<T: Debug>(result: Result<T>) {
|
|
match result {
|
|
Err(KmsError::ValidationError { .. }) => {}
|
|
other => panic!("expected ValidationError, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_cryptographic_error<T: Debug>(result: Result<T>) {
|
|
match result {
|
|
Err(KmsError::CryptographicError { .. }) => {}
|
|
other => panic!("expected CryptographicError, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_invalid_key_size<T: Debug>(result: Result<T>, expected: usize, actual: usize) {
|
|
match result {
|
|
Err(KmsError::InvalidKeySize {
|
|
expected: got_expected,
|
|
actual: got_actual,
|
|
}) => {
|
|
assert_eq!(got_expected, expected, "InvalidKeySize.expected");
|
|
assert_eq!(got_actual, actual, "InvalidKeySize.actual");
|
|
}
|
|
other => panic!("expected InvalidKeySize({expected}, {actual}), got {other:?}"),
|
|
}
|
|
}
|
|
|
|
/// Assert that a rendered representation carries none of the given secrets.
|
|
///
|
|
/// Used against `Debug` and serde output of configs and responses: the crate's
|
|
/// security rule is that key material never reaches a log or an API payload.
|
|
#[track_caller]
|
|
pub fn assert_no_secret_leak(rendered: &str, secrets: &[&str]) {
|
|
for secret in secrets {
|
|
assert!(
|
|
!rendered.contains(secret),
|
|
"rendered output leaked a secret ({} chars of it): {rendered}",
|
|
secret.len()
|
|
);
|
|
}
|
|
}
|