Repository navigation
Conversation
`item-approval approve|decline --approver-signer stdio --approver-public-key HEX` lets the process that holds a registered approver key sign the answer without handing the key to Flow. Flow looks the public key up under `approvers`, writes the exact bytes to sign as one JSON line on stdout and reads one line back: a signature, checked against the registered key before use, or a refusal. Anything else is a refusal, and nothing is recorded. Desktop uses this to sign with a key kept behind Touch ID. Also adds `authority.remove_principal`, which removes a registered key only while it is still the one registered, for key rotation and removal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a Mac with a Secure Enclave, Run it no longer needs terminal setup. The approval screen shows "Set up your approver key" until the key exists: one click and one Touch ID (or login password) prompt. - The Ed25519 approver key is sealed to a Secure Enclave key whose access control asks for user presence on every use, and kept in the login Keychain. It is never written to disk unsealed, never logged and never given to Flow. - Setup registers the public key under `approvers` with Flow's own `register_principal`, the function `jit register` calls. - Each answer runs Flow with `--approver-signer stdio`. Desktop checks that the bytes Flow asks it to sign are the answer the person chose, prompts, signs and drops the key. - No key, a cancelled prompt or a Keychain error means no answer. - Settings > Approver key replaces the key (new one registered before the old one is retired) or removes it. - Windows, Linux and Macs without a Secure Enclave keep the terminal setup. The Keychain's own access-control flags need a keychain-access-groups entitlement the Python engine doesn't have (errSecMissingEntitlement), so the key is sealed to the Secure Enclave instead, through Security.framework over ctypes. Real-hardware tests are opt-in (OPENADAPT_REAL_KEYCHAIN_TESTS=1) and never show a prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
errSecInteractionNotAllowed (-25308) while the screen is locked made the availability check remember False, so Desktop showed the terminal setup until restart. The check is no longer remembered then, and the refusal says to unlock the Mac instead of blaming Touch ID. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…feat/desktop-keychain-approver
… Touch ID Macs From the security review of #1500: - setup() re-registered a stored but unregistered key without a prompt, so anything that can write the Keychain item could plant its own public key. It now requires the key to open behind a presence prompt (a fresh signature over a setup challenge) first, and refuses an item that admits it has no presence check. - On a Mac that can keep the key behind Touch ID but has none set up, Run it fell back to a terminal-made file key, which asks for no presence. It now refuses and points to setup in OpenAdapt; the file key counts only where the app can't keep the key. Settings says so. - Desktop 0.24.0 with its changelog (after #1486's 0.23.0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Oct 25, 2026
From the security review of #1519: - Answering stops re-checks every card after the signature returns and before anything changes, so a card that changed or expired while the Touch ID prompt was up changes nothing. - Queue answers on a Mac that can keep the key behind Touch ID never fall back to a terminal-made key (as Run it, #1500). - Removed the unreachable re-arm branch and a method pasted into the portal's StopQueueBackend Protocol. - 0.24.0 changelog entries for the stop queue and re-arm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
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.
Follows #1486 and is stacked on its branch. Retarget to
mainonce #1486 merges.Problem
After #1486, Run it counts an approval only when a registered approver key signs it. Desktop users had to set that key up at a terminal (
jit keygen, thenjit register --role approver) before "Approve and save" worked.Change
On a Mac with a Secure Enclave (Apple silicon or a T2 chip), the key is set up in the app.
Setup is one screen and one prompt. Until the key exists, the approval screen shows Set up your approver key in place of Approve. One click and one Touch ID or login-password prompt do the following:
approverswith Flow'sregister_principal, the same functionjit registercalls.Every answer prompts. Desktop runs
item-approval approve|decline --approver-signer stdio --approver-public-key HEX. This is a new Flow option:decided_byhuman, routedesktop_run_it, and canonical JSON.Key handling. Flow never receives the key. Desktop holds the unsealed key only for one signature and zeroes the buffers. The key is never written to disk unsealed and never logged. The audit log records a 16-character fingerprint only.
Fail closed. Desktop sends no answer, and the screen says why, in each of these cases:
Rotation and removal. Settings > Approver key does both:
Flow's new
authority.remove_principalremoves a registered key only while it's still the registered one.Windows, Linux, and Macs without a Secure Enclave keep the terminal path from #1486, and the screen and Settings show those steps. Each would need its own presence-backed key:
KeyCredentialManager, a TPM key that asks for the PIN or face on each use).Why the Secure Enclave, not a Keychain access-control flag
The Keychain's
kSecAttrAccessControl(.userPresence) returnserrSecMissingEntitlement(-34018) for a process without a keychain-access-groups entitlement. That covers both the legacy and the data-protection Keychain, and the engine is a Python sidecar. A non-permanent Secure Enclave key with the same access control needs no entitlement. The Secure Enclave enforces the prompt itself: a caller that can't show a prompt getsLAErrorNotInteractive(-1004).PyObjC can't wrap a Secure Enclave key reference (its CF type lookup raises during the call), so
MacEnclavecalls Security.framework through ctypes. This adds no dependencies.Tests
Flow (
test_human_approval_stdio_signer.py, real subprocess):Desktop (
tests/test_engine/test_approver_keychain.py, fake Secure Enclave and in-memory keyring, 27 tests):--approver-signer stdioand no passphrase. Desktop refuses to sign a different request, decision, decider or route without prompting.Real Mac (
tests/test_approver_keychain_real.py, opt-in, no prompt shown):Run it with:
cd packages/desktop OPENADAPT_REAL_KEYCHAIN_TESTS=1 uv run pytest tests/test_approver_keychain_real.py -vResult: 3 passed on an M2 Max. Hosted macOS runners are VMs with no Secure Enclave, so the test skips there.
End to end, local: real Secure Enclave and Keychain, Desktop's
FlowBridge, and this branch's Flow CLI. Flow stored the answer with attestationdesktop-approver/desktop_key.Suites:
tscclean. New tests cover the setup-in-place-of-Approve flow.Not checked: a live Touch ID prompt from the packaged app. Automated tests can't touch the sensor.
Before this works in a released Desktop
openadapt-flow==2.1.0, which has neither--approver-signernorapprovers(fix: a person's approval needs a registered approver key's signature #1486 has the same dependency). Until a Flow release carries both and the pin moves, in-app setup refuses with "too old" and nothing is approved.desktop release intentwill ask for a version bump. A bump publishes Desktop on merge, so as in fix: a person's approval needs a registered approver key's signature #1486 it's left to the release decision.🤖 Generated with Claude Code