Skip to content

refactor: deepen keyring seam, transit domain, and audit signing#127

Merged
allisson merged 5 commits into
mainfrom
improve-codebase
May 23, 2026
Merged

refactor: deepen keyring seam, transit domain, and audit signing#127
allisson merged 5 commits into
mainfrom
improve-codebase

Conversation

@allisson
Copy link
Copy Markdown
Owner

Summary

Three architectural deepening refactors targeting modules identified as shallow in an internal review. Each commit is self-contained and passes the full test suite.

Commit 1 — Complete the Keyring seam (40098d8)

internal/keyring was introduced in #124 as a single module for envelope encryption, but callers were still importing internal/crypto/domain directly to access ErrDecryptionFailed, Zero, and Algorithm. This commit closes that gap:

  • Re-exports ErrDecryptionFailed, Zero, ErrKekNotFound from keyring so callers need no crypto/domain import
  • Maps ErrDekNotFound / ErrKekNotFound to ErrDecryptionFailed internally in Decrypt and openCipher — errors no longer leak across the seam
  • Migrates all feature callers (secrets, transit, tokenization HTTP/usecase) off crypto/domain

Commit 2 — Move transit nonce framing into transit/domain (9ca5342)

The usecase held a hardcoded const nonceSize = 12 and manually sliced nonce ‖ ciphertext. This is domain knowledge (the AEAD wire format) living in the wrong layer:

  • Adds AEADNonceSize = 12 to transit/domain constants
  • Adds NewFramedBlob(version, nonce, ciphertext) and SplitNonce() to EncryptedBlob
  • Removes the manual framing from the usecase; Encrypt and Decrypt become single-line domain calls
  • Full test coverage for NewFramedBlob and SplitNonce

Commit 3 — Move audit signing behind a KeySigner seam (9b3b117)

auditLogUseCase held a *cryptoDomain.KekChain — raw key material passing through the usecase layer. The AuditSigner service also took kekKey []byte directly. This commit keeps key bytes inside keyring:

  • Adds KeySigner interface to keyring: SignWithKey(data []byte) (sig, kekID) and VerifyWithKey(kekID, data, sig)
  • Production keyring implements KeySigner via HKDF-SHA256 + HMAC-SHA256 (same algorithm, now encapsulated)
  • Canonicalization logic moves from auth/service.auditSigner to auth/domain.AuditLog.Canonical() — the domain type owns its wire format
  • auditLogUseCase drops kekChain and auditSigner; constructor is now NewAuditLogUseCase(repo, keySigner)
  • keyring.Fake implements KeySigner with a deterministic zero-key HMAC, making it a real second adapter

Test plan

  • go test ./... — 1541 tests pass, no failures
  • Each commit builds and tests independently
  • keyring.Fake satisfies the new KeySigner interface, keeping feature tests decoupled from the database

🤖 Generated with Claude Code

allisson and others added 5 commits May 23, 2026 16:31
… DEK/KEK errors internally

ADR-0013 promised that feature modules would not need to import
crypto/domain. That was partially true: Algorithm was re-exported, but
ErrDecryptionFailed, ErrDekNotFound, ErrKekNotFound, and Zero were not.

- keyring.go: add ErrDecryptionFailed and Zero re-exports
- impl.go: map ErrDekNotFound/ErrKekNotFound → ErrDecryptionFailed at
  the Decrypt and openCipher boundaries; KEK/DEK vocabulary stays inside
  the module
- secrets, transit, tokenization usecases and HTTP handlers: replace all
  cryptoDomain.{Algorithm,Zero,ErrDecryptionFailed} with keyring.* equivalents
- secrets usecase: remove dead errors.Is check that both branches already
  returned ErrDecryptionFailed anyway
- tests: update assertions to reflect the new contract (ErrDekNotFound
  and ErrKekNotFound no longer propagate through the seam)

crypto/domain now has zero imports in feature production code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ADR-0013 flagged the hardcoded nonceSize = 12 in transit/usecase as a
known negative: a future AEAD algorithm with a different nonce length
would require a code search to find the buried constant.

- domain/const.go: add AEADNonceSize = 12 with an explanatory comment
  tying it to the ADR-0002 wire format contract
- domain/encrypted_blob.go: add NewFramedBlob(version, nonce, ct) and
  EncryptedBlob.SplitNonce() so framing is a domain concern, not usecase
  implementation detail
- domain/encrypted_blob_test.go: add round-trip and error-path tests for
  both new functions
- transit_key_usecase.go: remove const nonceSize and the manual
  append/slice framing; delegate to NewFramedBlob and SplitNonce

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The audit log use case previously held a raw *cryptoDomain.KekChain,
exposing key material outside the keyring module. Replace kekChain +
AuditSigner with a new keyring.KeySigner interface whose implementation
keeps HKDF key derivation and HMAC-SHA256 signing entirely inside the
keyring package.

Canonicalization logic moves from auth/service to auth/domain.AuditLog.Canonical()
— a domain method — so the byte representation that gets signed stays with
the type it describes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Align test/integration/audit_log_signature_test.go with the
NewAuditLogUseCase refactor that replaced (AuditSigner, *KekChain)
with a single keyring.KeySigner parameter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@allisson allisson merged commit 97aecea into main May 23, 2026
3 checks passed
@allisson allisson deleted the improve-codebase branch May 23, 2026 20:04
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.

1 participant