Opaque Security Assessment¶
NOTE: This document is historical and may not match the current code. For the latest adversarial review and demos, see Adversarial security review.
Date: 2026-02-12
Assessor: Security Engineering Review
Scope: Full source tree (crates/), documentation (docs/), PRD, configuration, and dependency analysis
Codebase Version: 0.1.0 (pre-release)
Table of Contents¶
- Threat Model
- Code-Level Findings (Current Implementation)
- Protocol Security
- Approval Flow Security
- Filesystem Security
- Supply Chain
- Operational Security Guide
- Security Roadmap
1. Threat Model¶
1.1 Threat Actors¶
| Actor | Description | Capability | Motivation |
|---|---|---|---|
| TA-1: Malicious LLM Agent | An AI coding tool (Codex, Claude Code) that has been jailbroken, compromised, or is behaving adversarially. | Can invoke any RPC exposed over the UDS socket. Can send arbitrary JSON payloads. Can run arbitrary commands on the user's machine if the tool runtime permits. Can read any file the user can read. | Exfiltrate secrets, escalate privileges, perform unauthorized operations against SaaS targets. |
| TA-2: Compromised Dependency | A crate in the supply chain (e.g., zbus, objc2, or a transitive dep) that has been backdoored via a supply chain attack. |
Full code execution within opaqued or opaque process at build time (proc macros) or runtime. Access to all in-memory secrets, the UDS socket, and OS credentials. |
Exfiltrate secrets, install persistent backdoors, pivot to cloud resources. |
| TA-3: Same-User Attacker | A malicious process running under the same Unix UID as the Opaque user. This could be malware, a compromised npm package, a hostile VS Code extension, etc. | Can connect to the UDS socket (same UID), read/write files in the user's home directory, attach debuggers (on some configurations), read /proc/<pid>/mem (Linux). |
Trigger unauthorized approval prompts (approval spam/fatigue), invoke privileged operations, read audit data, denial of service. |
| TA-4: Network-Adjacent Attacker | An attacker on the same LAN, relevant when the iOS mobile pairing HTTPS server is active. | Network traffic interception, mDNS spoofing, ARP spoofing. | Intercept pairing QR data, man-in-the-middle the mobile approval channel, steal device pairing keys. |
| TA-5: Malicious MCP Server | A rogue or compromised MCP server that relays requests from LLM tools to Opaque. | Can craft arbitrary operation requests, replay requests, attempt to extract sensitive information from responses or error messages. | Exfiltrate secrets through error message side channels, abuse approval leases, trigger operations against unauthorized targets. |
| TA-6: Insider / Malicious Developer | A developer with commit access to the Opaque repository or access to the build pipeline. | Can introduce backdoors in code, weaken policy defaults, add exfiltration paths. | Long-term persistent access to secrets across all deployments. |
1.2 Attack Surface Map¶
| Surface | Components | Exposed To | Notes |
|---|---|---|---|
| UDS Socket | opaqued listener at $XDG_RUNTIME_DIR/opaque/opaqued.sock or ~/.opaque/run/opaqued.sock |
TA-1, TA-3, TA-5 | Primary attack surface. All operations flow through this socket. |
| Approval UI (macOS) | LocalAuthentication framework, Touch ID / password dialog |
TA-1 (indirect via spam), TA-3 | User-facing. Social engineering vector. |
| Approval UI (Linux) | polkit CheckAuthorization, system auth agent |
TA-1 (indirect via spam), TA-3 | User-facing. Intent visibility depends on auth agent. |
| Provider Connectors | 1Password CLI/API, HashiCorp Vault API, AWS SDK, GitHub/GitLab APIs | TA-2, TA-6 | Credentials for these are the crown jewels. |
| Audit Log | SQLite database, Parquet exports, LanceDB embeddings | TA-1, TA-3, TA-5 | If readable by agents, becomes an exfiltration channel for target names, operation metadata. |
| Semantic Search Pipeline | LanceDB vector index, embedding computation | TA-1, TA-3 | Embedding text must be sanitized. If not, secret refs leak into the vector store. |
| Mobile Pairing (iOS) | HTTPS server on LAN, QR code payload, Secure Enclave challenge-response | TA-4 | Network-level attacks during pairing window. |
| HTTP Proxy | http.request_with_auth operation (planned) |
TA-1, TA-5 | Authenticated HTTP requests on behalf of agents. Response body is an exfiltration vector. |
| Configuration Files | TOML policy files, profile mappings | TA-3 | If writable by attacker, policy can be weakened. |
| Process Memory | In-memory plaintext secrets during operation execution | TA-2, TA-3 (via ptrace/debugger) | Secrets exist in broker memory during operation execution. |
1.3 Threat-Risk Matrix¶
| Threat | Likelihood | Impact | Risk Rating | Rationale |
|---|---|---|---|---|
| TA-1: LLM agent approval spam | High | Medium | HIGH | approval.prompt is currently exposed as a generic RPC with no authorization. Any connected client can trigger unlimited approval popups. |
| TA-1: LLM agent triggers unintended operation | High | High | CRITICAL | No policy engine exists yet. Once operations are implemented, any client can invoke any operation. |
| TA-3: Same-user process connects to socket | High | High | HIGH | Socket permissions (0600) are correct but same-UID processes can connect. No client identity verification beyond peer creds is implemented. |
| TA-3: Same-user process reads audit DB | Medium | Medium | MEDIUM | Audit DB will contain target metadata. File permissions must be strict. |
| TA-2: Compromised proc-macro crate | Low | Critical | MEDIUM | Many proc-macro crates in the dependency tree execute at build time with full access. |
| TA-4: MitM during mobile pairing | Low | High | MEDIUM | Pairing is a one-time event, but a successful MitM compromises the entire approval channel. |
| TA-5: Error message leaks secret | Medium | High | HIGH | Current error handling passes upstream error strings directly to clients. |
| TA-6: Insider backdoor | Low | Critical | MEDIUM | Mitigated by code review practices and small codebase. |
2. Code-Level Findings (Current Implementation)¶
Finding 2.1: CRITICAL -- Generic approval.prompt RPC Exposed Without Authorization¶
File: ../crates/opaqued/src/main.rs, lines 174-188
File: ../crates/opaque/src/main.rs, lines 47-53
Description:
The approval.prompt method is directly callable by any process that can connect to the UDS socket. There is no client identity verification, no policy check, and no operation binding. The reason string is entirely client-controlled and displayed directly to the user in the OS authentication dialog.
Code:
// opaqued/src/main.rs:174-188
"approval.prompt" => {
let reason = req
.params
.get("reason")
.and_then(|v| v.as_str())
.unwrap_or("");
let Ok(_permit) = state.approval_gate.acquire().await else {
return Response::err(Some(req.id), "internal", "approval gate closed");
};
match approval::prompt(reason).await {
Ok(approved) => Response::ok(req.id, serde_json::json!({ "approved": approved })),
Err(e) => Response::err(Some(req.id), "approval_failed", e.to_string()),
}
}
Attack Scenario:
1. A malicious LLM agent (TA-1) or same-user process (TA-3) connects to the UDS socket.
2. It sends: {"id":1,"method":"approval.prompt","params":{"reason":"Install critical security update - approve now"}}.
3. The macOS Touch ID or Linux polkit dialog appears with attacker-controlled text.
4. The user, seeing what appears to be a system prompt, approves.
5. The attacker receives {"approved": true} but this approval is not bound to any operation -- it is a free-floating "yes" that currently does nothing, but could be used to condition the user into approving future operation-bound prompts.
Proof of Concept:
# From any process running as the same user:
echo '{"id":1,"method":"approval.prompt","params":{"reason":"URGENT: Approve to prevent data loss"}}' | \
socat - UNIX-CONNECT:$HOME/.opaque/run/opaqued.sock
Impact: Social engineering via approval prompt spoofing. Approval fatigue attack (repeated prompts until user blindly approves).
Recommended Fix:
- Remove the generic approval.prompt RPC endpoint entirely for external clients (as specified in PRD US-001/FR-4).
- Approvals should only be triggered internally by the daemon during operation execution, never by client request.
- If a debug mode is needed, gate it behind a compile-time feature flag or a separate debug socket.
Finding 2.2: CRITICAL -- No Client Identity Verification or Authorization¶
File: ../crates/opaqued/src/main.rs, lines 122-168
Description:
The handle_conn function retrieves peer credentials (uid, gid, pid) but only logs them. No authorization decision is made. Every connected client has full access to all RPC methods.
Code:
// opaqued/src/main.rs:122-134
async fn handle_conn(state: Arc<AppState>, stream: UnixStream) -> std::io::Result<()> {
let fd = stream.as_raw_fd();
let peer = peer_info_from_fd(fd).ok();
if let Some(peer) = peer {
info!(
"client connected uid={} gid={} pid={:?}",
peer.uid, peer.gid, peer.pid
);
} else {
info!("client connected (peer creds unavailable)");
}
// ... proceeds to handle requests with no authorization check
Attack Scenario:
A malicious npm postinstall script (TA-3) running under the same UID connects to the socket and invokes approval.prompt or any future operation RPC. There is no allowlist, no executable hash verification, no policy evaluation.
Recommended Fix:
- Implement FR-2: Compute client identity from UDS peer creds + /proc/<pid>/exe readlink (Linux) or proc_pidpath (macOS) + SHA-256 of the executable.
- Implement FR-3: Evaluate every request against a policy allowlist keyed on (client identity, operation, target).
- Reject requests from unrecognized clients by default (deny-all policy).
Finding 2.3: HIGH -- Approval Error Leaks LocalAuthentication Details¶
File: ../crates/opaqued/src/approval.rs, lines 59-63
Description:
When LocalAuthentication is unavailable, the error includes the full localizedDescription() from NSError, which may contain system-internal details about the authentication configuration.
Code:
// approval.rs:59-63
if let Err(e) = unsafe { ctx.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthentication) } {
return Err(ApprovalError::Failed(format!(
"LocalAuthentication unavailable: {}",
e.localizedDescription()
)));
}
This error propagates to the client via:
Attack Scenario:
An LLM agent probes the approval.prompt endpoint. If LocalAuthentication is not available (daemon running headless, no Secure Enclave, etc.), the error response contains internal system details that reveal the security configuration of the machine.
Recommended Fix:
- Return a fixed, stable error message to the client: "approval_unavailable".
- Log the full NSError details locally at debug level only.
- Apply this pattern to all error paths per FR-8.
Finding 2.4: HIGH -- Client-Controlled Reason String Displayed in OS Auth Dialog¶
File: ../crates/opaqued/src/approval.rs, lines 68-85
File: ../crates/opaqued/src/approval.rs, lines 120-122
Description:
The reason parameter from the client is passed directly to:
- macOS: evaluatePolicy_localizedReason_reply (line 80-85) -- displayed in the Touch ID dialog.
- Linux: polkit details HashMap (line 121-122) -- may be shown in the auth agent dialog.
There is no sanitization, length limit, or content validation beyond trim() and empty check.
Attack Scenario:
An attacker sends a reason string designed to deceive the user:
- "System update requires authentication" (impersonating the OS)
- A very long string that causes UI overflow or hides the true source
- Unicode control characters or RTL override characters that alter the visual presentation
- Newlines that push important context off-screen
Recommended Fix:
- In the target architecture, the reason string should be derived server-side from the OperationRequest (operation + target + client identity), never from client-supplied text.
- As an interim fix: limit length to 200 characters, strip control characters and RTL overrides, prefix with "Opaque: " to distinguish from system prompts.
Finding 2.5: HIGH -- Unsafe Code Review in peer.rs¶
File: ../crates/opaque-core/src/peer.rs, lines 20-28 and 77-88
Description:
There are three unsafe blocks in peer.rs. All involve FFI calls to libc functions (getsockopt, getpeereid).
Block 1 (Linux, lines 20-28):
let rc = unsafe {
libc::getsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_PEERCRED,
std::ptr::addr_of_mut!(ucred).cast(),
&mut len,
)
};
Analysis: This is correctly structured. The ucred struct is stack-allocated and zero-initialized. The len parameter is correctly set to size_of::<ucred>(). The addr_of_mut! macro is used instead of raw pointer arithmetic, which is the modern recommended pattern. The return code is checked. No memory safety issue found.
Block 2 (macOS, line 43):
Analysis: Straightforward FFI call with stack-allocated output parameters. Return code is checked. No memory safety issue found.
Block 3 (macOS, lines 77-85):
let rc = unsafe {
libc::getsockopt(
fd,
SOL_LOCAL,
LOCAL_PEERPID,
std::ptr::addr_of_mut!(pid).cast(),
&mut len,
)
};
Analysis: Same pattern as Block 1. The hardcoded constants LOCAL_PEERPID = 0x002 and SOL_LOCAL = 0 are documented as stable on macOS. However, there is a concern:
Concern: The RawFd parameter is never validated. If the caller passes an invalid or already-closed file descriptor, getsockopt will return an error but the behavior is defined by the OS (returns EBADF). This is not a memory safety issue but could cause confusing error messages. The caller in main.rs line 123-124 uses stream.as_raw_fd() on a live UnixStream, so the fd is valid at point of call.
Recommended Fix:
- No immediate memory safety fix needed. These unsafe blocks are sound.
- Consider adding # Safety documentation comments to each unsafe block explaining the preconditions.
- Consider using the rustix crate (already in the dependency tree via zbus) which provides safe wrappers for getpeereid and getsockopt.
Finding 2.6: HIGH -- Unsafe Code Review in approval.rs¶
File: ../crates/opaqued/src/approval.rs, lines 57-85
Description:
There are three unsafe blocks in approval.rs, all related to Objective-C interop via the objc2 crate.
Block 1 (line 57):
Analysis: LAContext::new() allocates and initializes an Objective-C object. The objc2 crate handles retain/release semantics automatically via Retained<T>. This is the standard pattern. No issue found.
Block 2 (line 59):
Analysis: This is a method call that takes an NSError** out-parameter internally. The objc2 crate translates this into a Result. No issue found.
Block 3 (lines 79-85):
unsafe {
ctx.evaluatePolicy_localizedReason_reply(
LAPolicy::DeviceOwnerAuthentication,
&reason_ns,
&reply,
);
}
Analysis: This dispatches an asynchronous evaluation. The reply block is an RcBlock that captures a clone of the Arc<Mutex<Option<Sender>>>. The block may be called on an arbitrary dispatch queue.
Concern: The reply closure captures tx2 which is Arc<Mutex<Option<Sender<bool>>>>. If the block is called more than once (which LocalAuthentication should not do, but is not formally guaranteed by Apple's documentation), the second call would find tx.take() returns None and silently drop the result. This is safe but could mask bugs.
Concern: If opaqued is terminated while an approval is pending, the reply block may be called after the rx receiver has been dropped. The tx.send() will return Err and the result is silently dropped via let _ = tx.send(ok). This is safe.
Recommended Fix:
- No immediate memory safety fix needed. These unsafe blocks are sound.
- Add # Safety documentation.
- Consider adding a log warning if tx.take() returns None (indicates the callback was invoked more than once).
Finding 2.7: MEDIUM -- Denial of Service via Approval Semaphore Starvation¶
File: ../crates/opaqued/src/main.rs, lines 63, 181-183
Description:
The approval_gate is a Semaphore::new(1), which correctly ensures only one approval dialog is active at a time. However, the semaphore is held for the entire duration of the approval (up to 120 seconds on macOS). During this time, all other approval requests block.
Code:
// main.rs:63
approval_gate: tokio::sync::Semaphore::new(1),
// main.rs:181-183
let Ok(_permit) = state.approval_gate.acquire().await else {
return Response::err(Some(req.id), "internal", "approval gate closed");
};
Attack Scenario:
A malicious process (TA-3) sends an approval.prompt request. The approval dialog appears and the semaphore is acquired. While the user is deciding (up to 120 seconds), the attacker holds the gate. All legitimate operations that require approval are blocked. The attacker can repeat this indefinitely.
Even without a malicious actor, a legitimate but slow approval (user steps away) blocks all other operations.
Recommended Fix:
- Add a per-client request timeout shorter than the approval timeout (e.g., if the requester disconnects, cancel the approval and release the semaphore).
- Implement a queue with a maximum depth (e.g., 3 pending approvals). Reject additional requests with a "approval_busy" error code.
- When client identity is implemented, rate-limit approval requests per client identity.
Finding 2.8: MEDIUM -- Unbounded Connection Spawning¶
File: ../crates/opaqued/src/main.rs, lines 81-89
Description: Every incoming connection spawns a new Tokio task with no limit on concurrent connections.
Code:
// main.rs:81-89
res = listener.accept() => {
let (stream, _addr) = res?;
let state = state.clone();
tokio::spawn(async move {
if let Err(e) = handle_conn(state, stream).await {
warn!("connection error: {e}");
}
});
}
Attack Scenario:
A malicious process opens thousands of connections to the UDS socket. Each connection spawns a Tokio task and allocates a LengthDelimitedCodec with a 1MB max frame buffer. This can exhaust memory (thousands of tasks x codec buffer allocations) or file descriptors.
Recommended Fix:
- Add a connection semaphore limiting concurrent connections (e.g., Semaphore::new(64)).
- Add per-client-IP (per-PID where available) connection limits.
- Set a connection idle timeout (e.g., drop connections that haven't sent a request in 30 seconds).
Finding 2.9: MEDIUM -- Connection Error Leaks Frame Parsing Details¶
File: ../crates/opaqued/src/main.rs, lines 141-158
Description:
Frame parsing errors and JSON deserialization errors are sent back to the client with the full e.to_string() error message.
Code:
// main.rs:144
let resp = Response::err(None, "bad_frame", e.to_string());
// main.rs:154
let resp = Response::err(None, "bad_json", e.to_string());
Attack Scenario:
A malicious client sends malformed data to probe the internal codec implementation. The error messages reveal:
- tokio-util LengthDelimitedCodec version-specific error strings
- serde_json deserialization details (expected types, byte positions)
This information assists in fingerprinting the server implementation.
Recommended Fix:
- Return fixed error messages: "invalid frame" and "invalid request".
- Log the detailed errors server-side at debug level.
Finding 2.10: MEDIUM -- Peer Credentials Not Verified Against UID¶
File: ../crates/opaqued/src/main.rs, lines 122-134
Description: The daemon retrieves peer credentials but does not verify that the connecting process's UID matches the daemon's UID. While socket permissions (0600) should prevent cross-user connections, the daemon should defensively verify this.
Code:
Attack Scenario: If socket permissions are misconfigured (e.g., the directory is world-readable due to a race condition), a process from a different UID could connect. The daemon would log the foreign UID but proceed to handle requests normally.
Recommended Fix:
- After obtaining peer credentials, verify peer.uid == current_uid. Reject connections from different UIDs.
- If peer credentials are unavailable (the .ok() path), reject the connection rather than proceeding.
Finding 2.11: LOW -- whoami Endpoint Not Implemented¶
File: ../crates/opaqued/src/main.rs, lines 190-193
Description:
The whoami endpoint returns {"note": "not implemented"}. When implemented, it should return the server-observed client identity. If the implementation returns too much detail, it could be used for fingerprinting.
Code:
Recommended Fix: - When implemented, return only the information the policy allows the client to see about itself. - Do not return the executable hash or codesign info to the client -- that information is for the daemon's policy engine, not for the client.
Finding 2.12: LOW -- Request ID Type Is Not Cryptographically Random¶
File: ../crates/opaque-core/src/proto.rs, line 5
Description:
The Request.id field is a u64, and the client CLI hardcodes it to 1. When operation requests are implemented, the request_id should be a cryptographically random UUID to prevent prediction and replay.
Recommended Fix:
- Use UUID v4 (or v7 for time-ordered) for request identifiers.
- The daemon should generate the canonical request_id for audit, not trust a client-supplied one.
Finding 2.13: LOW -- No Rate Limiting on Any Endpoint¶
File: ../crates/opaqued/src/main.rs, lines 170-196
Description:
There is no rate limiting on RPC calls. A malicious client can send thousands of ping or version requests per second, or spam approval.prompt requests.
Recommended Fix: - Implement per-connection rate limiting (e.g., token bucket: 10 requests/second burst, 2 requests/second sustained). - Implement per-method rate limiting for sensitive endpoints (approval: 1 per 5 seconds).
3. Protocol Security¶
3.1 JSON Deserialization¶
File: ../crates/opaqued/src/main.rs, line 151
File: ../crates/opaque-core/src/proto.rs, lines 1-9
Analysis:
The protocol uses serde_json::from_slice to deserialize incoming frames into the Request struct. The params field is serde_json::Value, which means arbitrary JSON is accepted.
Risk: JSON Injection
- The params field accepts any JSON value, including deeply nested objects. serde_json has a default recursion limit (serde_json default is 128 levels), which mitigates stack overflow attacks.
- The method field is a String with no validation beyond the match in handle_request. Unknown methods correctly return an error.
- The id field is u64, which is not susceptible to injection.
Risk: Deserialization of Untrusted Data
- serde_json is well-audited and widely used. No known deserialization vulnerabilities in the current version (1.0.149).
- The params: serde_json::Value type means the daemon will allocate memory proportional to the input. Combined with the 1MB frame limit, a single request can allocate up to ~1MB of heap memory for the parsed JSON tree.
Recommendation:
- Add a depth limit for params parsing if serde_json's default is insufficient for your threat model.
- Consider defining typed param structs per method instead of accepting serde_json::Value.
3.2 Frame Smuggling via Length-Delimited Codec¶
File: ../crates/opaqued/src/main.rs, lines 135-137
File: ../crates/opaque/src/main.rs, lines 78-80
Analysis:
Both client and server use LengthDelimitedCodec with a 1MB max frame length. This codec prepends a 4-byte big-endian length header to each frame.
Risk: Frame Boundary Manipulation - The codec handles frame boundaries correctly. A malicious client cannot inject data into another client's connection because each connection has its own codec instance and TCP-like stream ordering is guaranteed by the kernel for UDS. - Since each client gets its own connection, there is no risk of cross-client frame smuggling.
Risk: Large Frame Allocation
- A client can send a frame header claiming a frame of exactly 1,048,576 bytes. The codec will attempt to allocate this buffer before receiving the full frame data. This is bounded by the max_frame_length setting.
- With many concurrent connections (see Finding 2.8), this becomes a memory exhaustion vector: 1000 connections x 1MB = 1GB.
Recommendation:
- Reduce max_frame_length to 64KB or 128KB unless there is a specific need for larger frames. Current RPC payloads are tiny (< 1KB).
- Combine with the connection limit from Finding 2.8.
3.3 Replay Attacks¶
Analysis: The current protocol has no authentication, no nonces, and no request signing. Any request can be replayed by a process that observed it.
However, since the transport is a UDS (not network), replay requires the attacker to have already connected to the socket, which requires same-UID access. If the attacker has same-UID access, they can craft new requests anyway -- replay adds no additional capability.
For the mobile pairing channel (future): Replay attacks are a real concern. The challenge construction (H(server_id || request_id || sha256(request_summary_json) || expires_at)) includes a request_id (random) and expires_at (TTL), which prevents replay if implemented correctly.
Recommendation:
- For UDS: replay protection is not needed beyond same-UID access control.
- For mobile pairing: ensure request_id values are never reused and that the daemon rejects signatures on expired challenges.
3.4 Resource Exhaustion Summary¶
| Vector | Limit | Risk |
|---|---|---|
| Frame size | 1MB | Medium -- should be reduced |
| Concurrent connections | Unbounded | High -- needs a limit |
| Requests per connection | Unbounded | Medium -- needs rate limiting |
| Pending approvals | 1 (semaphore) | Medium -- blocks all other approvals |
| JSON depth | 128 (serde default) | Low |
3.5 Lack of Request Authentication/Signing¶
Analysis: Requests are not signed or authenticated. The daemon relies entirely on socket-level access control (file permissions + peer credentials). This is a reasonable model for a single-user local daemon, but it means: - Any process that can connect to the socket is fully trusted. - There is no way to distinguish between different clients once connected. - Approval decisions cannot be cryptographically bound to the requesting client.
Recommendation: - Implement client identity as described in the PRD (peer creds + executable hash + optional codesign). - Bind approval decisions to the verified client identity. - Consider a per-session challenge-response if you want to prevent pid-reuse attacks (a short-lived process could connect, disconnect, and another process could reuse the PID).
4. Approval Flow Security¶
4.1 macOS LocalAuthentication¶
4.1.1 Prompt Spoofing¶
File: ../crates/opaqued/src/approval.rs, lines 68-85
Analysis:
The localizedReason string is the only context the user sees in the Touch ID dialog. macOS displays this as: "opaqued" is trying to [reason]. The application name comes from the process name, not the reason string.
Risk: A malicious client can set the reason to anything (see Finding 2.4). The user sees "opaqued" is trying to "Install critical security update" and may approve without understanding what operation is actually being authorized.
Mitigation (current): None. The reason is fully client-controlled.
Mitigation (target): The daemon should construct the reason string from the verified OperationRequest, never from client input. Example: "Allow Claude Code to set GitHub secret JWT for org/repo".
4.1.2 Daemon Losing UI Session¶
Analysis:
If opaqued is started as a LaunchDaemon (runs as root, no GUI session) instead of a LaunchAgent (runs in user session), LocalAuthentication will fail because there is no UI session to display the dialog.
The code at line 59 calls canEvaluatePolicy_error which should detect this and return an error. However, there are edge cases:
- If the daemon starts with a UI session but the user locks the screen, the behavior of evaluatePolicy is unclear (Apple documentation does not specify).
- If the daemon is started via SSH, there is no UI session.
Mitigation (current): The canEvaluatePolicy_error preflight check provides some protection.
Recommendation:
- Document the supported deployment model (LaunchAgent only, never LaunchDaemon).
- Test behavior when the screen is locked.
- Consider detecting IOServiceGetMatchingService(kIOMainPortDefault, ...) for display sleep state.
Status (PARTIALLY RESOLVED): The deployment model (LaunchAgent only, LimitLoadToSessionType: Aqua) is now documented in Deployment. The LaunchAgent plist prevents loading in non-GUI sessions. Session detection at daemon startup (calling canEvaluatePolicy as a preflight and refusing to start on failure) is specified but not yet implemented. Screen-lock behavior and Fast User Switching remain to be tested.
4.1.3 Process Interaction with Prompt¶
Analysis: The macOS LocalAuthentication dialog is system-owned and runs in the WindowServer's trust domain. A malicious process cannot: - Programmatically dismiss the dialog - Click the "Allow" button - Inject events into the dialog
However, a malicious process can: - Create a fake overlay window that looks like the Touch ID prompt (UI spoofing) - Use accessibility APIs (if granted) to interact with the dialog
Recommendation:
- Include the process name and a unique operation ID in the reason string so the user can verify authenticity.
- Consider using LAPolicyDeviceOwnerAuthenticationWithBiometrics instead of LAPolicyDeviceOwnerAuthentication to require biometrics (prevents password fallback, which is more susceptible to shoulder surfing).
4.1.4 120-Second Timeout as DoS Vector¶
File: ../crates/opaqued/src/approval.rs, line 88
Analysis:
The 120-second timeout on rx.recv_timeout(Duration::from_secs(120)) means the approval gate semaphore is held for up to 2 minutes per approval request.
Risk: Combined with Finding 2.7, an attacker can block all operations for 2 minutes at a time by triggering an approval that the user ignores.
Recommendation:
- Reduce the timeout to 60 seconds.
- Add a mechanism for the user to cancel the approval from the daemon side (e.g., via the CLI: opaque cancel).
- Release the semaphore when the requesting connection is dropped.
4.2 Linux polkit¶
4.2.1 auth_self Policy Analysis¶
File: ../assets/linux/polkit/com.opaque.approve.policy, lines 9-13
Policy:
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
Analysis:
- allow_any=no: Correctly denies requests from non-local sessions.
- allow_inactive=no: Correctly denies requests from inactive sessions (e.g., SSH).
- allow_active=auth_self: Requires the user to authenticate with their own password.
Risk Assessment:
auth_self is the correct choice for this use case. It requires proof-of-life without requiring root privileges. However:
- Some polkit auth agents (particularly CLI-based pkttyagent) do not show the details HashMap, so the user cannot see what they are approving.
- GNOME's polkit agent shows the message field but not individual details entries.
- KDE's polkit agent shows details in some versions.
This means on many Linux setups, the user sees: "Authentication is required to approve an Opaque operation." with no indication of which operation or target.
Recommendation:
- Test specific auth agents (gnome-shell, kde, mate, pkttyagent) and document which ones display intent.
- As specified in the PRD (US-006/FR-6): if the environment cannot display intent, fail closed with approval_unavailable.
- Consider implementing a dedicated Opaque approval UI helper for Linux that shows full operation details, using polkit only for the authentication step.
Status (RESOLVED): A two-step approval flow has been implemented in approval.rs. Step 1 shows an intent dialog via zenity --question or kdialog --yesno with full operation details. Step 2 performs the polkit authentication. This separates intent visibility (our code, always works) from authentication (polkit, always requires password). If no intent dialog UI is available (no zenity, no kdialog, no TTY), approval fails closed. Supported desktop tiers are documented in Deployment.
4.2.2 Action ID Hijacking¶
Analysis:
The polkit action ID com.opaque.approve is a reverse-DNS identifier. A malicious actor would need root access to install a competing policy file at /usr/share/polkit-1/actions/com.opaque.approve.policy. If they have root, they can bypass polkit entirely.
Risk: Low. Polkit action IDs cannot be hijacked without root access.
Recommendation:
- Verify the policy file integrity at daemon startup (hash check).
- Consider namespacing operations into separate action IDs (e.g., com.opaque.approve.github, com.opaque.approve.k8s) for more granular policy in the future.
4.3 Mobile (iOS) Pairing¶
4.3.1 QR Pairing Crypto Protocol Weaknesses¶
File: mobile-approvals.md, Section 2
Analysis:
The pairing protocol described in the documentation includes:
- server_pubkey: Daemon's public key for identity pinning
- pairing_code: One-time high-entropy secret with 5-minute TTL
- endpoint: HTTPS URL with pinned cert fingerprint
Weakness 1: QR Code Shoulder Surfing
The QR code contains the server_pubkey and pairing_code in plaintext. If an attacker photographs the QR code within the 5-minute TTL, they can pair their own device.
Weakness 2: No Key Confirmation
The protocol does not include a key confirmation step. After pairing, neither side verifies that the channel is not being MitM'd. The daemon stores the device's public key on receipt of the pairing_code, but there is no mutual authentication beyond the one-time code.
Weakness 3: Pairing Code Entropy The documentation says "high-entropy" but does not specify the length or character set. If too short, it is brute-forceable within the TTL window.
Recommendation:
- Require a key confirmation step: after pairing, display a short verification code (derived from both public keys) on both devices for the user to compare (similar to Signal safety numbers).
- Specify minimum pairing code entropy: at least 128 bits (e.g., 32 hex characters or 22 base64 characters).
- Rate-limit pairing attempts to prevent brute-force (max 5 attempts per pairing session).
- Add device listing: opaque devices list should show paired devices with their last-seen timestamp.
4.3.2 MitM Prevention During Pairing¶
Analysis: The QR code includes either a pinned self-signed cert fingerprint or the server's public key. This provides a trust anchor for the phone-to-daemon connection.
Risk: If the attacker can intercept the QR code AND sit on the network path between phone and daemon, they can present their own certificate. However, the phone has the server's pinned cert/pubkey from the QR, so the TLS connection would fail unless the attacker also obtained the QR contents.
Risk: If the local network uses mDNS for discovery (v1), an attacker could respond to mDNS queries first and present a rogue endpoint. The cert pinning from the QR mitigates this.
Recommendation: - Always pin the server certificate using the pubkey from the QR code. - Use certificate-based mutual TLS after pairing (the device presents its Secure Enclave-backed certificate). - Never fall back to unpinned TLS.
4.3.3 Challenge Construction Security¶
Documentation states:
Analysis:
- Including server_id prevents cross-server replay.
- Including request_id (random) prevents same-server replay.
- Including sha256(request_summary_json) binds the approval to the exact operation intent.
- Including expires_at prevents approval after expiry.
Weakness: The concatenation || operator is ambiguous. If the fields are simply concatenated as strings without a delimiter, an attacker could shift bytes between fields (concatenation collision). For example, server_id="ab" || request_id="cd" equals server_id="abc" || request_id="d".
Recommendation:
- Use a structured encoding before hashing (e.g., canonical_json({server_id, request_id, summary_hash, expires_at})) or use length-prefixed encoding.
- Alternatively, use HMAC-SHA256 with the device's shared secret as the key, and include the structured fields as the message.
- Specify the hash algorithm explicitly (SHA-256 recommended).
5. Filesystem Security¶
5.1 Socket Path Permissions¶
File: ../crates/opaque-core/src/socket.rs, lines 24-39
File: ../crates/opaqued/src/main.rs, lines 96-104
Analysis:
The socket creation follows these steps:
1. ensure_socket_parent_dir creates the directory with 0o700 permissions (line 35).
2. UnixListener::bind creates the socket file (line 55).
3. lock_down_socket_path sets socket permissions to 0o600 (line 101).
Risk: Race Condition Between Steps 2 and 3
Between bind() and set_permissions(), there is a window where the socket may have default permissions (typically 0o755 minus umask). If the attacker can connect during this window, they gain access.
Recommended Fix:
- Set the process umask to 0o077 before calling bind():
let old_umask = unsafe { libc::umask(0o077) };
let listener = UnixListener::bind(&socket)?;
unsafe { libc::umask(old_umask) };
5.2 Directory Permission Race Conditions¶
File: ../crates/opaque-core/src/socket.rs, lines 24-39
Analysis:
create_dir_all followed by set_permissions has a TOCTOU race: another process could create the directory (or a symlink) between the check and the permission set.
Risk: If $XDG_RUNTIME_DIR/opaque/ does not exist, create_dir_all creates it. But $XDG_RUNTIME_DIR itself is typically user-owned (created by pam_systemd with correct permissions). The risk is low in practice.
Higher risk path: ~/.opaque/run/ under the HOME fallback. If ~/.opaque/ already exists and is a symlink to an attacker-controlled location, create_dir_all will follow the symlink and create run/ in the attacker's directory.
Recommended Fix:
- After create_dir_all, verify that the resulting path is owned by the current user and is not a symlink:
let meta = std::fs::symlink_metadata(parent)?;
if meta.file_type().is_symlink() {
return Err(io::Error::new(io::ErrorKind::Other, "socket directory is a symlink"));
}
if meta.uid() != current_uid {
return Err(io::Error::new(io::ErrorKind::Other, "socket directory not owned by current user"));
}
5.3 Stale Socket File Handling (TOCTOU)¶
File: ../crates/opaqued/src/main.rs, lines 40-53
Code:
if socket.exists() {
match UnixStream::connect(&socket).await {
Ok(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::AddrInUse,
format!("socket already in use: {}", socket.display()),
));
}
Err(_) => {
// Stale socket file.
tokio::fs::remove_file(&socket).await?;
}
}
}
Analysis:
This has a TOCTOU race: between the exists() check (or the failed connect()) and the remove_file(), another instance of opaqued could start, create its socket, and then this instance removes the other's socket.
Additionally, between remove_file() and bind(), another process could create a file (or symlink) at the socket path.
Risk: Low in practice (daemon startup is infrequent), but could cause confusing behavior with systemd socket activation or rapid restart scenarios.
Recommended Fix:
- Use flock() or a PID file with advisory locking to ensure single-instance operation:
let lockfile = socket.with_extension("lock");
let lock = std::fs::File::create(&lockfile)?;
if flock(lock.as_raw_fd(), FlockArg::LockExclusiveNonblock).is_err() {
return Err("another instance is running");
}
5.4 Symlink Attacks on Socket Path¶
File: ../crates/opaque-core/src/socket.rs, lines 6-22
Analysis:
The OPAQUE_SOCK environment variable allows the user to specify an arbitrary socket path. If the daemon runs as the user, and the path points to a symlink, UnixListener::bind will follow the symlink.
Attack Scenario (TA-3):
1. Attacker creates a symlink: ln -s /tmp/shared_socket ~/.opaque/run/opaqued.sock
2. When the daemon starts, it binds to /tmp/shared_socket (which may be world-accessible).
3. The attacker can now connect to the socket from any user.
Recommended Fix: - Before binding, check that the socket path is not a symlink:
if socket.symlink_metadata()?.file_type().is_symlink() {
return Err("socket path is a symlink, refusing to bind");
}
6. Supply Chain¶
6.1 Dependency Overview¶
The project has 3 crates and the following key dependency categories:
| Category | Crates | Risk Level |
|---|---|---|
| Async runtime | tokio, tokio-util, futures-util, mio | Low -- widely audited, maintained by Tokio team |
| Serialization | serde, serde_json, serde_derive | Low -- ubiquitous, heavily audited |
| CLI | clap, clap_derive | Low |
| macOS Obj-C interop | objc2, objc2-foundation, objc2-local-authentication, block2, dispatch2 | Medium -- FFI boundary, less widely audited than core Rust ecosystem |
| Linux D-Bus/polkit | zbus, zbus_polkit, zvariant, zbus_macros | Medium -- complex protocol implementation, IPC boundary |
| Proc macros | syn, proc-macro2, quote, serde_derive, clap_derive, async-trait, async-recursion, zbus_macros, zvariant_derive | Medium -- execute at build time with full access |
| WASM | wasm-bindgen, wit-bindgen, wasmparser (transitive via zbus->uuid) | Low -- not used at runtime on macOS/Linux |
| Logging | tracing, tracing-subscriber | Low |
6.2 High-Risk Dependencies¶
objc2 Ecosystem (macOS)¶
Crates: objc2 0.6.3, objc2-foundation 0.3.2, objc2-local-authentication 0.3.2, block2 0.6.2
Risk: These crates provide Rust bindings to Objective-C frameworks via FFI. They are inherently unsafe at the boundary. A supply chain compromise of these crates could:
- Bypass LocalAuthentication entirely (always return true)
- Exfiltrate secrets from process memory
- Execute arbitrary code at the Obj-C runtime level
Mitigation:
- Pin exact versions in Cargo.lock (already done).
- Monitor for security advisories via cargo audit.
- Consider vendoring these crates for production builds.
- The maintainer (madsmtm) is a known, active contributor in the Rust community.
zbus Ecosystem (Linux)¶
Crates: zbus 5.13.2, zbus_polkit 5.0.0, zvariant 5.9.2
Risk: zbus implements the D-Bus wire protocol and connects to the system bus. A vulnerability in zvariant deserialization could be triggered by a malicious D-Bus message. zbus_polkit trusts the polkit daemon's responses; if the D-Bus session is compromised, polkit responses could be forged.
Mitigation:
- Pin exact versions.
- Monitor zbus security advisories.
- The zbus project is maintained by the GNOME/freedesktop community.
Proc-Macro Crates¶
Crates: syn, proc-macro2, quote, serde_derive, clap_derive, zbus_macros, zvariant_derive, async-trait, async-recursion, enumflags2_derive, thiserror-impl, tokio-macros
Risk: These crates execute arbitrary Rust code at build time during cargo build. A compromised proc-macro crate could:
- Read environment variables (API keys, CI tokens)
- Write files (backdoor the compiled binary)
- Phone home (exfiltrate build metadata)
Mitigation:
- Use cargo vet or cargo crev to verify trusted publishers.
- Build in a sandboxed environment without network access.
- Audit Cargo.lock changes in PRs.
6.3 Compromised Dependency Impact¶
If a runtime dependency is compromised:
| Dependency | Impact |
|---|---|
serde_json |
Can intercept and exfiltrate all deserialized request data. Can forge responses. |
tokio |
Full control over async runtime. Can intercept all I/O. |
objc2-local-authentication |
Can bypass biometric approval. Can always return approved = true. |
zbus |
Can intercept polkit communication. Can forge authorization responses. |
tracing |
Can exfiltrate all logged data (which currently excludes params, but includes peer info). |
Recommendation:
- Run cargo audit in CI to check for known vulnerabilities.
- Consider cargo supply-chain to analyze maintainer trust chains.
- For production deployments, vendor dependencies and build from a verified source tree.
- Implement reproducible builds to verify binary integrity.
7. Operational Security Guide¶
7.1 Hardening Checklist¶
Filesystem¶
- [ ] Socket directory (
$XDG_RUNTIME_DIR/opaque/or~/.opaque/run/) has permissions0700, owned by the daemon user. - [ ] Socket file has permissions
0600. - [ ] No symlinks exist in the socket path chain.
- [ ] SQLite database directory (when implemented) has permissions
0700. - [ ] TOML policy files have permissions
0600and are owned by the daemon user. - [ ] Verify
$XDG_RUNTIME_DIRis mounted astmpfs(Linux) -- prevents socket persistence across reboots. - [ ] On macOS, verify
~/.opaque/is excluded from Time Machine and Spotlight indexing.
Process Isolation¶
- [ ]
opaquedruns as a LaunchAgent (macOS) or systemd user service (Linux), never as root. - [ ] On Linux, create a systemd unit with:
- [ ] On macOS, if using a signed app bundle, the binary should have hardened runtime enabled.
- [ ] Disable core dumps for the daemon process (
ulimit -c 0orprctl(PR_SET_DUMPABLE, 0)on Linux). - [ ] On Linux, set
kernel.yama.ptrace_scope=1(or higher) to prevent ptrace from other same-UID processes.
Network¶
- [ ] The daemon's UDS socket must never be exposed over TCP or any network transport.
- [ ] When the mobile pairing HTTPS server is active (future), bind it to the LAN interface only, never
0.0.0.0. - [ ] Use a firewall to restrict outbound connections from
opaquedto only the required provider endpoints (1Password, Vault, GitHub API, GitLab API, AWS endpoints).
Secrets Management¶
- [ ] Provider credentials (PATs, Vault tokens) must be stored in the OS keychain, never in plaintext config files.
- [ ] Rotate provider credentials regularly (quarterly at minimum).
- [ ] Use short-lived credentials where possible (Vault dynamic secrets, AWS STS, GitHub App installation tokens).
- [ ] Never store plaintext secret values in the SQLite database.
Logging¶
- [ ] Set
RUST_LOG=infofor production (neverdebugortracein production -- these may log sensitive details). - [ ] Rotate log files. Do not let logs grow unbounded.
- [ ] Ensure log files have permissions
0600. - [ ] Never log request params (the current code already avoids this -- see
main.rs:161).
7.2 Monitoring Recommendations¶
Daemon Health¶
- Monitor daemon process uptime (systemd
is-activeor launchctl print). - Alert if the daemon crashes and restarts more than 3 times in 5 minutes (indicates a potential DoS or exploit attempt).
- Monitor socket file existence and permissions (alert if permissions change from
0600).
Security Events (When Audit Log Is Implemented)¶
- Alert on: More than 5 approval denials in 10 minutes (possible approval fatigue attack).
- Alert on: Approval requests from unrecognized client identities.
- Alert on: Operations targeting production resources outside business hours.
- Alert on: Rapid succession of approval requests (more than 3 per minute).
- Alert on: Failed provider authentication (credentials may be expired or stolen).
Resource Usage¶
- Monitor file descriptor count for
opaqued(alert if > 100). - Monitor memory usage (alert if > 256MB -- indicates possible memory exhaustion attack or leak).
- Monitor CPU usage (alert if sustained > 50% -- possible DoS).
7.3 Incident Response Procedures¶
Suspected Compromise of Opaque Daemon¶
- Contain: Kill the
opaquedprocess immediately:kill -9 $(pgrep opaqued). - Preserve: Copy the audit log (SQLite DB) and log files to a secure location before they are modified.
- Revoke: Revoke all provider credentials (PATs, Vault tokens, AWS keys) that were configured in Opaque.
- Rotate: Rotate all secrets that were managed through Opaque operations (GitHub secrets, GitLab CI variables, Kubernetes secrets).
- Investigate: Review the audit log for unauthorized operations. Check for operations against unexpected targets or from unrecognized client identities.
- Rebuild: Reinstall Opaque from a verified source. Do not reuse the old binary or configuration.
- Re-pair: If iOS device pairing was in use, revoke all paired devices and re-pair.
Suspected Approval Prompt Spoofing / Social Engineering¶
- Do not approve any pending prompts.
- Check
opaque devices list(when implemented) for unauthorized paired devices. - Review the audit log for any operations that were approved during the suspicious time window.
- Revoke approval leases if any are active.
- Investigate which process triggered the suspicious approval (check daemon logs for client PID/exe).
Provider Credential Leak¶
- Revoke the leaked credential immediately at the provider (GitHub, GitLab, AWS, Vault, 1Password).
- Audit the provider's access logs for unauthorized usage during the exposure window.
- Rotate any downstream secrets that were accessible via the leaked credential.
- Update the Opaque configuration with the new credential.
7.4 Backup and Recovery for Audit Data¶
What to Back Up¶
| Data | Location | Frequency | Retention |
|---|---|---|---|
| SQLite audit DB | ~/.opaque/data/audit.db (planned) |
Daily incremental | 1 year minimum |
| Parquet exports | ~/.opaque/data/parquet/ (planned) |
On creation | 2+ years |
| Policy files | ~/.opaque/policy.toml (planned) |
On change | Indefinite (version control recommended) |
| Paired device keys | In SQLite DB | With audit DB | Until device is revoked |
Backup Procedures¶
- SQLite: Use
.backupcommand orsqlite3 audit.db ".backup /path/to/backup.db"to create a consistent backup. Do not copy the file while the daemon is running (WAL mode can leave the copy inconsistent). - Policy files: Store in version control (git). Review diffs before applying changes.
- Encrypt backups: Use
ageorgpgto encrypt backup files before storing them off-machine. - Test recovery: Periodically restore from backup to verify integrity.
Recovery¶
- Stop
opaqued. - Replace the SQLite DB with the backup copy.
- Verify integrity:
sqlite3 audit.db "PRAGMA integrity_check". - Restart
opaqued. - Note: Approval leases are intentionally not persisted (fail-closed). After recovery, users will need to re-approve operations.
8. Security Roadmap¶
8.1 Critical -- Do Before Any Deployment¶
| ID | Finding | Action | Effort |
|---|---|---|---|
| C-1 | Finding 2.1 | ~~Remove approval.prompt as a client-callable RPC.~~ DONE: Removed in hardening pass. Approvals are triggered internally by Enclave::handle_approval() only. |
Small |
| C-2 | Finding 2.2 | ~~Implement client identity verification (peer creds + exe path + hash).~~ DONE: ClientIdentity with uid/gid/pid/exe_path/exe_sha256 implemented. Policy engine evaluates against client identity. |
Medium |
| C-3 | Finding 2.4 | ~~Never pass client-supplied strings to OS approval dialogs.~~ DONE: Approval description is constructed by the enclave from verified OperationRequest fields, never from client-supplied reason text. |
Small |
| C-4 | Finding 2.3 | ~~Sanitize all error messages returned to clients.~~ DONE: Error messages scrubbed in hardening pass. bad_frame -> "malformed frame", bad_json -> "invalid JSON request", workspace errors -> generic message. Details logged server-side only. |
Small |
| C-5 | Finding 2.10 | ~~Verify peer UID matches daemon UID.~~ DONE: verify_peer_uid() implemented. Connections from different UIDs or with unavailable peer creds are silently rejected. |
Small |
| C-6 | Section 5.1 | Set umask to 0o077 before bind() to eliminate the socket permission race window. |
Small |
8.2 High Priority -- Do Before v1 Release¶
| ID | Finding | Action | Effort |
|---|---|---|---|
| H-1 | Finding 2.8 | Add a connection semaphore (max 64 concurrent connections). | Small |
| H-2 | Finding 2.7 | Add per-client approval rate limiting. Release semaphore when client disconnects. | Medium |
| H-3 | Section 3.2 | Reduce max_frame_length from 1MB to 128KB. |
Small |
| H-4 | Finding 2.13 | Implement per-connection rate limiting (token bucket). | Medium |
| H-5 | Section 5.2 | Add symlink and ownership checks on socket directory. | Small |
| H-6 | Section 5.3 | Add PID file with advisory locking for single-instance protection. | Small |
| H-7 | Section 4.2.1 | ~~Implement polkit intent visibility detection. Fail closed when the auth agent cannot show operation details.~~ DONE: Two-step approval flow (intent dialog + polkit auth) implemented. Supported desktops documented in Deployment. | Medium |
| H-8 | Section 4.1.2 | ~~Document supported macOS deployment models (LaunchAgent only).~~ PARTIALLY DONE: Documented in Deployment. Session detection at daemon startup not yet implemented. | Medium |
| H-9 | Section 6.3 | Add cargo audit to CI pipeline. Pin all dependency versions. |
Small |
| H-10 | -- | Implement the OperationRequest envelope (PRD US-002) with versioning, binding approvals to specific operations. |
Large |
8.3 Medium Priority -- Address in v1 Lifecycle¶
| ID | Finding | Action | Effort |
|---|---|---|---|
| M-1 | Section 4.3.1 | Implement key confirmation step for mobile pairing. | Medium |
| M-2 | Section 4.3.3 | Use structured encoding (canonical JSON or length-prefixed) for challenge construction. | Small |
| M-3 | Finding 2.12 | Use UUID v4/v7 for request identifiers. Generate canonical IDs server-side. | Small |
| M-4 | Section 5.4 | Add symlink check before binding to socket path (especially when OPAQUE_SOCK is set). |
Small |
| M-5 | Section 3.5 | Implement per-session challenge-response to prevent PID reuse attacks. | Medium |
| M-6 | -- | Implement audit log with redaction levels (human vs. agent feed). | Large |
| M-7 | -- | Implement approval leases with scoped TTL (PRD US-004). | Medium |
| M-8 | -- | Add client executable hash verification on macOS (codesign) and Linux (/proc/pid/exe). | Medium |
| M-9 | Section 7.1 | Implement core dump prevention (prctl(PR_SET_DUMPABLE, 0) on Linux). |
Small |
| M-10 | -- | Implement connection idle timeout (30 seconds without a request). | Small |
8.4 Low Priority -- Track for Future¶
| ID | Finding | Action | Effort |
|---|---|---|---|
| L-1 | Finding 2.11 | Implement whoami with appropriate information disclosure controls. |
Small |
| L-2 | Section 3.1 | Define typed param structs per RPC method instead of serde_json::Value. |
Medium |
| L-3 | Finding 2.5 | Add # Safety documentation to all unsafe blocks. |
Small |
| L-4 | Finding 2.6 | Add logging for unexpected double-invocation of the LA callback. | Small |
| L-5 | Section 4.3.1 | Specify minimum pairing code entropy (128 bits). Rate-limit pairing attempts. | Small |
| L-6 | Section 6.2 | Consider vendoring objc2 and zbus ecosystems for production builds. |
Medium |
| L-7 | -- | Implement reproducible builds for binary verification. | Large |
| L-8 | -- | Add integration tests that verify error messages never contain secret values. | Medium |
| L-9 | -- | Consider rustix safe wrappers for peer credential lookups instead of raw libc FFI. |
Medium |
| L-10 | Section 4.1.3 | Consider requiring biometrics-only policy (LAPolicyDeviceOwnerAuthenticationWithBiometrics) to prevent password fallback. |
Small |
Appendix A: Files Reviewed¶
| File | Lines | Purpose |
|---|---|---|
../crates/opaque-core/src/lib.rs |
6 | Core library root, API version constant |
../crates/opaque-core/src/proto.rs |
46 | JSON-RPC Request/Response types |
../crates/opaque-core/src/socket.rs |
40 | Socket path resolution and directory setup |
../crates/opaque-core/src/peer.rs |
91 | UDS peer credential extraction (unsafe FFI) |
../crates/opaque/src/main.rs |
103 | CLI client |
../crates/opaqued/src/main.rs |
200 | Daemon: listener, connection handler, request dispatch |
../crates/opaqued/src/approval.rs |
137 | macOS LocalAuthentication and Linux polkit approval flows |
../assets/linux/polkit/com.opaque.approve.policy |
16 | polkit policy XML |
../Cargo.toml |
21 | Workspace configuration |
../Cargo.lock |
1489 | Full dependency tree |
architecture.md |
381 | System architecture |
operations.md |
163 | Operation contract definitions |
policy.md |
115 | Policy model |
llm-harness.md |
137 | LLM integration safety model |
mobile-approvals.md |
107 | Mobile pairing and approval protocol |
storage.md |
206 | Storage and data model |
audit-analytics.md |
228 | Audit, live feed, and analytics design |
linux-polkit.md |
23 | Linux polkit setup instructions |
../tasks/prd-secure-approval-and-audit-hardening.md |
165 | PRD for security hardening |
../README.md |
24 | Project overview |
../AGENTS.md |
25 | LLM tool guidance |
Appendix B: Dependency Count Summary¶
- Direct dependencies (across all workspace crates): 21
- Total transitive dependencies (from Cargo.lock): 93
- Proc-macro crates (build-time code execution): 12
- Crates with
unsafecode (estimated):libc,objc2,block2,mio,socket2,tokio,rustix, plus the project's ownpeer.rsandapproval.rs
Appendix C: Severity Definitions¶
| Severity | Definition |
|---|---|
| CRITICAL | Exploitable vulnerability that could lead to secret disclosure, unauthorized operations, or complete bypass of security controls. Must be fixed before any deployment. |
| HIGH | Significant weakness that could be exploited under realistic conditions. Should be fixed before production use. |
| MEDIUM | Vulnerability that requires specific conditions to exploit or has limited impact. Should be addressed in the v1 lifecycle. |
| LOW | Code quality issue, missing defense-in-depth measure, or future risk that does not currently pose an exploitable threat. Track and address opportunistically. |