USB Passthrough — Phase 2e Security Review Checklist
This page is for an external reviewer to walk before USB passthrough is enabled by default. It is not itself a sign-off — that lives in whatever ticket / record system the project uses.
Until every item below is checked off and signed by a reviewer who is
not the author of the code, the passthrough feature must remain
behind enable_usb_passthrough(True) (off by default).
Threat model
Trust boundary: the viewer is a peer outside the host’s local
trust domain. They can send arbitrary frames over the usb
DataChannel. The host must never:
Claim a device the operator has not approved (ACL).
Claim more devices than the policy allows (max_claims).
Spend unbounded buffer space on viewer-driven payloads (payload cap + credit window).
Continue to honor a viewer that is provably misbehaving (rate / lockout, inherited from the REST auth gate when channels are gated by the same session).
The viewer is also a potential victim of a malicious host — but this checklist is host-side only. A separate review for the viewer client comes in Phase 2f.
ACL
- [ ]
UsbAcldefaults to"deny"when no file exists. Verify with a fresh user account.
- [ ]
- [ ] When the file is corrupt / wrong version, the ACL also defaults
to deny (test
test_unknown_version_is_ignored).
- [ ]
prompt_on_openrules without a wired callback fall back to deny (test
test_session_prompt_no_callback_means_deny).
- [ ]
- [ ] If the prompt callback raises, the open is denied (test
test_session_prompt_callback_raising_means_deny).
- [ ] ACL file is written with mode
0o600on POSIX (test test_save_persists_to_disk_with_safe_mode).
- [ ] ACL file is written with mode
- [ ] Recommend storing the ACL on a filesystem that supports POSIX
permissions; document the Windows ACL story in the deploy guide.
- [ ] OQ8 — ACL integrity (HMAC) implemented. The ACL carries an
HMAC-SHA256 sidecar signature and fails closed on tamper (tests
test_tampered_acl_file_fails_closed,test_explicit_key_roundtrip_and_wrong_key_fails). Residual risk: the default key lives in a same-user-readableusb_acl.json.key; a same-identity process can still forge a signature. High-assurance deployments should pass a keychain-derived key viaUsbAcl(hmac_key=...)— confirm whether the deployment does.
Audit
- [ ] Every ACL decision is logged via
audit_logwith one of: usb_open_allowed,usb_open_denied,usb_open_rejected_max_claims,usb_open_backend_error,usb_close. Confirm by inspecting recent audit rows after a manual exercise.
- [ ] Every ACL decision is logged via
- [ ] Audit rows include
viewer_idso a row can be attributed to a peer (test
test_session_audit_captures_open_decisions).
- [ ] Audit rows include
- [ ] Audit log itself is hash-chained (round 25). Confirm
verify_chain()returnsok=Trueafter a passthrough session.
- [ ] Frame-level transfer logging is intentionally OFF to avoid
capturing key material on YubiKey-class devices. ERRORs only are surfaced via the project logger.
Protocol hardening
- [ ] Frame header is 4 bytes;
decode_framerejects buffers smaller than that (test
test_decode_rejects_short_buffer).
- [ ] Frame header is 4 bytes;
- [ ] Unknown opcodes raise
ProtocolError(test test_decode_rejects_unknown_opcode) — the session never sees the bad frame.
- [ ] Unknown opcodes raise
- [ ] Payloads are capped at
MAX_PAYLOAD_BYTES(16 KiB) on both decode (test
test_decode_rejects_oversize_payload) and construct (testtest_frame_constructor_validates).
- [ ] Payloads are capped at
- [ ] CTRL/BULK/INT request bodies that fail to parse return ERROR,
not crash (test
test_bad_transfer_payload_returns_error).
- [ ] Backend exceptions are caught and returned as
{"ok": false, "error": "..."}— the session never propagates a host-side RuntimeError to the wire (testtest_backend_error_translates_to_ok_false).
Resource bounds
- [ ]
max_claimscap enforced (test test_max_concurrent_claims_enforced).
- [ ]
- [ ] CREDIT-based inbound flow control prevents a peer from filling
the host’s process queue (test
test_credit_exhaustion_returns_error).
- [ ] CREDIT replenishment is 1 frame per reply — well-behaved peer
doesn’t stall (test
test_each_transfer_consumes_then_replenishes_one_credit).
- [ ] CREDIT messages with bad payloads are silently dropped (test
test_credit_message_with_bad_payload_is_ignored).
- [ ] CREDIT for unknown claim_id is silent (test
test_credit_message_for_unknown_claim_is_silent).
Lifecycle
- [ ]
close_all()releases every outstanding handle and tolerates per-handle close errors (test
test_close_all_releases_every_outstanding_claim).
- [ ]
- [ ] FakeHandle
closeis idempotent (test test_backend_handle_close_is_idempotent); same property verified for the libusb backend during hardware testing.
- [ ] FakeHandle
- [ ] Closing a handle and then issuing a transfer raises (test
test_fake_handle_transfer_after_close_raises).
- [ ] Viewer client
shutdown()releases pending request waiters (test
test_shutdown_unblocks_pending_transfers).
- [ ] Viewer client
Per-OS requirements
- [ ] Linux libusb: udev rule documented for the target devices;
tested without root.
- [ ] Linux libusb:
libusb_detach_kernel_driverinvoked before a HID device is claimed; reattached on close. Confirm host OS keyboard / mouse remains functional after a session.
- [ ] Linux libusb:
- [ ] Windows WinUSB (Phase 2b — implemented, hardware-unverified):
the device must already be associated with WinUSB (Zadig / libwdi); only bound devices appear in
list(). Sign off only after running the bulk / HID / composite test matrix on real hardware.
- [ ] macOS IOKit (Phase 2c — implemented, hardware-unverified):
native IOKit enumeration + libusb transfers. Notarisation for non-App-Store distribution; document SIP exclusions. Sign off only after running the test matrix on real hardware.
- [ ] All three backends: opening a device that another driver owns
surfaces as a clear “busy” RuntimeError, not a hang or crash.
Pen-test scenarios
These are recommended scenarios for an external pen-tester to attempt before sign-off. None should succeed:
ACL bypass via case folding. Try VID/PID with mixed case and leading zeros; confirm only the canonical form matches.
ACL bypass via Unicode normalization. Try a serial string that is visually identical but Unicode-different from the rule.
Credit DoS. Send 1 million transfer frames as fast as possible against a small
max_claims; confirm host RSS stays bounded.Frame fragmentation attack. Send a frame with a header that claims a payload size larger than what arrives; confirm
decode_framerejects the truncated stream.Concurrent OPEN race. Two peers (or one peer with multiple threads) issuing OPEN simultaneously — confirm exactly one
claim_idis granted per OPEN request and the bookkeeping doesn’t drift.Audit tampering. Edit an
usb_*row inaudit.dbvia raw SQLite; confirmverify_chain()flags the row.Prompt callback timing. A slow prompt callback (sleeping 30s) should not allow another peer to slip a CTRL through in the meantime — confirm the prompt callback is awaited before any subsequent decision for the same vid/pid.
Permission downgrade. Run the host as a non-privileged user on Linux without the udev rule; confirm OPEN fails cleanly with a clear “permission denied” message rather than crashing.
Sign-off
Reviewer name: ____________________________________________________
Reviewer affiliation: _____________________________________________
Date: _____________________________________________________________
Items above all checked: [ ] yes [ ] no — list failing items below.
Recommendation:
[ ] Ready to ship Phase 2 default-on. [ ] Ready to ship behind opt-in flag (current state). [ ] Block release; remediation required.
Notes / remediation list: