Skip to content

[kms]: Add KeyMaterial to avoid hand-rolled framing - #172

Open
pseudomuto wants to merge 1 commit into
mainfrom
ext_server_material
Open

pseudomuto wants to merge 1 commit into
mainfrom
ext_server_material

Conversation

@pseudomuto

Copy link
Copy Markdown
Collaborator

Decrypt receives only the ciphertext Encrypt returns, so anything an extension server needs to find its wrapping key again has to ride inside it. Every server writing one of these hits that problem and answers it with its own solution. For example, the example used a binary frame, with a version byte, a length-prefixed namespace, and the sealed DEK.

Add api.ext.v1.KeyMaterial as an optional framing to use instead. The wrapped DEK is the only required field; version and namespace cover what a server needs (or may need) to select the key again. The opaque field belongs to the server for custom data and is round-tripped untouched, so anything that needs more than those two fields doesn't necessarily need to roll its own solution. Everything but the DEK is metadata in the clear, as the message says plainly, since this is framing, not encryption.

Switch the KMS example over, which drops the frame offsets, the uint16 namespace length check, and the version byte. The nonce goes in opaque (it's not a secret) simply as a working example of that field. The example derives its key from the version the material carries rather than just validating it. Bumping currentVersion seals new payloads under a new key while everything already sealed keeps opening. This simulates how a provider backed by a real key service (e.g., Vault) would handle key versions.

Decrypt receives nothing but the ciphertext Encrypt returned, so
anything an extension server needs to find its wrapping key again has to
ride inside it. Every server writing one of these hits that problem and
answers it with its own solution. For example, the example used a binary
frame, with a version byte, a length-prefixed namespace, and the sealed
DEK.

Add api.ext.v1.KeyMaterial as an optional framing to use instead. The
wrapped DEK is the only required field; version and namespace cover what
a server needs (or may need) to select the key again. The `opaque` field
belongs to the server itself for custom data and is round-tripped
untouched, so one that needs more than those two fields doesn't need to
roll their own solution necessarily. Everything but the DEK is metadata
in the clear, which the message says plainly, since this is framing and
not encryption.

Switch the kms example over, which drops the frame offsets, the uint16
namespace length check, and the version byte. The nonce goes in opaque,
where it doubles as a worked example of that field. The example derives
its key from the version the material carries rather than checking it,
so bumping currentVersion seals new payloads under a new key while
everything already sealed keeps opening. This simulates the way a
provider backed by a real key service (e.g. Vault) would address a key
version.
@pseudomuto
pseudomuto requested a review from a team as a code owner September 16, 2026 15:32
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

if len(ciphertext) < headerSize {
return nil, errors.New("server: ciphertext is too short to hold a header")
material := &extv1.KeyMaterial{
EncryptedDek: gcm.Seal(nil, nonce, dek, []byte(namespace)),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a demo, but IRL, passing nonce here instead of nil would include it in the ciphertext and make it available during decryption. I omitted it to show how opaque works.

@pseudomuto
pseudomuto added this pull request to stack #174 September 16, 2026 18:37
// it. Rather than hand-roll a binary frame, a server can marshal this message as
// its ciphertext and unmarshal it on the way back.
//
// The fields other than encrypted_dek are metadata in the clear: this is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we clarify here that if cleartext metadata is used for key selection or decrypt, changing it must cause decrypt to fail? KeyMaterial only defines how these fields are stored; it does not protect them on its own. NewKeyWrapper handles this by including the fields in AEAD additional data. An extension that uses KeyMaterial directly would need to get the same protection from its own wrapping operation or key-management backend.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other fields (version, namespace, opaque) aren't used for decryption (directly) and are not sensitive or secret values. Instead, they identify which key produced the encrypted dek. Changing any of them would cause decryption to fail because it couldn't find the key.

Adding additional data (AD) ensures that if the values were tampered with, decryption would fail even if the correct key was somehow found (e.g., the implementation provides a fallback key). This is because we use the values from the proto to construct the AD, which, if different, would preemptively fail to decrypt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants