Skip to content

Add system keyring credential caching - #865

Draft
wlynch wants to merge 3 commits into
sigstore:mainfrom
wlynch:claude/gitsign-system-keyring-563b8b
Draft

Add system keyring credential caching#865
wlynch wants to merge 3 commits into
sigstore:mainfrom
wlynch:claude/gitsign-system-keyring-563b8b

Conversation

@wlynch

@wlynch wlynch commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Adds a credential cache backed by the OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service), so signing credentials (ephemeral private key + Fulcio cert/chain) can be reused for the lifetime of the certificate without running the gitsign-credential-cache daemon:

git config --global gitsign.credentialCacheMode system

(or GITSIGN_CREDENTIAL_CACHE_MODE=system; socket selects the existing daemon.)

Design

  • Per-identity caching, multiple identities at once. The OIDC identity isn't known until the auth flow completes, so entries are keyed by a hash of the configuration used to obtain them: Fulcio URL, OIDC issuer, client ID, connector ID, and committer email (user.email). Per-repo config naturally selects different cached identities.
  • Lifetime = cert lifetime. NotAfter is recorded at store time; expired or invalid entries are deleted lazily on read and re-validated against the Fulcio roots (same checks as the daemon client, incl. the 30s expiry window).
  • Soft failure. Keyring errors (locked keychain, headless Linux without a Secret Service/D-Bus session) fall through to the normal OIDC flow. Plain misses are a sentinel error, so first use doesn't print error getting cached creds (for either backend).
  • Windows blob limits. The chain is chunked across entries to stay under Credential Manager's 2560-byte credential blob limit.
  • Uses 99designs/keyring for native OS credential stores (Windows Credential Manager, Secret Service, KWallet), with its Keys() API powering enumeration. Its macOS Keychain backend requires cgo, which gitsign builds don't use (releases cross-compile darwin on Linux runners), so on macOS a small backend implements the keyring.Keyring interface on top of the /usr/bin/security CLI — behavior is identical across release binaries and source builds regardless of CGO settings.

Daemon consolidation

Both backends now share the same building blocks (cache.Cache/cache.Manager interfaces, key derivation, cert validation, credential encode/decode):

  • The daemon client uses the config-derived identity key instead of hostname@cwd. The key is opaque to the daemon, so mixed client/daemon versions interoperate; the daemon gains per-identity, multi-identity caching. Behavior change: repos sharing identical identity config now share a cached credential instead of caching per working directory (see cmd/gitsign-credential-cache/README.md update).
  • The daemon stores entries with a TTL matching the certificate lifetime instead of a fixed 10 minutes (behavior change for private Sigstore instances with longer-lived certs), overwrites on re-store instead of erroring, and rejects already-expired certs.

Management

New gitsign credentials list / gitsign credentials clear [--all] subcommands inspect and remove cached credentials for whichever backend is configured (keyring directly via Keys() enumeration; daemon via new ListCredentials/DeleteCredential/DeleteAllCredentials RPCs — old daemons get an explicit "upgrade the daemon" error).

$ gitsign credentials list
EMAIL             ISSUER                            CLIENTID  CONNECTOR  FULCIO                       EXPIRES                    STATUS
you@example.com   https://oauth2.sigstore.dev/auth  sigstore  -          https://fulcio.sigstore.dev  2026-08-04T12:10:00-04:00  valid

Docs

  • New docs/keyring-cache.md: setup, multi-identity semantics, platform notes, and security considerations (per-session process access, encrypted-at-rest persistence across reboots, config-derived key caveat).
  • README config/env tables + FAQ, daemon README, regenerated docs/cli/.

