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
6 changes: 6 additions & 0 deletions .changeset/keyset-backend-full-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@cipherstash/auth': patch
'stash': patch
---

`keysetCreate` and `stash keyset create --backend` now also take the full backend names that their output gives: `aws-kms`, `gcp-kms`, `azure-key-vault` and `vault-transit`. The short names (`aws`, `gcp`, `azure`, `vault`, `static`) still work. Before, the output named a backend `aws-kms`, but the input refused that name. The error for an unknown backend now lists both forms.
6 changes: 5 additions & 1 deletion languages/typescript/packages/auth/native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

/** The options of [`keyset_create`]. */
export interface KeysetCreateOptions {
/** The key backend: `aws`, `gcp`, `azure`, `vault` or `static`. */
/**
* The key backend: `aws`, `gcp`, `azure`, `vault` or `static`, or the
* full name that [`KeysetResult::backend`] gives (`aws-kms`,
* `gcp-kms`, `azure-key-vault`, `vault-transit`).
*/
backend: string
/**
* The vendor key that wraps the keyset's key: an AWS KMS key ARN, a
Expand Down
50 changes: 46 additions & 4 deletions languages/typescript/packages/auth/src/keyset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const STATIC_KEY_LEN: usize = 32;
#[napi(object)]
#[derive(Debug, Clone)]
pub struct KeysetCreateOptions {
/// The key backend: `aws`, `gcp`, `azure`, `vault` or `static`.
/// The key backend: `aws`, `gcp`, `azure`, `vault` or `static`, or the
/// full name that [`KeysetResult::backend`] gives (`aws-kms`,
/// `gcp-kms`, `azure-key-vault`, `vault-transit`).
pub backend: String,
/// The vendor key that wraps the keyset's key: an AWS KMS key ARN, a
/// Cloud KMS crypto key name, an Azure Key Vault key identifier, a Vault
Expand Down Expand Up @@ -230,7 +232,11 @@ fn vendor_config(vendor: KeyVendor, error: impl std::error::Error + 'static) ->
// Parsing the options
// ---------------------------------------------------------------------------

/// The `--backend` values, in the order the help text lists them.
/// The backends `stash keyset` can use, in the order the help text lists
/// them, each with its short name (the `--backend` value). The output
/// names a backend with [`KeyVendor::name`] (`aws-kms`, …), and the input
/// takes that name too, so a value the output gives is a valid input.
/// This table is the only list of the short names.
pub(crate) const BACKENDS: [(&str, KeyVendor); 5] = [
("aws", KeyVendor::Aws),
("gcp", KeyVendor::Gcp),
Expand All @@ -239,18 +245,36 @@ pub(crate) const BACKENDS: [(&str, KeyVendor); 5] = [
("static", KeyVendor::Static),
];

/// The backend that `text` names: a short name of [`BACKENDS`] or the
/// [`KeyVendor::name`] of one of its vendors.
fn parse_backend(text: &str) -> Result<KeyVendor, KeysetError> {
BACKENDS
.iter()
.find(|(name, _)| *name == text)
.find(|(short, vendor)| *short == text || vendor.name() == text)
.map(|(_, vendor)| *vendor)
.ok_or_else(|| {
KeysetError::Options(format!(
"Unknown key backend {text:?}. Use one of: aws, gcp, azure, vault, static."
"Unknown key backend {text:?}. {}",
backend_choices()
))
})
}

/// The accepted backend names, for an error message, from [`BACKENDS`].
fn backend_choices() -> String {
let short: Vec<&str> = BACKENDS.iter().map(|(short, _)| *short).collect();
let full: Vec<&str> = BACKENDS
.iter()
.map(|(_, vendor)| vendor.name())
.filter(|name| !short.contains(name))
.collect();
format!(
"Use one of: {}. The full names {} also work.",
short.join(", "),
full.join(", ")
)
}

fn parse_mode(text: &str) -> Result<KeyMode, KeysetError> {
KeyMode::ALL
.into_iter()
Expand Down Expand Up @@ -871,6 +895,24 @@ mod tests {
}
}

/// The input takes every name the output gives, and the table has every
/// vendor.
#[test]
fn the_backend_input_takes_the_output_names() {
for vendor in KeyVendor::ALL {
assert_eq!(parse_backend(vendor.name()).unwrap(), vendor);
assert!(BACKENDS.iter().any(|(_, v)| *v == vendor), "{vendor}");
}
let Err(KeysetError::Options(message)) = parse_backend("zerokms") else {
panic!("zerokms is not a key vendor backend");
};
assert_eq!(
message,
"Unknown key backend \"zerokms\". Use one of: aws, gcp, azure, vault, static. \
The full names aws-kms, gcp-kms, azure-key-vault, vault-transit also work."
);
}

#[test]
fn a_vendor_config_error_names_the_vendor() {
let envelope = envelope(KeysetError::VendorConfig {
Expand Down
3 changes: 2 additions & 1 deletion languages/typescript/packages/cli/src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,8 @@ export const registry: CommandGroup[] = [
{
name: '--backend',
value: '<aws|gcp|azure|vault|static>',
description: 'The key vendor backend. Required.',
description:
'The key vendor backend. The full names that the output gives (aws-kms, gcp-kms, azure-key-vault, vault-transit) also work. Required.',
},
{
name: '--vendor-key',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,25 @@ describe('stash keyset create', () => {
1,
)
expect(errors()).toContain(messages.keyset.unknownBackend)
expect(errors()).toContain(
'Use one of: aws, gcp, azure, vault, static. The full names aws-kms, gcp-kms, azure-key-vault, vault-transit also work.',
)
const hints = clack.log.info.mock.calls.map((call) => String(call[0]))
expect(hints).toContain(messages.keyset.zeroKmsNote)
})

it('takes the backend name that its own output gives', async () => {
authMock.keysetCreate.mockResolvedValue({ data: result() })
await keysetCreateCommand({
backend: 'aws-kms',
vendorKey: 'arn:aws:kms:us-east-1:111122223333:key/k',
mode: 'wrapped',
})
expect(authMock.keysetCreate).toHaveBeenCalledWith(
expect.objectContaining({ backend: 'aws-kms' }),
)
})

it('refuses wrapped data keys on the static backend', async () => {
await expectExit(
keysetCreateCommand({
Expand Down
43 changes: 33 additions & 10 deletions languages/typescript/packages/cli/src/commands/keyset/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,37 @@ import { emitJsonError, emitJsonEvent } from '../auth/events.js'
*/
const CHROME = { output: process.stderr } as const

/** The `--backend` values. The Rust side holds the same list. */
export const KEYSET_BACKENDS = [
'aws',
'gcp',
'azure',
'vault',
'static',
] as const
/**
* The `--backend` values: each short name, and the full name that the output
* (`# backend: …`, the `backend` of `--json`) and the SDKs use. `--backend`
* takes either, so a value the output gives is a valid input. The Rust side
* (`BACKENDS` in `@cipherstash/auth`) holds the same table.
*/
export const KEYSET_BACKEND_NAMES = {
aws: 'aws-kms',
gcp: 'gcp-kms',
azure: 'azure-key-vault',
vault: 'vault-transit',
static: 'static',
} as const

/** The short `--backend` names, in the order the help text lists them. */
export const KEYSET_BACKENDS = Object.keys(KEYSET_BACKEND_NAMES) as Array<
keyof typeof KEYSET_BACKEND_NAMES
>

/** The full names that are not also a short name. */
const KEYSET_FULL_BACKENDS = Object.values(KEYSET_BACKEND_NAMES).filter(
(name) => !(KEYSET_BACKENDS as readonly string[]).includes(name),
)

/** Whether `--backend` takes `value`. */
export function isKeysetBackend(value: string): boolean {
return (
(KEYSET_BACKENDS as readonly string[]).includes(value) ||
(KEYSET_FULL_BACKENDS as readonly string[]).includes(value)
)
}

/** The `--mode` values. */
export const KEYSET_MODES = ['wrapped', 'derived'] as const
Expand Down Expand Up @@ -296,10 +319,10 @@ function validateCreate(options: KeysetCreateOptions) {
)
}
const backend = options.backend as string
if (!(KEYSET_BACKENDS as readonly string[]).includes(backend)) {
if (!isKeysetBackend(backend)) {
throw new KeysetError(
'invalid_backend',
`${messages.keyset.unknownBackend} '${backend}'. Use one of: ${KEYSET_BACKENDS.join(', ')}.`,
`${messages.keyset.unknownBackend} '${backend}'. Use one of: ${KEYSET_BACKENDS.join(', ')}. The full names ${KEYSET_FULL_BACKENDS.join(', ')} also work.`,
messages.keyset.zeroKmsNote,
)
}
Expand Down
3 changes: 3 additions & 0 deletions skills/stash-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,9 @@ CS_KEYSET_RECORD_USERS=stack-keyset:…
- `--backend <aws|gcp|azure|vault|static>`, `--vendor-key <ref>` and
`--mode <wrapped|derived>` are required. `--name <name>` labels the keyset and
names the variable (`CS_KEYSET_RECORD_<NAME>`, else `CS_KEYSET_RECORD`).
The output names the backend in full (`aws-kms`, `gcp-kms`,
`azure-key-vault`, `vault-transit`, `static`), and `--backend` takes those
full names too.
- **The record is not secret.** Only the vendor key can unwrap its key. Keep it
in config or an env var. **No SDK reads `CS_KEYSET_RECORD*` by itself**: the
app passes the value to `encrypt.WithKeysetRecords` (Go) or
Expand Down
Loading