Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/kms-router-optional-vendor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cipherstash/auth': patch
---

Internal change to the key vendor crates that `keysetCreate` and `keysetRewrap` compile in. The key router can now hold no key vendor backend. There is no change to what `keysetCreate` and `keysetRewrap` do.
39 changes: 30 additions & 9 deletions languages/golang/encrypt/guest/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ use stack_encrypt::ErrorPayload;
/// ZeroKMS is the host-transport client with host-supplied tokens — or, in
/// the `deterministic-kms` test build, the seeded source the record fixture
/// was sealed under. The vendor is [`GuestVendor`], over the step loop; with
/// none configured it holds no keyset and every request goes to ZeroKMS.
/// none configured the router has no vendor and every request goes to
/// ZeroKMS.
#[cfg(not(feature = "deterministic-kms"))]
type GuestZeroKms = crate::zerokms::MaybeZeroKms<
stack_kms::StackKms<crate::host::HostTokenStrategy, crate::host::WasiHostConnection>,
Expand Down Expand Up @@ -235,15 +236,23 @@ fn cipher_init(config: InitConfig) -> Result<Vec<u8>, u32> {
return Err(init_out_of_order());
}

let vendor = build_vendor(config.vendor)?;
let kms = match config.client_key {
let vendor = config.vendor.map(build_vendor).transpose()?;
let kms = match (config.client_key, vendor) {
// A key vendor backend and no ZeroKMS (CIP-5311): no ZeroKMS client
// is built, no token is asked for, and the default keyset is the
// vendor's.
None => {
(None, Some(vendor)) => {
stack_kms_vendor::KeyRouter::new(MaybeZeroKms::Absent, vendor).with_vendor_default()
}
Some(client_key) => {
// `parse_config` asks for the ZeroKMS keys when there is no key
// vendor backend, so this config does not parse. Refuse it here
// too, rather than build a client with no key source.
(None, None) => {
return Err(malformed(
"the config has no ZeroKMS client key and no key vendor backend",
))
}
(Some(client_key), vendor) => {
// One request at a time: a step hands the host one request, and the
// first version of the step loop keeps it that way. Raising it later
// changes no ABI: a step's request list can hold several.
Expand Down Expand Up @@ -271,7 +280,7 @@ fn cipher_init(config: InitConfig) -> Result<Vec<u8>, u32> {
STATUS_KMS_TRANSPORT,
)
})?;
stack_kms_vendor::KeyRouter::new(MaybeZeroKms::Present(zerokms), vendor)
router(MaybeZeroKms::Present(zerokms), vendor)
}
};
let mut builder = StackCipher::builder().kms(kms);
Expand Down Expand Up @@ -311,9 +320,9 @@ fn cipher_init(config: InitConfig) -> Result<Vec<u8>, u32> {
"the deterministic build's default keyset is the seeded source's",
));
}
let kms = stack_kms_vendor::KeyRouter::new(
let kms = router(
crate::zerokms::MaybeZeroKms::Present(config.seed.source()),
build_vendor(config.vendor)?,
config.vendor.map(build_vendor).transpose()?,
);
let builder = StackCipher::builder().kms(kms);
let initialising = Initialising::begin();
Expand Down Expand Up @@ -353,14 +362,26 @@ impl Drop for Initialising {
}
}

/// The key source of a client with ZeroKMS: ZeroKMS and the key vendor
/// backend, or ZeroKMS alone when none is configured.
fn router(
zerokms: GuestZeroKms,
vendor: Option<stack_kms_vendor::VendorKeySource<GuestVendor>>,
) -> GuestKms {
match vendor {
Some(vendor) => stack_kms_vendor::KeyRouter::new(zerokms, vendor),
None => stack_kms_vendor::KeyRouter::zerokms_only(zerokms),
}
}

