Skip to content

Close the at-rest gap: sign the binary, then re-test the macOS Keychain #2

Description

@DanielCarmingham

This is the one place gitwho is genuinely weaker than git-credential-manager, and it is worth stating plainly rather than leaving implied.

The gap

identity.key and secrets.age sit in the same directory, both owned by the user. Any process running as that user reads both and decrypts. The encryption is therefore nominal against a local-process threat — a malicious npm postinstall, a compromised dev tool.

Worse than that, local code need not go near the key or the ciphertext. It can just ask:

printf 'protocol=https\nhost=github.com\npath=Org/repo.git\n' | gitwho credential get

and get password=<token> back. The credential helper is a decryption oracle for its own store by construction — being one is the entire job.

git-credential-manager stores in the login Keychain, where a different application must be ACL-authorised or macOS prompts the user. That barrier is what gitwho lacks. FileVault covers powered-off, not this.

Why the Keychain backend is implemented but not the default

KeychainBackend exists and sits behind the same Backend trait. It was abandoned during development for a measured reason: macOS keys a Keychain ACL to the calling binary's designated requirement, and for an unsigned binary that is its code hash. Writing an entry with one build and reading it with the next hung on a GUI prompt until killed. On a credential helper that runs on every git transport operation, that is fatal.

The binary is currently ad-hoc signed with no TeamIdentifier, so its ACL identity changes on every rebuild.

The fix, in order

  1. Sign the installed binary with a stable Apple Development identity, so the designated requirement survives rebuilds.
  2. Re-run examples/keychain_probe.rs — it writes an entry with one build, rebuilds, and reads with the next. That probe exists precisely to answer this and takes about a minute. If a stable signature fixes the prompt it passes; if it does not, the Keychain stays off the table and the gap has to be closed another way.
  3. If it passes, switch the default backend on macOS. It is a default change plus a migration path from the existing age store — not a rewrite.

Step 2 is not optional and must not be skipped on the grounds that step 1 "should" work. A valid signature does not prove the ACL survived; only the probe does.

Worth evaluating alongside

  • age-plugin-se encrypts to Apple's Secure Enclave, which would give hardware-backed keys without the per-binary ACL problem — possibly a better fit than the Keychain, since the failure mode above is specifically about per-application ACLs. Should be measured against the same latency budget before being preferred.
  • Whatever is chosen, the read stays on the hot path: single-digit milliseconds, with the existing regression test that pins secret reads under 100 ms.

What this does not fix

Nothing here stops code running as you from calling gitwho credential get. A Keychain ACL raises the cost of reading the store at rest; it does not remove the oracle. Any claim otherwise would be overselling it — see SECURITY.md.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedWaiting on another issue or on hardwareenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions