← index2026-05-07 17:51 (Beirut)(backfill from DOCUMENTATION/)

Brian Evidence-Gate — Day 5 #3 plan: Ed25519 migration for true uid separation

Brian Evidence-Gate — Day 5 #3 plan: Ed25519 migration for true uid separation

Status: PLAN, not yet implemented (2026-05-07).
Prerequisite for: Real OS-enforced trust boundary (Day 4 #4 closure).

Why this is needed

Day 4 #4 attempted uid separation by giving a non-root brian-validator user
ownership of the HMAC key. Codex round-table flagged this as a fundamental
design flaw: HMAC is symmetric. The gate hook runs INSIDE Brian's CC process
and needs to verify signatures. To verify it must read the key. But if Brian's
CC process can read the key, it can also forge entries.

Symmetric crypto cannot give us uid separation when the verifier and the
forger run in the same process.

The fix: switch from HMAC-SHA256 to Ed25519 asymmetric signatures.

Target architecture

brian-validator (system user, no shell)
├── /root/.claude/system/secrets/validator_ed25519.key      (private signing key, 0400 brian-validator)
└── /root/.claude/system/secrets/validator_ed25519.key.prev (rotation overlap)

shared (everyone-readable):
└── /root/.claude/system/secrets/validator_ed25519.pub      (public verification key, 0444)

Brian-CC process (regular user):
├── invokes: sudo -u brian-validator /opt/agent/scripts/brian_validator.py
├── reads: validator_ed25519.pub for gate's _verify_signature
└── CANNOT read: validator_ed25519.key (private key, OS-enforced 0400)

The brian-validator process signs entries with its private key. Brian's CC
process verifies signatures using the public key. Even if Brian-CC is fully
compromised, it cannot forge — private key is unreachable.

Migration steps (each independently testable)

1. Add asymmetric crypto to validator runner

2. Add asymmetric verification to gate

3. Generate keypair

python3 -c "
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
    Encoding, PrivateFormat, PublicFormat, NoEncryption)
import os
priv = Ed25519PrivateKey.generate()
priv_bytes = priv.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
pub_bytes = priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
open('/root/.claude/system/secrets/validator_ed25519.key', 'wb').write(priv_bytes)
open('/root/.claude/system/secrets/validator_ed25519.pub', 'wb').write(pub_bytes)
os.chmod('/root/.claude/system/secrets/validator_ed25519.key', 0o400)
os.chmod('/root/.claude/system/secrets/validator_ed25519.pub', 0o444)
"

4. Run uid prep

/opt/agent/scripts/setup_validator_uid_separation.sh prep

This creates brian-validator + sudoers entry. Update its key chown logic
to also chown the new Ed25519 private key.

5. Switch Brian's CC harness to non-root user

6. Verification + cutover

7. Decommission HMAC

Why this is Day 5+ work, not today

This plan is the spec; the implementation should be done in a fresh session
with full operator attention. The Day 4 #4 prep script does the safe parts
(create user, install sudoers); this plan does the unsafe parts (uid switch
of running services).

Defenses gained

Defenses still partial after this