/// Build the configured key vendor backend over the step loop: its requests,
/// credentials and backoff waits all pause the operation for the host (no
/// vendor call here: records are parsed and checked, keys are unwrapped on
/// first use). A setting the vendor client refuses is
/// `STATUS_ENCODING` naming its config key; a refused keyset record keeps
/// its error for `se_last_error` ([`crate::status::fail_record`]).
fn build_vendor(
config: Option<crate::vendor::VendorConfig>,
config: crate::vendor::VendorConfig,
) -> Result<stack_kms_vendor::VendorKeySource<GuestVendor>, u32> {
use crate::vendor::BuildError;
crate::vendor::build(
Expand Down
76 changes: 25 additions & 51 deletions languages/golang/encrypt/guest/src/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
//!
//! * [`GuestVendor`] is one enum over the four vendor clients and the static
//! key backend, so the cipher has one key source type whatever the host
//! chose: `KeyRouter<ZeroKMS, GuestVendor>`. With no backend configured it
//! is [`GuestVendor::None`] with no keyset records, and the router sends
//! every request to ZeroKMS, as before.
//! chose: `KeyRouter<ZeroKMS, GuestVendor>`. With no backend configured
//! there is no `GuestVendor`: the router is
//! `KeyRouter::zerokms_only`, and sends every request to ZeroKMS, as
//! before.
//! * The vendor clients send through the host's HTTP client
//! (`StepVendorTransport`: each request, and each backoff wait, pauses the
//! operation through the step loop) and sign each request here, in the
Expand Down Expand Up @@ -400,9 +401,6 @@ impl<C: CredentialImport> stack_kms_azure::TokenProvider for HostCredentials<C>
/// One enum, not a type parameter, so that the instance's cipher has one
/// type whatever the host chose at run time.
pub enum GuestVendor<T, C> {
/// No key vendor backend. It holds no keyset records, so the router
/// never calls it, and every method refuses.
None,
Aws(stack_kms_aws::AwsKms<T, HostCredentials<C>>),
Vault(stack_kms_vault::VaultTransit<T, HostCredentials<C>>),
Gcp(stack_kms_gcp::GcpKms<T, HostCredentials<C>>),
Expand All @@ -413,7 +411,6 @@ pub enum GuestVendor<T, C> {
impl<T, C> std::fmt::Debug for GuestVendor<T, C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::None => "none",
Self::Aws(_) => KeyVendor::Aws.name(),
Self::Vault(_) => KeyVendor::Vault.name(),
Self::Gcp(_) => KeyVendor::Gcp.name(),
Expand All @@ -424,18 +421,10 @@ impl<T, C> std::fmt::Debug for GuestVendor<T, C> {
}
}

/// Every method of [`GuestVendor::None`]: it holds no keyset, so a call is
/// a routing bug, refused rather than answered. It is not a backend
/// failure, because there is no backend to name.
fn no_vendor() -> Error {
Error::Unexpected("no key vendor backend is configured".into())
}

/// Dispatch one `KeyVendorBackend` method to the configured client.
macro_rules! dispatch {
($self:expr, $backend:ident => $call:expr, none => $none:expr) => {
($self:expr, $backend:ident => $call:expr) => {
match $self {
GuestVendor::None => $none,
GuestVendor::Aws($backend) => $call,
GuestVendor::Vault($backend) => $call,
GuestVendor::Gcp($backend) => $call,
Expand All @@ -450,55 +439,52 @@ where
T: VendorTransport + Send + Sync,
C: CredentialImport,
{
/// The configured vendor. [`GuestVendor::None`] answers `Static`: it is
/// never asked, because it holds no keyset (the router asks a backend
/// only for a keyset one of its records names).
fn vendor(&self) -> KeyVendor {
dispatch!(self, b => b.vendor(), none => KeyVendor::Static)
dispatch!(self, b => b.vendor())
}

fn supports_mode(&self, mode: KeyMode) -> bool {
dispatch!(self, b => b.supports_mode(mode), none => false)
dispatch!(self, b => b.supports_mode(mode))
}

async fn wrap(&self, key_ref: &str, key: &KeyBytes, aad: &[u8]) -> Result<Vec<u8>, Error> {
dispatch!(self, b => b.wrap(key_ref, key, aad).await, none => Err(no_vendor()))
dispatch!(self, b => b.wrap(key_ref, key, aad).await)
}

async fn unwrap(&self, key_ref: &str, wrapped: &[u8], aad: &[u8]) -> Result<KeyBytes, Error> {
dispatch!(self, b => b.unwrap(key_ref, wrapped, aad).await, none => Err(no_vendor()))
dispatch!(self, b => b.unwrap(key_ref, wrapped, aad).await)
}

async fn generate_wrapped(
&self,
key_ref: &str,
aad: &[u8],
) -> Result<(KeyBytes, Vec<u8>), Error> {
dispatch!(self, b => b.generate_wrapped(key_ref, aad).await, none => Err(no_vendor()))
dispatch!(self, b => b.generate_wrapped(key_ref, aad).await)
}

fn batch_limit(&self) -> Option<NonZeroUsize> {
dispatch!(self, b => b.batch_limit(), none => None)
dispatch!(self, b => b.batch_limit())
}

async fn wrap_batch(
&self,
key_ref: &str,
items: &[WrapItem<'_>],
) -> Result<Vec<Vec<u8>>, Error> {
dispatch!(self, b => b.wrap_batch(key_ref, items).await, none => Err(no_vendor()))
dispatch!(self, b => b.wrap_batch(key_ref, items).await)
}

async fn unwrap_batch(
&self,
key_ref: &str,
items: &[UnwrapItem<'_>],
) -> Result<Vec<KeyBytes>, Error> {
dispatch!(self, b => b.unwrap_batch(key_ref, items).await, none => Err(no_vendor()))
dispatch!(self, b => b.unwrap_batch(key_ref, items).await)
}

async fn sleep(&self, duration: Duration) {
dispatch!(self, b => b.sleep(duration).await, none => ())
dispatch!(self, b => b.sleep(duration).await)
}
}

Expand All @@ -520,26 +506,24 @@ impl From<RecordError> for BuildError {
}

/// Build the key source of the configured backend: the client over
/// `transport` and `import`, and the adapter over the keyset records. With
/// no config, [`GuestVendor::None`] and no records.
/// `transport` and `import`, and the adapter over the keyset records. A
/// config with no backend has no key source to build: the caller routes
/// every request to ZeroKMS.
pub fn build<T, C>(
config: Option<VendorConfig>,
config: VendorConfig,
transport: T,
import: C,
) -> Result<VendorKeySource<GuestVendor<T, C>>, BuildError>
where
T: VendorTransport + Send + Sync,
C: CredentialImport,
{
let Some(VendorConfig {
let VendorConfig {
settings,
records,
default_keyset,
root_key_ttl,
}) = config
else {
return Ok(VendorKeySource::new(GuestVendor::None, [])?);
};
} = config;
let credentials = HostCredentials::new(import);
let backend = match settings {
VendorSettings::Aws { region, endpoint } => {
Expand Down Expand Up @@ -870,7 +854,7 @@ mod tests {
("session_token", string("fixture-session-token")),
]))
});
let source = build(Some(aws_settings()), &transport, import).unwrap();
let source = build(aws_settings(), &transport, import).unwrap();
let key = block(source.backend().unwrap(
"arn:aws:kms:us-east-1:111122223333:key/k",
b"wrapped",
Expand All @@ -894,16 +878,6 @@ mod tests {
);
}

#[test]
fn no_config_builds_no_vendor_and_no_keyset() {
let transport = Recording::default();
let source =
build::<&Recording, FakeImport>(None, &transport, FakeImport::unused()).unwrap();
assert!(matches!(source.backend(), GuestVendor::None));
assert!(block(source.backend().unwrap("k", b"w", b"a")).is_err());
assert!(!source.backend().supports_mode(KeyMode::Derived));
}

#[test]
fn a_bad_setting_names_its_config_key() {
let config = VendorConfig {
Expand All @@ -915,7 +889,7 @@ mod tests {
default_keyset: None,
root_key_ttl: None,
};
let error = build(Some(config), &Recording::default(), FakeImport::unused()).unwrap_err();
let error = build(config, &Recording::default(), FakeImport::unused()).unwrap_err();
assert!(matches!(error, BuildError::Setting("aws_region")));

let config = VendorConfig {
Expand All @@ -928,7 +902,7 @@ mod tests {
default_keyset: None,
root_key_ttl: None,
};
let error = build(Some(config), &Recording::default(), FakeImport::unused()).unwrap_err();
let error = build(config, &Recording::default(), FakeImport::unused()).unwrap_err();
assert!(matches!(error, BuildError::Setting("vault_address")));
}

Expand All @@ -945,7 +919,7 @@ mod tests {
default_keyset: None,
root_key_ttl: None,
};
let error = build(Some(config), &Recording::default(), FakeImport::unused()).unwrap_err();
let error = build(config, &Recording::default(), FakeImport::unused()).unwrap_err();
assert!(matches!(
error,
BuildError::Record(RecordError::ForeignVendor { .. })
Expand All @@ -956,7 +930,7 @@ mod tests {
..aws_settings()
};
assert!(matches!(
build(Some(config), &Recording::default(), FakeImport::unused(),),
build(config, &Recording::default(), FakeImport::unused(),),
Err(BuildError::Record(_))
));
}
Expand Down
Loading
Loading