By default, one account runs the owner app, the receiver and the witness. A process that runs as that account can change the receiver's state directly. It can also sign a false history with the witness key. A passkey does not stop this, because the passkey protects approvals, not the receiver's files.
scripts/local_roles.py moves the receiver and the witness to their own
operating-system accounts on the same computer.
| Before | After install |
|---|---|
| Receiver and witness keys are files in the owner home. | Each key is readable only by its service account. The owner home keeps no role keys. |
| Receiver and witness state are SQLite files the owner can write. | Each state is owned by its service account, mode 700. |
| The owner app runs the role commands itself. | The owner app reaches each role only through its role service (openproof-role serve) on a Unix socket. The service answers only the owner's account and root, by the user ID the kernel reports for the connection, and runs a fixed set of operations, one length-prefixed JSON message for each connection. Every change still passes the receiver's approval and passkey checks. |
| Role code runs from the checkout. | The services run one root-owned copy of the openproof-role binary. It holds only the receiver and witness code (openproof-receiver, openproof-core), not the owner app, the broker or the gate. No interpreter or downloaded package is involved. install refuses to continue if the owner account can change the role location. |
The owner keeps the approval gate, the authority key and the root key. Trust
epochs (passkey set-up, renew) still work: the role services install a
root-signed epoch, the role state refuses rollback and refuses to remove or
replace a pinned owner passkey, and the service refuses a change of role keys.
- It does not make the witness independent. The witness still runs on the owner's computer, and the root key is still in the owner home. G2 still needs a witness operated by another party.
- It does not protect against an administrator who enters the administrator password. Root can read every account's files.
- Without a passkey, the authority key is still a file the owner account can read. Set up a passkey first (PASSKEY.md).
-
Stop the owner app.
-
Execute or recover all approved actions.
installrefuses to start while some receipts are not witnessed. -
Pull and build a checkout you have checked:
cargo build --workspace --locked. -
Run, from the checkout. The script needs only the Python standard library, so use the system Python, isolated (
-I), not a virtual environment that your account can change:sudo /usr/bin/python3 -I scripts/local_roles.py install --home outputs/passkey-trialIf your login is a standard account, run it through the administrator account with
su ADMIN -c 'sudo …'and full paths.The install:
- copies the
openproof-rolebinary and the status check (roles_status.py) to/private/var/db/openproof/libexec/as root; - creates the hidden accounts
_openproofrcvand_openproofwit; - copies the role keys and state, and starts two launchd daemons
(
com.antagon.openproof.receiver,.witness) whose sockets are/private/var/db/openproof/sockets/ROLE/service.sock, in a folder only that role's account can change; - compares the history before and after, then moves the owner's copies of the role folders into a root-only backup.
If any step fails, the install undoes its changes and the owner home stays as it was.
With
--publishat the end of the command, the receiver also writes each approved document as a file that only its account can change (PROTECTED-FILES.md). - copies the
-
Start the owner app as usual. To move an existing install to a new binary, or to add or remove
--publish, rununinstall, theninstallagain.statusrefuses an install record of an earlier version of the script and says so;uninstallaccepts it.
uninstall also removes an install made before 10 Nov 2026, which used
loopback TLS. install then sets up the Unix sockets.
If install stops with an error, it undoes its changes. If uninstall stops
with an error before it has returned the role folders, the keys and the history
are still in the role location, and the services are stopped. The folder of
protected files, if there was one, is already removed. In both cases,
correct the cause and run the same command again. The home and the role
location must be on the same volume.
Run it from the root-owned copy that install made, as your own account or as the administrator:
/usr/bin/python3 -I /private/var/db/openproof/libexec/roles_status.py --home outputs/passkey-trial
The status check asks the role services directly on their sockets. It never
runs the owner app's binary, the installer or other code from the checkout,
and it does not rely on what the installer did: it checks the result
(TCB.md). Code that runs in your account can still change what your
own terminal shows, for example through your shell's start-up files. For a
check that does not depend on your account, run the same command as the
administrator with sudo. The role services answer only your account and
root, so another account without sudo gets "not reachable".
The status check believes only what root owns. If the install record is not a file of root under folders of root, it stops with an error.
"separated": true means all of this (TCB.md has the reasons and
the limits):
- the role binary and the status check are files of root that no other
account can write (
code_protected).code_sha256is the SHA-256 of the installed role binary. Compare it with a binary from a build that you trust, for example one that you built before an agent ran in your account; - the two role accounts are separate accounts without a login shell
(
accounts_separate); - each role folder belongs to its role account, and only that account can
enter it (
folder_private); - the file that starts each service is a file of root and holds exactly what
an install puts in it (
service_as_installed); - each socket is in a folder that only its role account can change
(
socket_folder_protected), and the process that answers there runs under that account; - both services answer, and the owner home belongs to the owner.
"role_keys_out_of_owner_reach": true means that no role file is where your
account can read it, in the role location or in the role folders of the owner
home. The status check cannot find a copy in another place.
"passkey_required_by_receiver": true means the receiver service itself
reports an owner passkey in the trust it admits under: it then refuses every
new change without an assertion from that passkey. The service reads this from
the trust it enforces, so it reports what the receiver requires, whatever the
owner app shows. G1 holds when both values are true (TCB.md).
With --publish, the answer also has a "protected_files" part
(PROTECTED-FILES.md).
sudo /usr/bin/python3 -I scripts/local_roles.py uninstall --home outputs/passkey-trial
This stops the daemons, returns the keys and the full history to the owner home, compares the history, keeps the role folders in a root-only backup, and removes the accounts and the role code. It also removes the folder of protected files; the documents stay in the history.
scripts/tests/test_local_roles.py runs the full install, workflow, trust
renewal, separation probe, a refused connection from a third account, status
and uninstall with real accounts, and the same for protected files with a
failed install. It runs only as root on Linux with
OPENPROOF_ROLE_ACCOUNT_TEST=1. scripts/tests/test_role_service.py checks the
role service itself on every run: the peer check, the message form, each
role's operations, tampering, replay, opt-in trust updates and protected
files. The root test also puts the install into states that a defective
installer could leave, and checks that the status check reports each one.
On a Mac (launchd, 10 Nov 2026), these were checked by hand: uninstall of a
version 3 install, install --publish of version 4, and the status check as
the owner and as the administrator with sudo, with every value true. The
states of a defective install were tested on Linux only. Confirm an install
with the status check.