-
Notifications
You must be signed in to change notification settings - Fork 0
UX: consolidate dual macOS keychain prompts into a single password request during signing #160
Description
Problem
During commit (and artifact) signing, macOS shows two separate password prompts back-to-back:
auths-sign wants to use your confidential information stored in "dev.auths.agent" in your keychainauths-sign wants to use your confidential information stored in "dev.auths.password" in your keychain
This feels redundant and confusing to users. There is no reason to prompt twice — both reads happen in the same signing operation for the same key alias.
Root Cause
Two independent SecItemCopyMatching calls are made against separate keychain services:
MacOsPassphraseCache::load()→dev.auths.passphrase(crates/auths-core/src/storage/passphrase_cache.rs)MacOSKeychain::load_key()→dev.auths.agent(crates/auths-core/src/storage/macos_keychain.rs)
macOS prompts once per protected keychain item access, so two items = two prompts.
Proposed Fix
Use a shared LAContext (from LocalAuthentication.framework) evaluated once before both reads. Pass it to each SecItemCopyMatching call via kSecUseAuthenticationContext. macOS will recognise the context is already authorised and skip the second prompt.
Implementation outline
-
New
AuthContexthelper (crates/auths-core/src/storage/auth_context.rs, macOS only)- Wraps an
LAContextObjC object AuthContext::evaluate(reason: &str)— callsevaluatePolicy:localizedReason:reply:synchronously via a dispatch semaphore, returning an authorised context or an error- Exposes
as_cf_type_ref()for use askSecUseAuthenticationContext
- Wraps an
-
Context-aware variants on both keychain structs
MacOSKeychain::load_key_with_context(&self, alias, ctx: &AuthContext)MacOsPassphraseCache::load_with_context(&self, alias, ctx: &AuthContext)- Both inject
kSecUseAuthenticationContextinto their CFDictionary query; existing no-context methods remain unchanged for CI / file-fallback paths
-
Single evaluation point in the signing workflow
Inload_key_with_passphrase_retry(crates/auths-sdk/src/workflows/signing.rs):#[cfg(target_os = "macos")] let auth_ctx = AuthContext::evaluate("Auths commit signing").ok(); // Both subsequent reads receive auth_ctx
If evaluation fails (user cancels, item not biometric-protected),
auth_ctxisNoneand both calls fall back to their existing behaviour — no regression for CI or non-biometric setups. -
Propagate through
KeychainPassphraseProvider
Addget_passphrase_with_context(prompt, Option<&AuthContext>)alongside the existingget_passphrase.
Dependencies
Check whether objc2 / objc2-local-authentication is already in the tree. If not, a small extern "C" FFI shim against LocalAuthentication.framework avoids adding a new dep entirely.
Files to change
| File | Change |
|---|---|
crates/auths-core/src/storage/auth_context.rs |
New — AuthContext wrapper |
crates/auths-core/src/storage/macos_keychain.rs |
Add load_key_with_context |
crates/auths-core/src/storage/passphrase_cache.rs |
Add load_with_context |
crates/auths-core/src/storage/mod.rs |
Export auth_context |
crates/auths-core/src/signing.rs |
Add get_passphrase_with_context on KeychainPassphraseProvider |
crates/auths-sdk/src/workflows/signing.rs |
Create AuthContext once, pass to both reads |
Acceptance Criteria
- macOS shows exactly one keychain password dialog per signing operation
- CI environments (no keychain /
PrefilledPassphraseProvider) are unaffected cargo nextest run --workspacepasses on macOS, Linux, and Windows