Reviewer notes

  • Wire compatibility: gob ignores the new StoreCredentialRequest.Meta field on old daemons; cache keys are opaque strings; the client maps "not found" errors to the miss sentinel by message match (net/rpc flattens errors to strings).
  • A golden test (internal/cache/key_test.go) pins the key derivation so accidental changes that would orphan users' stored entries fail CI.
  • The keyring backend was verified end-to-end against a real macOS Keychain (store/get/list/clear round-trip, under both CGO_ENABLED=0 and 1), and gitsign credentials was verified against a live daemon over its socket.
  • Security model is documented as comparable to the daemon socket (any process in the user session can read entries); a follow-up idea discussed for macOS is a darwin && cgo backend using Security.framework ACLs so only the gitsign binary reads silently.

Testing

  • Unit tests: keyring round-trip, multi-identity, expiry-deletes-entry, chain chunking, wrong-root validation, unavailable keyring, delete/delete-all, key-derivation golden + distinctness, shared cert validation, config precedence, cache-mode dispatch, daemon store/get/overwrite/expired-reject/list/delete over a real socket.
  • go build ./..., go vet ./..., full go test ./... pass.

🤖 Generated with Claude Code

wlynch and others added 2 commits August 4, 2026 18:36
Add a credential cache backed by the OS keyring (macOS Keychain, Windows
Credential Manager, Linux Secret Service) via zalando/go-keyring, so signing
credentials (ephemeral private key + Fulcio cert/chain) can be reused for the
lifetime of the certificate without running the gitsign-credential-cache
daemon. Enable with gitsign.credentialCacheMode=keyring (or
GITSIGN_CREDENTIAL_CACHE_MODE=keyring).

Credentials are cached per identity, keyed by a hash of the configuration
used to obtain them (Fulcio URL, OIDC issuer, client ID, connector ID, and
committer email), so multiple identities can be stored concurrently. Expired
or invalid entries are deleted lazily on read, and keyring failures (locked
keychain, headless hosts) fall through to the normal OIDC flow. Chain data is
chunked across entries to stay under the Windows credential blob size limit.

The existing daemon cache is consolidated onto the same building blocks:

- A shared cache.Cache interface, credential key derivation, cert validation,
  and credential encode/decode helpers are used by both backends. The daemon
  client now uses the config-derived identity key instead of hostname@cwd
  (the key is opaque to the daemon, so mixed client/daemon versions
  interoperate), giving the daemon per-identity, multi-identity caching.
- The daemon stores entries with a TTL matching the certificate lifetime
  instead of a fixed 10 minutes, overwrites on re-store instead of erroring,
  and rejects already-expired certs.
- Plain cache misses are reported as a sentinel error so first use no longer
  prints "error getting cached creds".

A new `gitsign credentials list` / `gitsign credentials clear [--all]`
subcommand inspects and removes cached credentials for whichever backend is
configured (keyring directly, or the daemon via new List/Delete RPCs; old
daemons get a clear upgrade error).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Billy Lynch <billy@chainguard.dev>
gitsign.credentialCacheMode now accepts "system" (or "socket") - the
"keyring" alias is removed before the option ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Billy Lynch <billy@chainguard.dev>
@wlynch
wlynch marked this pull request as draft August 4, 2026 23:03
Replace zalando/go-keyring with 99designs/keyring for the system keyring
credential cache:

- Keys() enumeration removes the need for the best-effort index entry that
  powered `gitsign credentials list` - entries are now enumerated directly
  from the keyring.
- Storage is restricted to native OS credential stores (Windows Credential
  Manager, Secret Service, KWallet) - no file/pass fallbacks that would
  need their own password prompts.
- 99designs' macOS Keychain backend requires cgo, but gitsign is built with
  CGO_ENABLED=0 everywhere (releases cross-compile darwin on Linux runners).
  On macOS, use a small backend implementing the keyring.Keyring interface
  on top of the /usr/bin/security CLI instead (the same approach
  zalando/go-keyring uses). This keeps behavior identical across release
  binaries and source builds regardless of CGO settings.
- Tests inject keyring.NewArrayKeyring instead of relying on process-global
  mock state; a live unavailable-keyring stub covers soft-fail behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Billy Lynch <billy@chainguard.dev>
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