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-vendor-name-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cipherstash/auth': patch
---

Internal change to the key vendor clients that `keysetCreate` and `keysetRewrap` use. Each client now names its vendor with a typed value, not a string. The `vendor` field of a `VENDOR_ERROR` failure keeps the same values (`aws-kms`, `gcp-kms`, `azure-key-vault`, `vault-transit`, `static`).
13 changes: 13 additions & 0 deletions languages/golang/encrypt/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ func deterministicClient(ctx context.Context, wasm []byte, seed [32]byte) (*Clie
return c, nil
}

// KeyVendorWire is the KeyVendor byte and the key_vendor name the client
// sends to the guest for v.
func KeyVendorWire(v KeyVendor) (byte, string, error) {
spec, err := v.keyVendor()
if err != nil {
return 0, "", err
}
return byte(spec.vendor), spec.vendor.String(), nil
}

// KeyVendorNameCount is the number of vendors in the Go table of names.
func KeyVendorNameCount() int { return len(keyVendorNames) }

// NewDeterministicVendorClient is NewDeterministicClient with a key vendor
// backend beside the seeded ZeroKMS stand-in: the deterministic build's
// object config, {"seed", key_vendor, keyset_records, ...}. Vendor requests
Expand Down
5 changes: 1 addition & 4 deletions languages/golang/encrypt/guest/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,7 @@ impl VendorWire<'_> {
let missing = |key: &str| format!("missing field `{key}`");
let invalid = |key: &str| format!("{key}: a value its type does not accept");
let name = self.key_vendor.ok_or_else(|| missing("key_vendor"))?;
let vendor = KeyVendor::ALL
.into_iter()
.find(|v| v.name() == name)
.ok_or_else(|| invalid("key_vendor"))?;
let vendor = KeyVendor::from_name(name).ok_or_else(|| invalid("key_vendor"))?;
let records = self
.keyset_records
.ok_or_else(|| missing("keyset_records"))?;
Expand Down
4 changes: 2 additions & 2 deletions languages/golang/encrypt/guest/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ mod tests {
use stack_kms::BackendErrorKind as Kind;
let status = |kind| {
status_for_error(&stack_encrypt::Error::Kms(stack_kms::Error::Backend {
vendor: "test-vendor",
backend: stack_kms::KeyVendor::Aws.into(),
kind,
source: Box::new(std::io::Error::other("detail")),
}))
Expand All @@ -395,7 +395,7 @@ mod tests {
assert_eq!(status(Kind::InvalidCiphertext), STATUS_AUTH);
assert_eq!(
status_for_error(&stack_encrypt::Error::Kms(stack_kms::Error::ZeroKmsOnly {
vendor: "test-vendor",
vendor: stack_kms::KeyVendor::Aws,
feature: stack_kms::ZeroKmsFeature::DecryptionPolicy,
})),
STATUS_ENCODING
Expand Down
9 changes: 3 additions & 6 deletions languages/golang/encrypt/guest/src/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,13 +425,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.
/// 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::Backend {
vendor: "none",
kind: BackendErrorKind::NotFound,
source: "no key vendor backend is configured".into(),
}
Error::Unexpected("no key vendor backend is configured".into())
}

/// Dispatch one `KeyVendorBackend` method to the configured client.
Expand Down
4 changes: 2 additions & 2 deletions languages/golang/encrypt/guest/src/zerokms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl std::error::Error for NoZeroKms {}
/// The refusal of every request that reaches [`MaybeZeroKms::Absent`].
pub fn no_zerokms() -> Error {
Error::Backend {
vendor: KeyBackend::ZeroKms.name(),
backend: KeyBackend::ZeroKms,
kind: BackendErrorKind::NotFound,
source: Box::new(NoZeroKms),
}
Expand Down Expand Up @@ -175,7 +175,7 @@ mod tests {
assert_eq!(absent.key_mode(id), None);
// Another backend error is not this one.
assert!(!is_no_zerokms(&Error::Backend {
vendor: KeyBackend::ZeroKms.name(),
backend: KeyBackend::ZeroKms,
kind: BackendErrorKind::NotFound,
source: "other".into(),
}));
Expand Down
67 changes: 45 additions & 22 deletions languages/golang/encrypt/keyvendor.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ type KeyVendor interface {
// vendorSpec is a KeyVendor as the client uses it: the config keys the
// guest reads, and where the per-request credential comes from.
type vendorSpec struct {
// name is the guest's key_vendor value.
name string
// id is the KeyVendor byte the guest names in a credential request.
id uint32
// vendor is the backend. Its byte is what the guest names in a
// credential request; its name is the guest's key_vendor value.
vendor keyVendorID
// fields are the backend's settings in the guest config.
fields vcvalue.Object
// staticKey is StaticKey's key, consumed by NewClient.
Expand All @@ -50,16 +49,41 @@ type vendorSpec struct {
token AccessTokenSource
}

// The KeyVendor bytes of the v2 leaf header and the keyset record. They
// cross in the guest's credential requests (step.go).
// keyVendorID is a KeyVendor byte of the v2 leaf header and the keyset
// record. It crosses in the guest's credential requests (step.go).
type keyVendorID uint32

// The KeyVendor bytes. They are a storage commitment, as in the Rust crate
// stack-kms (KeyVendor::byte).
const (
vendorAWS = 1
vendorGCP = 2
vendorAzure = 3
vendorVault = 4
vendorStatic = 5
vendorAWS keyVendorID = 1
vendorGCP keyVendorID = 2
vendorAzure keyVendorID = 3
vendorVault keyVendorID = 4
vendorStatic keyVendorID = 5
)

// keyVendorNames is the one table of the vendor names on the Go side: the
// guest's key_vendor value of each KeyVendor byte. The guest reads the name
// with KeyVendor::from_name of the Rust crate stack-kms, so these strings
// are a wire format, not a choice. TestKeyVendorNamesMatchTheGuest pins
// each one to the guest's name for the same byte.
var keyVendorNames = map[keyVendorID]string{
vendorAWS: "aws-kms",
vendorGCP: "gcp-kms",
vendorAzure: "azure-key-vault",
vendorVault: "vault-transit",
vendorStatic: "static",
}

// String is the vendor's name, as the guest spells it.
func (id keyVendorID) String() string {
if name, ok := keyVendorNames[id]; ok {
return name
}
return fmt.Sprintf("key vendor %d", uint32(id))
}

// AWSKMS is AWS KMS as the key vendor backend. The key in each keyset
// record is a KMS key ARN. Both key modes.
type AWSKMS struct {
Expand All @@ -85,7 +109,7 @@ func (v AWSKMS) keyVendor() (*vendorSpec, error) {
if v.Endpoint != "" {
fields = append(fields, vcvalue.Field{Key: "aws_endpoint", Value: v.Endpoint})
}
return &vendorSpec{name: "aws-kms", id: vendorAWS, fields: fields, aws: v.Credentials}, nil
return &vendorSpec{vendor: vendorAWS, fields: fields, aws: v.Credentials}, nil
}

// GoogleCloudKMS is Google Cloud KMS as the key vendor backend. The key in
Expand All @@ -109,7 +133,7 @@ func (v GoogleCloudKMS) keyVendor() (*vendorSpec, error) {
if v.Endpoint != "" {
fields = append(fields, vcvalue.Field{Key: "gcp_endpoint", Value: v.Endpoint})
}
return &vendorSpec{name: "gcp-kms", id: vendorGCP, fields: fields, token: v.Token}, nil
return &vendorSpec{vendor: vendorGCP, fields: fields, token: v.Token}, nil
}

// AzureKeyVault is Azure Key Vault or Managed HSM as the key vendor
Expand All @@ -128,7 +152,7 @@ func (v AzureKeyVault) keyVendor() (*vendorSpec, error) {
if v.Token == nil {
return nil, fmt.Errorf("%w: AzureKeyVault needs a Token source", ErrEncoding)
}
return &vendorSpec{name: "azure-key-vault", id: vendorAzure, token: v.Token}, nil
return &vendorSpec{vendor: vendorAzure, token: v.Token}, nil
}

// VaultTransit is HashiCorp Vault's Transit secrets engine as the key
Expand Down Expand Up @@ -162,7 +186,7 @@ func (v VaultTransit) keyVendor() (*vendorSpec, error) {
if v.Mount != "" {
fields = append(fields, vcvalue.Field{Key: "vault_mount", Value: v.Mount})
}
return &vendorSpec{name: "vault-transit", id: vendorVault, fields: fields, token: v.Token}, nil
return &vendorSpec{vendor: vendorVault, fields: fields, token: v.Token}, nil
}

// StaticKey is a 32-byte key the application holds, as the key vendor
Expand Down Expand Up @@ -200,8 +224,7 @@ func (v StaticKey) keyVendor() (*vendorSpec, error) {
return nil, fmt.Errorf("%w: StaticKey's Key is %d bytes, not 32", ErrEncoding, len(v.Key))
}
return &vendorSpec{
name: "static",
id: vendorStatic,
vendor: vendorStatic,
fields: vcvalue.Object{{Key: "static_key_ref", Value: v.KeyRef}},
staticKey: v.Key,
}, nil
Expand Down Expand Up @@ -330,7 +353,7 @@ func keyVendorConfig(v KeyVendor, records []string) (*vendorSpec, error) {
// The result holds a StaticKey's key: the caller wipes what it encodes.
func (s *vendorSpec) configFields(records []string) vcvalue.Object {
fields := vcvalue.Object{
{Key: "key_vendor", Value: s.name},
{Key: "key_vendor", Value: s.vendor.String()},
{Key: "keyset_records", Value: append([]string(nil), records...)},
}
fields = append(fields, s.fields...)
Expand Down Expand Up @@ -510,8 +533,8 @@ func StaticAccessToken(token string) AccessTokenSource {
// asked for, and renders it as the guest reads it. The result holds a
// secret; the caller wipes it.
func (s *vendorSpec) vendorCredential(ctx context.Context, vendor uint32, scope string) ([]byte, error) {
if vendor != s.id {
return nil, fmt.Errorf("encrypt: the guest asked for a credential of key vendor %d, but the client's is %s", vendor, s.name)
if keyVendorID(vendor) != s.vendor {
return nil, fmt.Errorf("encrypt: the guest asked for a credential of key vendor %d, but the client's is %s", vendor, s.vendor)
}
var fields vcvalue.Object
var expires time.Time
Expand All @@ -538,12 +561,12 @@ func (s *vendorSpec) vendorCredential(ctx context.Context, vendor uint32, scope
return nil, err
}
if t.Token == "" {
return nil, fmt.Errorf("%w: the %s token source returned an empty token", ErrNoKeyVendorCredentials, s.name)
return nil, fmt.Errorf("%w: the %s token source returned an empty token", ErrNoKeyVendorCredentials, s.vendor)
}
fields = vcvalue.Object{{Key: "token", Value: t.Token}}
expires = t.Expires
default:
return nil, fmt.Errorf("encrypt: the %s key backend takes no credentials", s.name)
return nil, fmt.Errorf("encrypt: the %s key backend takes no credentials", s.vendor)
}
if !expires.IsZero() {
fields = append(fields, vcvalue.Field{Key: "expires_at", Value: expires.Unix()})
Expand Down
34 changes: 34 additions & 0 deletions languages/golang/encrypt/keyvendor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,40 @@ func (refusing) RoundTrip(*http.Request) (*http.Response, error) {
return nil, errors.New("this test makes no HTTP request")
}

// TestKeyVendorNamesMatchTheGuest pins the Go table of vendor names to the
// Rust names (KeyVendor::name in stack-kms). For each backend, the guest
// must accept the name the client sends as key_vendor, and a keyset record
// with the byte the client uses. The guest refuses a name it does not know,
// and a record of another vendor, so a drift on either side fails here.
func TestKeyVendorNamesMatchTheGuest(t *testing.T) {
token := encrypt.StaticAccessToken("t")
vendors := []encrypt.KeyVendor{
encrypt.AWSKMS{Region: "us-east-1", Credentials: &countingCredentials{}},
encrypt.GoogleCloudKMS{Token: token},
encrypt.AzureKeyVault{Token: token},
encrypt.VaultTransit{Address: "https://vault.example.com", Token: token},
encrypt.StaticKey{KeyRef: "k", Key: bytes.Repeat([]byte{1}, 32)},
}
seen := map[byte]string{}
for _, v := range vendors {
id, name, err := encrypt.KeyVendorWire(v)
if err != nil {
t.Fatal(err)
}
if other, ok := seen[id]; ok {
t.Fatalf("%s and %s have the same byte %d", other, name, id)
}
seen[id] = name
t.Run(name, func(t *testing.T) {
record := keysetRecord(randomID(t), id, modeDerived, "pin", "k", []byte{1})
_ = vendorClient(t, refusing{}, v, record)
})
}
if len(seen) != encrypt.KeyVendorNameCount() {
t.Errorf("the test covers %d vendors, the table has %d", len(seen), encrypt.KeyVendorNameCount())
}
}

// fakeKMS answers the AWS KMS JSON API for the three operations the client
// uses. A "ciphertext blob" is the key and the SHA-256 of the encryption
// context's cipherstash:aad, so a decrypt under another context fails as
Expand Down
6 changes: 3 additions & 3 deletions languages/typescript/packages/auth/src/keyset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ impl KeysetError {
if let Some(help) = error.help() {
let _ = fields.insert("help".into(), help.to_string().into());
}
if let RecordError::Kms(stack_kms::Error::Backend { vendor, kind, .. }) = error {
let _ = fields.insert("vendor".into(), (*vendor).into());
if let RecordError::Kms(stack_kms::Error::Backend { backend, kind, .. }) = error {
let _ = fields.insert("vendor".into(), backend.name().into());
let _ = fields.insert("kind".into(), kind.as_str().into());
}
}
Expand Down Expand Up @@ -832,7 +832,7 @@ mod tests {
to: "b".into(),
}),
KeysetError::Record(RecordError::Kms(stack_kms::Error::Backend {
vendor: "static",
backend: KeyVendor::Static.into(),
kind: stack_kms::BackendErrorKind::NotFound,
source: "none".into(),
})),
Expand Down
2 changes: 1 addition & 1 deletion packages/stack-encrypt/src/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,7 @@ mod tests {
let vitur = |kind| ViturRequestError::new(kind, "stubbed", std::io::Error::other("detail"));
let backend = |kind| {
Error::Kms(stack_kms::Error::Backend {
vendor: "test-vendor",
backend: stack_kms::KeyVendor::Static.into(),
kind,
source: Box::new(std::io::Error::other("detail")),
})
Expand Down
8 changes: 4 additions & 4 deletions packages/stack-encrypt/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ mod tests {
use crate::ErrorPayload;
use serde_json::json;
let failure = Error::Kms(crate::kms::Error::Backend {
vendor: "test-vendor",
backend: crate::kms::KeyVendor::Aws.into(),
kind: crate::kms::BackendErrorKind::AccessDenied,
source: Box::new(std::io::Error::other("vendor detail")),
});
Expand All @@ -397,10 +397,10 @@ mod tests {
);
assert_eq!(
serde_json::Value::Object(failure.payload()),
json!({ "vendor": "test-vendor", "kind": "AccessDenied" })
json!({ "vendor": "aws-kms", "kind": "AccessDenied" })
);
let refusal = Error::Kms(crate::kms::Error::ZeroKmsOnly {
vendor: "test-vendor",
vendor: crate::kms::KeyVendor::Vault,
feature: crate::kms::ZeroKmsFeature::UnverifiedContext,
});
assert_eq!(
Expand All @@ -409,7 +409,7 @@ mod tests {
);
assert_eq!(
serde_json::Value::Object(refusal.payload()),
json!({ "vendor": "test-vendor", "feature": "unverified_context" })
json!({ "vendor": "vault-transit", "feature": "unverified_context" })
);
}

Expand Down
8 changes: 4 additions & 4 deletions packages/stack-kms-aws/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use crate::credentials::{CredentialsError, CredentialsProvider};
use crate::sigv4::{sign, SigningParams};
use crate::time::{system_clock, Clock};

/// The vendor name in errors.
const VENDOR: &str = "aws-kms";
/// This client's vendor: what [`KeyVendorBackend::vendor`] answers and what its errors name.
const VENDOR: KeyVendor = KeyVendor::Aws;
/// The AWS KMS JSON protocol content type.
const CONTENT_TYPE: &str = "application/x-amz-json-1.1";
/// The SigV4 signing name of AWS KMS.
Expand Down Expand Up @@ -80,7 +80,7 @@ pub enum AwsKmsError {

fn backend(kind: BackendErrorKind, error: AwsKmsError) -> Error {
Error::Backend {
vendor: VENDOR,
backend: VENDOR.into(),
kind,
source: Box::new(error),
}
Expand Down Expand Up @@ -372,7 +372,7 @@ fn plaintext_key(operation: &'static str, response: KeyResponse) -> Result<KeyBy

impl<T: VendorTransport + Send + Sync, C: CredentialsProvider> KeyVendorBackend for AwsKms<T, C> {
fn vendor(&self) -> KeyVendor {
KeyVendor::Aws
VENDOR
}

async fn wrap(&self, key_ref: &str, key: &KeyBytes, aad: &[u8]) -> Result<Vec<u8>, Error> {
Expand Down
4 changes: 2 additions & 2 deletions packages/stack-kms-aws/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ fn record_aad() -> Vec<u8> {

fn kind_of(error: Error) -> BackendErrorKind {
match error {
Error::Backend { vendor, kind, .. } => {
assert_eq!(vendor, "aws-kms");
Error::Backend { backend, kind, .. } => {
assert_eq!(backend.name(), "aws-kms");
kind
}
other => panic!("not a backend error: {other:?}"),
Expand Down
Loading
Loading