diff --git a/.changeset/kms-router-one-vendor-type.md b/.changeset/kms-router-one-vendor-type.md new file mode 100644 index 000000000..a6511e9a0 --- /dev/null +++ b/.changeset/kms-router-one-vendor-type.md @@ -0,0 +1,5 @@ +--- +'@cipherstash/auth': patch +--- + +A key router now holds ZeroKMS and at most one key vendor backend by its type. The ZeroKMS side of a router must be a ZeroKMS key source, and a router is not one, so a router that holds a second router does not compile. No runtime behaviour changes. diff --git a/docs/plans/2026-11-05-key-vendor-backends-decisions.md b/docs/plans/2026-11-05-key-vendor-backends-decisions.md index 8a4f6a49e..6796faaa1 100644 --- a/docs/plans/2026-11-05-key-vendor-backends-decisions.md +++ b/docs/plans/2026-11-05-key-vendor-backends-decisions.md @@ -365,9 +365,15 @@ the alternatives and the reason. in `stack-kms-vendor`. A keyset id or name that a vendor record has goes to the vendor (a local record name wins over a ZeroKMS keyset of the same name); everything else, and the default keyset, goes to ZeroKMS. The type - holds exactly one vendor; `VendorKeySource::new` refuses records of two - vendors with `RecordError::SecondVendor`, and a record of another vendor - than the backend with `RecordError::ForeignVendor`. + holds at most one vendor: its ZeroKMS side must implement the marker + `stack_kms::ZeroKmsKeySource`, which no router implements, so + `KeyRouter, Gcp>` does not compile (a `compile_fail` + doc test shows it). This was not true when first written: the ZeroKMS + side took any `DataKeySource`, so a router could hold a second router; the + review after CIP-5313 found it and the marker closes it. + `VendorKeySource::new` refuses records of two vendors with + `RecordError::SecondVendor`, and a record of another vendor than the + backend with `RecordError::ForeignVendor`. - **Decision:** the static key backend (`StaticKeyVendor`) wraps with AES-256-GCM under a random 96-bit nonce: `version(1)=1 ‖ nonce(12) ‖ ciphertext ‖ tag`. It holds several keys by reference, for rotation. @@ -1810,6 +1816,10 @@ ticket that recorded it. - CIP-5311 → CIP-5313: the capability matrix on the Rust side. - CIP-5312 → CIP-5313: the CLI README section for `stash keyset` (the docs site is still open, above). +- **Review after CIP-5313 (FIXED):** "at most one vendor" was not held by + the type: `KeyRouter`'s ZeroKMS side took any `DataKeySource`, so a router + could nest a second router and vendor. The ZeroKMS side must now implement + `stack_kms::ZeroKmsKeySource`, which no router implements. - **CIP-5313 (FIXED, defect):** a rewrap of a wrapped-data-key record to another vendor key made its stored values unreadable. `KeysetRecord::rewrap` now refuses it (`RecordError::RewrapToAnotherKey`; `REWRAP_REFUSED` in diff --git a/languages/golang/encrypt/guest/src/zerokms.rs b/languages/golang/encrypt/guest/src/zerokms.rs index 29dc187fd..2b011efb8 100644 --- a/languages/golang/encrypt/guest/src/zerokms.rs +++ b/languages/golang/encrypt/guest/src/zerokms.rs @@ -27,6 +27,9 @@ pub enum MaybeZeroKms { Absent, } +// MaybeZeroKms stands for ZeroKMS (present or not), never for a router. +impl stack_kms::ZeroKmsKeySource for MaybeZeroKms {} + /// The cause of [`no_zerokms`]: its type is what the status layer finds to /// report `STATUS_NO_ZEROKMS`. #[derive(Debug, Clone, Copy)] diff --git a/packages/stack-kms-vendor/src/router.rs b/packages/stack-kms-vendor/src/router.rs index 59507722e..de083c5c8 100644 --- a/packages/stack-kms-vendor/src/router.rs +++ b/packages/stack-kms-vendor/src/router.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use stack_kms::{ DataKey, DataKeySource, DataKeyWithTag, Error, GenerateKeyPayload, IdentifiedBy, IndexKey, IndexKeySource, KeyBackend, KeyMode, LoadedIndexKey, RetrieveKeyPayload, SelectedKeyset, - UnverifiedContext, + UnverifiedContext, ZeroKmsKeySource, }; use uuid::Uuid; @@ -17,8 +17,24 @@ use crate::vendor::KeyVendorBackend; /// /// A cipher holds ZeroKMS and **at most one** key vendor backend. The type /// has room for one or none: [`new`](Self::new) gives it one, and -/// [`zerokms_only`](Self::zerokms_only) none. [`VendorKeySource::new`] -/// refuses records that name a second vendor +/// [`zerokms_only`](Self::zerokms_only) none. The ZeroKMS side must be a +/// [`ZeroKmsKeySource`], which a router is not, so a router cannot hold a +/// second router and its vendor: +/// +/// ```compile_fail +/// use stack_kms::FakeDataKeySource; +/// use stack_kms_vendor::{FakeVendor, KeyRouter, VendorKeySource}; +/// +/// fn nested( +/// inner: KeyRouter, +/// vendor: VendorKeySource, +/// ) { +/// // A router is not a ZeroKmsKeySource. +/// let _ = KeyRouter::new(inner, vendor); +/// } +/// ``` +/// +/// [`VendorKeySource::new`] refuses records that name a second vendor /// ([`RecordError::SecondVendor`](crate::RecordError::SecondVendor)). /// /// Routing: @@ -52,7 +68,7 @@ impl std::fmt::Debug for KeyRouter { } } -impl KeyRouter { +impl KeyRouter { /// Route between `zerokms` and `vendor`. pub fn new(zerokms: Z, vendor: VendorKeySource) -> Self { Self { @@ -71,7 +87,9 @@ impl KeyRouter { vendor_default: false, } } +} +impl KeyRouter { /// Make naming no keyset select the vendor's default keyset (its only /// record, or the one set with /// [`VendorKeySource::with_default_keyset`]), not ZeroKMS's. For a diff --git a/packages/stack-kms/src/key_source.rs b/packages/stack-kms/src/key_source.rs index 1e007f758..3b00975e7 100644 --- a/packages/stack-kms/src/key_source.rs +++ b/packages/stack-kms/src/key_source.rs @@ -255,6 +255,18 @@ impl LoadedIndexKey { } } +/// A key source that stands for ZeroKMS: the ZeroKMS side of a router that +/// adds a key vendor backend (`stack_kms_vendor::KeyRouter`). +/// +/// It is a marker. The router asks for it so that its ZeroKMS side cannot be +/// another router, which would put two key vendor backends in one cipher +/// (ADR-0009: ZeroKMS and at most one vendor). Implement it only for a +/// source that talks to ZeroKMS, or that takes its place in a test. Never +/// implement it for a source that routes to a key vendor backend. +pub trait ZeroKmsKeySource {} + +impl ZeroKmsKeySource for crate::StackKms {} + impl DataKeySource for crate::StackKms where C: stack_auth::AuthStrategyBounds, @@ -403,6 +415,8 @@ mod fake { } } + impl super::ZeroKmsKeySource for FakeDataKeySource {} + impl DataKeySource for FakeDataKeySource { async fn generate_keys( &self, @@ -529,6 +543,8 @@ mod deterministic { } } + impl super::ZeroKmsKeySource for DeterministicSource {} + impl DataKeySource for DeterministicSource { async fn generate_keys( &self, diff --git a/packages/stack-kms/src/lib.rs b/packages/stack-kms/src/lib.rs index 375a863e1..1de089a2f 100644 --- a/packages/stack-kms/src/lib.rs +++ b/packages/stack-kms/src/lib.rs @@ -149,7 +149,9 @@ pub use backend::{KeyBackend, KeyMode, KeyVendor}; pub use key_source::DeterministicSource; #[cfg(feature = "test-support")] pub use key_source::FakeDataKeySource; -pub use key_source::{DataKeySource, IndexKeySource, KeyLifetime, LoadedIndexKey, SelectedKeyset}; +pub use key_source::{ + DataKeySource, IndexKeySource, KeyLifetime, LoadedIndexKey, SelectedKeyset, ZeroKmsKeySource, +}; // Key providers pub use key_provider::{