fix(kryphos): stop the audit log recording credential names in cleartext - #457
Merged
Conversation
#215 stopped the fjall store revealing credential names. The tamper log kept writing them verbatim, so post-#215 the audit log was the weaker link for the exact threat that issue named: filesystem access without the passphrase. An adversary with the log learned the operator's credential inventory, and from the operation sequence their rotation and revocation cadence, without touching the AEAD. The entry now carries a reference derived from the name under the vault key, the same treatment `lookup_key` gives names in the keyspace. Deterministic, so every entry touching one credential shares a reference and its history stays followable; keyed, so a reader with the file and not the passphrase learns neither the name nor whether two vaults hold the same one. A separate domain from `lookup_key`, deliberately. The two identifiers answer different questions -- one addresses a fjall record, the other groups audit entries -- and a shared domain would make an audit reference usable as a store key, a capability the audit log has no reason to hand anyone. The koinon field is renamed `credential_name` to `credential_ref`, because a field called `credential_name` holding a digest is exactly the kind of lie this repo's audits keep finding. Its doc says what it is and, more usefully, says that koinon does not derive it: what the derivation is, and therefore who can reverse it, belongs to the writer. `vault_mutations_append_intact_tamper_log` asserted the plaintext name round-tripped -- it documented the leak. It now asserts the reference is not the name, is the fixed-width derived form, is stable across one credential's three entries, and differs between credentials. `the_audit_log_does_not_reveal_credential_names` is the sibling of #215's `on_disk_fjall_contents_do_not_reveal_credential_name`, scanning the raw log bytes, with an acceptance partner asserting the operation IS still recorded so the absence is a protected name rather than an empty log. Mutation-checked: reverting the writer to the plaintext name fails both. Closes #378
forkwright
pushed a commit
that referenced
this pull request
Aug 21, 2026
🤖 I have created a release *beep* *boop* --- ## [0.6.1](v0.6.0...v0.6.1) (2026-08-21) ### Bug Fixes * **kerykeion:** surface AES init failures during trial decryption ([#459](#459)) ([6a0e514](6a0e514)) * **kryphos:** stop the audit log recording credential names in cleartext ([#457](#457)) ([84055c0](84055c0)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #378
Post-#215, the audit log was the weaker link for the exact threat #215 named.
That issue stopped the fjall store revealing credential names.
tamper.logkept writing themverbatim, so an adversary with filesystem access and no passphrase learned the operator's credential
inventory — and from the operation sequence, their rotation and revocation cadence — without ever
touching the ChaCha20-Poly1305 AEAD. Two stores, one threat model, and only one of them defended.
One detail shows the protection was already at the wrong layer:
LogEntryKind's manualDebugimplhas always printed
<redacted>for this field. The type knew it was sensitive and kept it out oflogs, while writing it to disk in the clear.
The fix
The entry now carries a reference derived from the name under the vault key — the same treatment
Vault::lookup_keyalready gives names in the keyspace. Deterministic, so every entry touching onecredential shares a reference and its history stays followable; keyed, so a reader holding the file
and not the passphrase learns neither the name nor whether two vaults hold the same one.
A separate domain from
lookup_key, deliberately. The two identifiers answer different questions— one addresses a fjall record, the other groups audit entries — and a shared domain would make an
audit reference usable as a store key. That is a capability the audit log has no reason to hand
anyone.
The field is renamed
credential_name→credential_ref. A field calledcredential_nameholding a digest is precisely the class of lie this repo's audits keep finding. Its doc also states
that koinon does not derive the reference: what the derivation is, and therefore who can reverse
it, belongs to the writer. koinon carries it and nothing more.
The test that documented the leak
vault_mutations_append_intact_tamper_logasserted the plaintext name round-tripped through the log.It was, in effect, the leak's specification — and it would have gone on passing forever.
It now asserts the four properties that actually matter: the reference is not the name, is the
fixed-width derived form, is stable across one credential's three entries, and differs
between credentials. Stability and distinctness are the point of deriving rather than randomising —
without them an operator could not follow one credential's history.
the_audit_log_does_not_reveal_credential_namesis the sibling of #215'son_disk_fjall_contents_do_not_reveal_credential_name, scanning the raw log bytes for the name. Itsacceptance partner asserts the operation string is still present, so a pass means a protected
name rather than a log that recorded nothing.
Mutation-checked rather than assumed: reverting the writer to
CompactString::from(name)fails bothtests.
What this does not do
#378offered two routes — protect the name, or document the exception. This takes the first, so notrust-model carve-out is needed and the README needs no change.
It also does not encrypt the operation (
add/rotate/revoke/remove), which stays cleartext bydesign: it carries no inventory information on its own, and an audit log that cannot be read at all
by its own operator is not an audit log. The cadence of operations remains visible; what is now
hidden is which credential each one concerns.
Verification
utilities/pre-push-verify.sh, CI-exact across all seven affected packages: