Skip to content

feat(envelope): AES-256-GCM seam for user-secret values (RIG-3655 T1) - #1111

Merged
trunk-io[bot] merged 1 commit into
compass-server/rig-3655-scope-amendmentfrom
compass-server/rig-3655-t1-envelope
Sep 12, 2026
Merged

trunk-io[bot] merged 1 commit into
compass-server/rig-3655-scope-amendmentfrom
compass-server/rig-3655-t1-envelope

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

This PR is part of a stack containing 7 PRs:

  1. main
  2. docs(server): scope user secrets per tenant, user, and agent (RIG-3655) #1110
  3. "feat(envelope): AES-256-GCM seam for user-secret values (RIG-3655 T1)" (this PR)
  4. feat(store): scope and encrypt user secrets at rest (RIG-3655 T2) #1118
  5. feat(secrets): add the DB-backed StoreResolver (RIG-3655 T3) #1138
  6. docs(server): require admin for tenant-scoped secret writes (RIG-3655) #1141
  7. feat(server): resolve the at-rest master key at boot (RIG-3655 T4) #1146
  8. test(server): prove the armed forge-secret boot path with the real resolver (RIG-3656) #1155

T1 of the user-secret store: the crypto primitive the Postgres-backed store
encrypts values with. Pure package, no Postgres, no callers yet -- T2 adds the
columns that use it.

The API makes the two classic AEAD misuses unrepresentable rather than
documented. Encrypt takes no nonce parameter and draws a fresh 96-bit nonce
from crypto/rand per call, so nonce reuse has no expression. The key bytes live
in an unexported [32]byte, so no reflection-based logger or marshaler can reach
them, and NewKey copies its input so the caller can zero its own slice.

Decrypt collapses every failure -- wrong key, tampered ciphertext, tampered
nonce, wrong AAD, wrong nonce length -- to one opaque ErrDecrypt that wraps no
cipher internals. A caller cannot learn which part it got wrong.

UserSecretAAD binds the full scope tuple from the A9 amendment: version tag,
tenant, scope kind, scope id, name, key version, NUL-separated so the encoding
is injective. Binding the value to its row identity is what makes a row that is
copied to another tenant, renamed, or re-scoped fail to decrypt instead of
silently resolving.

Tests are red-green and each defends a property rather than a line. I
mutation-checked the suite: removing the NUL separators, fixing the nonce, and
dropping the scope field from the AAD each turn it red, so the injectivity,
freshness, and scope-binding assertions can all actually fail.

stdlib crypto only -- no golang.org/x/crypto.

Refs RIG-3655

Co-authored-by: Matt Wilkinson matt@rigel.build

@linear-code

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

RIG-3655

@rigel-mintaka
rigel-mintaka added this pull request to stack #1112 September 11, 2026 20:05
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-3655-t1-e.compass-eng-docs.pages.dev

Deployed from compass-server/rig-3655-t1-envelope at e9e174d.

T1 of the user-secret store: the crypto primitive the Postgres-backed store
encrypts values with. Pure package, no Postgres, no callers yet -- T2 adds the
columns that use it.

The API makes the two classic AEAD misuses unrepresentable rather than
documented. `Encrypt` takes no nonce parameter and draws a fresh 96-bit nonce
from crypto/rand per call, so nonce reuse has no expression. The key bytes live
in an unexported [32]byte, so no reflection-based logger or marshaler can reach
them, and `NewKey` copies its input so the caller can zero its own slice.

`Decrypt` collapses every failure -- wrong key, tampered ciphertext, tampered
nonce, wrong AAD, wrong nonce length -- to one opaque `ErrDecrypt` that wraps no
cipher internals. A caller cannot learn which part it got wrong.

`UserSecretAAD` binds the full scope tuple from the A9 amendment: version tag,
tenant, scope kind, scope id, name, key version, NUL-separated so the encoding
is injective. Binding the value to its row identity is what makes a row that is
copied to another tenant, renamed, or re-scoped fail to decrypt instead of
silently resolving.

Tests are red-green and each defends a property rather than a line. I
mutation-checked the suite: removing the NUL separators, fixing the nonce, and
dropping the scope field from the AAD each turn it red, so the injectivity,
freshness, and scope-binding assertions can all actually fail.

stdlib crypto only -- no golang.org/x/crypto.

Refs RIG-3655

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@trunk-io
trunk-io Bot merged commit d58e256 into main Sep 12, 2026
19 of 29 checks passed
@trunk-io
trunk-io Bot deleted the compass-server/rig-3655-t1-envelope branch September 12, 2026 18:36
@trunk-io

trunk-io Bot commented Sep 12, 2026

Copy link
Copy Markdown

This pull request was merged into main as part of stacked PR 1155.

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.

2 participants