v0.0.16 - Implement deny-by-default approval model with on-demand nostr_index derivation and --preapprove CLI flag
This commit is contained in:
357
plans/deny_by_default_approvals.md
Normal file
357
plans/deny_by_default_approvals.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# Plan: Deny-by-default approvals + on-demand role derivation
|
||||
|
||||
Status: **implemented**
|
||||
|
||||
This plan replaces several entangled mechanisms (same-uid auto-allow, role pre-registration as a hard rule, global `[a]` flag) with a single, coherent approval model. It also addresses the `n_OS_tr` system-services-at-boot scenario.
|
||||
|
||||
If implemented, this plan supersedes:
|
||||
|
||||
- The same-uid auto-allow rule in [`policy_init_default()`](../src/policy.c:551).
|
||||
- The "role_path / nostr_index must be pre-registered" rule recorded in [`plans/nsigner.md:159`](nsigner.md:159) (the rule is *narrowed*, not removed — see §5).
|
||||
- The global `g_prompt_always_allow` flag in [`src/server.c:469`](../src/server.c:469) as the meaning of prompt-`[a]`. The running-TUI `[a]` hotkey is unaffected (or removed; see §11).
|
||||
|
||||
---
|
||||
|
||||
## 1. Goals
|
||||
|
||||
1. **Deny-by-default.** No client can sign or get a public key without an explicit human approval at the terminal **or** an explicit pre-approval declared at startup.
|
||||
2. **One approval mechanism.** Both the runtime prompt (human approves a request) and the boot-time pre-approval (OS-level config) populate the **same** in-memory policy table. There is exactly one path through the dispatcher that determines whether to sign.
|
||||
3. **On-demand role creation.** When a request specifies an unregistered `nostr_index`, the prompt path may auto-derive a fresh role at that index *only after* the human (or a startup pre-approval) consents. Pre-registration of `role_path` keys remains required.
|
||||
4. **Boot-time ergonomics for `n_OS_tr`.** OS-level system services that need keys at boot can be pre-approved by the OS distribution via CLI flags, with no prompts and no on-disk runtime config files.
|
||||
5. **Auditable approval source.** The activity log distinguishes between approvals granted at runtime by the user, approvals loaded from startup pre-approvals, and approvals already in place from prior session activity.
|
||||
|
||||
## 2. Non-goals
|
||||
|
||||
- This plan does **not** add per-verb scoping at the prompt. `[a]` means "this caller may use this role for all verbs the role's purpose/curve permits." Per-verb scoping is a future iteration.
|
||||
- This plan does **not** add persistent (across-restart) policy storage. Approvals still vanish on process exit.
|
||||
- This plan does **not** introduce capability tokens or any new authentication primitive. Identity remains transport-derived.
|
||||
- This plan does **not** change the wire protocol. Clients see the same JSON-RPC request/response shape.
|
||||
|
||||
## 3. The new model in one paragraph
|
||||
|
||||
The signer maintains an in-memory policy table whose entries are tuples of `(caller_id, role, prompt_mode)`. At startup, the table is empty except for entries declared via `--preapprove` CLI flags (set by `n_OS_tr` system unit files for boot-required services). Every request without a matching policy entry triggers an interactive prompt at the terminal. If the user picks `[a]`, a new policy entry for `(this caller, this role)` is appended to the table and any future request matching that tuple is served silently. If the request specifies an unregistered `nostr_index`, the consent path also derives a fresh role at that index. The `*` catch-all is `PROMPT_DENY` for any caller that hasn't been explicitly added.
|
||||
|
||||
## 4. Concept renaming and positioning
|
||||
|
||||
The user-facing language collapses to three concepts:
|
||||
|
||||
| User-facing term | Internal mapping |
|
||||
|---|---|
|
||||
| **Identity** | `caller_identity_t` (uid, qubes vm name, tcp peer, etc.) |
|
||||
| **Index / Path** | `nostr_index` shorthand or full `role_path` for advanced use |
|
||||
| **Approval** | A policy table entry with `prompt_mode = PROMPT_NEVER` |
|
||||
|
||||
The word **role** is retained internally (the role table, the [`role_entry_t`](../src/role_table.c:103) struct, derivation paths) but de-emphasized in user-facing prompts and docs. A user reading a prompt sees:
|
||||
|
||||
```
|
||||
Approval required
|
||||
caller: qubes:nostr-relay
|
||||
verb: sign_event
|
||||
index: nostr_index=0 (resolves to the default identity)
|
||||
```
|
||||
|
||||
instead of `role: main`. Internally the resolved role is still recorded for audit.
|
||||
|
||||
## 5. The narrowed pre-registration rule
|
||||
|
||||
The original rule was: **all** selectors must resolve to a pre-registered role.
|
||||
|
||||
The new rule is:
|
||||
|
||||
> Pre-registration is required for **`role_path`** selectors. For **`nostr_index`** selectors, an unregistered index is a valid prompt trigger, and the role for that index is derived on the fly **after** explicit consent (prompt or `--preapprove`).
|
||||
|
||||
Rationale:
|
||||
|
||||
- `nostr_index` paths follow a fixed scheme (`m/44'/1237'/<n>'/0/0`) with locked purpose (`nostr`) and curve (`secp256k1`). Auto-derivation cannot accidentally produce a key that mixes purposes. The risk is solely "this is a fresh nostr identity"; the human at the prompt can answer that.
|
||||
- `role_path` is free-form and can address any BIP-32 path, including paths that overlap with bitcoin/SSH/age domains. Auto-derivation here would let a malicious or buggy client coerce the signer into producing keys whose purpose the human can't easily verify. We keep that locked.
|
||||
- This narrowing is the smallest crack we can open in the original rule that still solves the user's `nostr_index: 1` use case.
|
||||
|
||||
[`plans/nsigner.md:159`](nsigner.md:159) ("`role_path` must be pre-registered") remains true. The `nostr_index` clause becomes new and explicit.
|
||||
|
||||
## 6. The four pieces of work
|
||||
|
||||
### Piece A — Deny-by-default policy
|
||||
|
||||
#### A.1 Default policy table
|
||||
|
||||
[`policy_init_default()`](../src/policy.c:551) currently inserts two entries:
|
||||
|
||||
1. `caller = "uid:<owner>"` → `PROMPT_NEVER` (auto-allow same-uid)
|
||||
2. `caller = "*"` → `PROMPT_DENY`
|
||||
|
||||
The new default inserts only:
|
||||
|
||||
1. `caller = "*"` → `PROMPT_PROMPT`
|
||||
|
||||
There is no same-uid special case. Even the user's own client triggers a prompt the first time it connects.
|
||||
|
||||
If `--preapprove` flags were given at startup, those entries are appended **before** the catch-all `*` so they win first-match. See §C.
|
||||
|
||||
#### A.2 Removal of `g_prompt_always_allow` as a *prompt outcome*
|
||||
|
||||
The flag is currently set to 1 by both:
|
||||
|
||||
- The interactive prompt's `[a]` outcome ([`src/server.c:684`](../src/server.c:684))
|
||||
- The running-TUI `[a]` hotkey ([`src/main.c:974`](../src/main.c:974), [`src/main.c:1239`](../src/main.c:1239))
|
||||
|
||||
The prompt's `[a]` no longer flips the flag. Instead it appends a per-`(caller, role)` policy entry. See §B.
|
||||
|
||||
The running-TUI `[a]` hotkey can either be:
|
||||
|
||||
- Removed, since it is now redundant with explicit per-caller entries, **or**
|
||||
- Retained as a debugging/panic mode under a non-default name, e.g. `[A]` (capital) for "auto-approve everything regardless of caller; for development only."
|
||||
|
||||
The plan recommends retaining it as a deliberately ugly, capital-letter hotkey so its destructive scope is visible. Default-off; activates only on explicit press.
|
||||
|
||||
#### A.3 Wire-level effect
|
||||
|
||||
A previously-unknown caller making a first request observes:
|
||||
|
||||
1. Length-prefixed request lands.
|
||||
2. Selector resolves (or fails — see §B).
|
||||
3. Policy lookup returns `POLICY_PROMPT` because no matching entry exists.
|
||||
4. Server blocks the request, prints the prompt to the controlling terminal, waits for keystroke.
|
||||
5. On `[a]`, server appends `(caller, role) → PROMPT_NEVER` and serves the request.
|
||||
6. On `[y]`, server serves once without appending.
|
||||
7. On `[n]`, server returns `policy_denied` without appending.
|
||||
8. On EOF / non-interactive: server returns `policy_denied`. There is no implicit allow path for non-interactive sessions; OS distributions must use `--preapprove`.
|
||||
|
||||
### Piece B — On-demand role derivation at the prompt
|
||||
|
||||
#### B.1 Failure path today
|
||||
|
||||
When `selector_resolve()` returns `SELECTOR_ERR_NOT_FOUND` for an unregistered `nostr_index`, the request currently dies in the dispatcher with `unknown_role` (error 1002). The user observes nothing in the terminal.
|
||||
|
||||
#### B.2 New failure path
|
||||
|
||||
The server's request-handling loop checks selector resolution **before** invoking the dispatcher. The new control flow:
|
||||
|
||||
```
|
||||
1. Receive request, parse method + selector.
|
||||
2. Try selector_resolve():
|
||||
a. SELECTOR_OK -> proceed to step 3 with the resolved role.
|
||||
b. SELECTOR_ERR_NOT_FOUND on nostr_index -> proceed to step 3 with
|
||||
a PENDING_DERIVATION marker holding the requested index.
|
||||
c. SELECTOR_ERR_NOT_FOUND on role_path -> reject with unknown_role.
|
||||
d. SELECTOR_ERR_AMBIGUOUS -> reject with ambiguous_role_selector.
|
||||
e. SELECTOR_ERR_NO_DEFAULT -> reject with internal_error
|
||||
(this should never happen if main role is always present).
|
||||
3. Determine policy: ALLOW / DENY / PROMPT.
|
||||
- For PENDING_DERIVATION cases, the prompt displays the requested
|
||||
index and clearly states "this will create a new identity."
|
||||
4. On PROMPT, ask human; on [a] or [y], proceed; on [n], reject.
|
||||
5. On approval to proceed:
|
||||
a. If PENDING_DERIVATION, register a role for that index in the
|
||||
role_table now. Synthesize a name like nostr_idx_7. Trigger
|
||||
key derivation for the new role.
|
||||
b. If [a] (not [y]), append a session policy entry for
|
||||
(caller, resolved-role-name) with PROMPT_NEVER.
|
||||
6. Invoke the dispatcher with the resolved role.
|
||||
7. Send response, append activity log line including approval source.
|
||||
```
|
||||
|
||||
#### B.3 Auto-registered role naming
|
||||
|
||||
A role auto-registered for `nostr_index = N` is named `nostr_idx_N` by the signer.
|
||||
|
||||
- Predictable, so humans reading the activity log can correlate.
|
||||
- Will not collide with the built-in `main` (which is `nostr_idx_0` by index but `main` by name; the role table allows lookups by either).
|
||||
- Does not require user input at the prompt. Naming UX is a future iteration.
|
||||
|
||||
#### B.4 Role-table sizing
|
||||
|
||||
[`ROLE_TABLE_MAX_ENTRIES`](../src/role_table.c) is currently small. With on-demand derivation a single session might accumulate dozens of `nostr_idx_*` entries. Bump the constant to an explicit budget (suggest 256). On overflow, return a clear error like `role_table_full` rather than crashing.
|
||||
|
||||
#### B.5 Persistence
|
||||
|
||||
Same as everything else in n_signer: derived roles vanish on process exit. The next session re-derives `nostr_idx_7` from the same mnemonic, getting the same key. This is normal and intentional.
|
||||
|
||||
### Piece C — `--preapprove` CLI flag
|
||||
|
||||
#### C.1 Flag syntax
|
||||
|
||||
```
|
||||
--preapprove <SPEC>
|
||||
```
|
||||
|
||||
`SPEC` is a comma-separated list of `key=value` pairs. Required keys:
|
||||
|
||||
- `caller=<id>` — exact caller identity to match. Format depends on transport:
|
||||
- `uid:1000`
|
||||
- `qubes:work-vm`
|
||||
- `tcp:[fdb8::1]:0` (port 0 means "any port")
|
||||
- `qubes:*` is **not** supported. Wildcards are intentionally restricted to the catch-all entry only.
|
||||
- One of:
|
||||
- `role=<name>` — match the role by name. Useful when the OS knows it wants `main`.
|
||||
- `nostr_index=<n>` — match by index. Equivalent to `role=nostr_idx_<n>` after auto-derivation.
|
||||
|
||||
Optional keys (deferred for now, but reserved):
|
||||
|
||||
- `verbs=<list>` — comma-separated verb list. Default is `*`.
|
||||
- `purposes=<list>` — comma-separated purpose list. Default is `*`.
|
||||
|
||||
Multiple `--preapprove` flags are allowed; each becomes a separate policy entry.
|
||||
|
||||
#### C.2 Example: `n_OS_tr` boot
|
||||
|
||||
`n_OS_tr`'s systemd unit file:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=/usr/bin/n_signer \
|
||||
--preapprove caller=qubes:nostr-relay,role=main \
|
||||
--preapprove caller=qubes:dm-handler,nostr_index=1 \
|
||||
--preapprove caller=qubes:contacts,nostr_index=2 \
|
||||
--preapprove caller=qubes:zaps,nostr_index=3
|
||||
```
|
||||
|
||||
At startup, n_signer:
|
||||
|
||||
1. Parses each `--preapprove` flag into a `policy_entry_t` with `prompt = PROMPT_NEVER`.
|
||||
2. For each entry that references an unregistered `nostr_index`, auto-derives the role *immediately at startup* (not lazily — we want all key derivation to happen before services start signaling readiness).
|
||||
3. Inserts each policy entry into the table **before** the catch-all `*` deny.
|
||||
4. Logs each pre-approval to stderr at boot so the systemd unit's journal records what was preapproved.
|
||||
|
||||
When `qubes:nostr-relay` connects and asks for `sign_event` against `role=main`, `policy_check()` matches the preapprove entry, returns `POLICY_ALLOW`, and the request is served with no prompt.
|
||||
|
||||
When some other caller (say, a misconfigured systemd service) connects, the catch-all matches and the request is denied (or prompts, in interactive mode — see §C.4).
|
||||
|
||||
#### C.3 What `--preapprove` cannot do
|
||||
|
||||
- **Cannot deny.** Deny entries can only come from defaults or the prompt's `[n]` (if we ever add caller-deny). Pre-approve only widens.
|
||||
- **Cannot match wildcards on caller.** Each entry must specify exactly one caller_id. The OS distribution is responsible for knowing exactly who its services are.
|
||||
- **Cannot bypass purpose/curve enforcement.** A pre-approval to use `role=main` for `verb=sign_btc_tx` (a hypothetical bitcoin verb) still fails enforcement because `main`'s purpose is `nostr`. The pre-approval grants access to the role; enforcement still gates the verb.
|
||||
|
||||
#### C.4 Interaction with interactive mode
|
||||
|
||||
The same binary supports both:
|
||||
|
||||
- `n_signer` with no `--preapprove` flags → pure deny-by-default, every first request prompts.
|
||||
- `n_signer --preapprove ...` → starts with the listed entries already in the policy table.
|
||||
|
||||
Both modes use exactly the same code path. There is no separate "system mode" toggle.
|
||||
|
||||
#### C.5 Auditability
|
||||
|
||||
When a pre-approval matches, the activity log line includes the source:
|
||||
|
||||
```
|
||||
qubes:nostr-relay sign_event(role=main) ALLOWED:preapprove
|
||||
```
|
||||
|
||||
So an operator reviewing the log can tell which decisions came from boot config vs. runtime user approvals.
|
||||
|
||||
#### C.6 Validation
|
||||
|
||||
Bad `--preapprove` syntax fails fast at startup with a clear error message. n_signer refuses to start. This avoids the "I thought I configured this but typoed it" silent-degrade problem.
|
||||
|
||||
### Piece D — Audit-log labeling of approval source
|
||||
|
||||
#### D.1 The four label values
|
||||
|
||||
| Label | Meaning |
|
||||
|---|---|
|
||||
| `:preapprove` | Matched a policy entry installed via `--preapprove` at startup. |
|
||||
| `:session-grant` | Matched a policy entry created earlier in this session by a prompt `[a]`. |
|
||||
| `:prompt` | Just-now approved by the user pressing `[y]` or `[a]` at the prompt. |
|
||||
| `:auto-allow` | Matched the running-TUI `[a]` panic mode (if retained). |
|
||||
|
||||
A line in the activity log under the new model:
|
||||
|
||||
```
|
||||
[2026-05-04 16:03:11] qubes:nostr-relay sign_event(role=main) ALLOWED:preapprove
|
||||
[2026-05-04 16:03:14] uid:1000 sign_event(role=main) ALLOWED:session-grant
|
||||
[2026-05-04 16:03:18] tcp:[::1]:54122 get_public_key(role=nostr_idx_7) ALLOWED:prompt
|
||||
[2026-05-04 16:03:21] uid:1001 sign_event(role=main) DENIED:no-match
|
||||
```
|
||||
|
||||
#### D.2 Implementation
|
||||
|
||||
The label is a small enum threaded from `policy_check()` (which returns the matching entry's source tag) and from `prompt_for_policy_decision()` (which returns `:prompt`). `server_handle_one()` includes the label in the activity-log line.
|
||||
|
||||
#### D.3 Visibility
|
||||
|
||||
The label appears in:
|
||||
|
||||
- The TUI's running activity buffer.
|
||||
- The structured activity event passed to the `server_activity_cb` callback (so external observers — e.g. a future syslog feeder — can also see the source).
|
||||
|
||||
#### D.4 No on-disk persistence
|
||||
|
||||
The activity buffer remains in-memory only. If `n_OS_tr` wants persistent audit, it forwards the activity callback to its own logging subsystem outside of n_signer's process.
|
||||
|
||||
## 7. Code change inventory
|
||||
|
||||
| Module | Change |
|
||||
|---|---|
|
||||
| [`src/policy.c`](../src/policy.c) | `policy_init_default()` collapses to one entry. `policy_check()` extends to return the matching entry's source label. New helper `policy_table_add_at_position()` to insert before the catch-all. |
|
||||
| [`src/server.c`](../src/server.c) | `prompt_for_policy_decision()` rewritten: appends a policy entry on `[a]`, no longer flips the global flag. New pre-derivation control flow in `server_handle_one()`. New CLI parser for `--preapprove`. |
|
||||
| [`src/role_table.c`](../src/role_table.c) | Bump `ROLE_TABLE_MAX_ENTRIES` to 256. New `role_table_register_nostr_index(int n)` helper that creates a `nostr_idx_<n>` entry and triggers derivation. |
|
||||
| [`src/key_store.c`](../src/key_store.c) | Add a one-role derivation path so newly-registered roles can be derived without re-deriving the whole table. |
|
||||
| [`src/dispatcher.c`](../src/dispatcher.c) | No semantic change — dispatcher continues to assume the role exists. The server makes that true before invoking. |
|
||||
| [`src/main.c`](../src/main.c) | CLI parsing for `--preapprove`. Remove or restrict the running-TUI `[a]` hotkey. Log preapproves to stderr at boot. |
|
||||
| [`tests/test_policy.c`](../tests/test_policy.c) | New tests: deny-by-default, prompt-`[a]` appends entry, preapprove flag parsing, source-label propagation. |
|
||||
| [`tests/test_dispatcher.c`](../tests/test_dispatcher.c) | New tests: unregistered nostr_index path with consent-required gating. |
|
||||
| [`tests/test_integration.c`](../tests/test_integration.c) | New scenario: `--preapprove` allows scripted client; without it, scripted client gets denied. |
|
||||
| [`documents/SECURITY.md`](../documents/SECURITY.md) | Rewritten in identity / index / approval terms (separate task after this plan ships). |
|
||||
| [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md) | Add a note about possible `policy_denied` on first contact in interactive mode; pointer to `--preapprove` for system services. |
|
||||
| [`plans/nsigner.md`](nsigner.md) | Decisions log entry referencing this plan. The `role_path` pre-registration rule remains; the `nostr_index` clause becomes new. |
|
||||
|
||||
## 8. Test plan
|
||||
|
||||
Tests must cover at minimum:
|
||||
|
||||
1. **Default deny.** A startup with no `--preapprove` flags receives a request from any caller and (in non-interactive mode) returns `policy_denied`.
|
||||
2. **Prompt `[a]` appends entry.** Inject a non-interactive prompt-default of `ALLOW`, observe a policy entry added, verify a second matching request is served without re-prompting.
|
||||
3. **Prompt `[y]` does not append entry.** Same fixture, second request still triggers the prompt path.
|
||||
4. **`--preapprove` with `role=main`.** Request from named caller succeeds without prompt; request from any other caller is denied.
|
||||
5. **`--preapprove` with `nostr_index=N` for unregistered N.** Role is auto-derived at startup; request succeeds; activity log shows `:preapprove`.
|
||||
6. **`--preapprove` cannot bypass enforcement.** Pre-approve `(caller=X, role=main)` and request a verb that violates purpose/curve. Response is `purpose_mismatch` or `curve_mismatch`.
|
||||
7. **Unknown `role_path` still rejected.** Even with consent, an unregistered `role_path` returns `unknown_role`. (The narrowing is `nostr_index`-only.)
|
||||
8. **Audit-log labels.** Each path produces the expected source tag.
|
||||
9. **Multi-instance isolation.** Two n_signers with different `--preapprove` lists serve different requests; cross-instance requests are denied as expected.
|
||||
10. **Role table overflow.** Force more than `ROLE_TABLE_MAX_ENTRIES` derivations and observe `role_table_full` instead of corruption.
|
||||
|
||||
## 9. Backwards-compatibility implications
|
||||
|
||||
- **Existing scripts that rely on same-uid auto-allow break.** They now hit the prompt (interactive) or get denied (non-interactive). Migration path: add `--preapprove caller=uid:1000,role=main` to the startup invocation. We document this prominently.
|
||||
- **Interactive desktop users see a prompt the first time their client connects, where today they don't.** This is the intended behavior change. We document it in the README.
|
||||
- **Wire protocol unchanged.** No client-side changes required.
|
||||
- **Existing `[a]` running-TUI hotkey may change semantics or disappear.** This is the most disruptive UI change for someone who actually uses the panic-allow today. Document the change in the release notes.
|
||||
|
||||
## 10. Rollout staging
|
||||
|
||||
This is too much to land in one commit. Suggested order:
|
||||
|
||||
1. **Stage 1 — deny-by-default + prompt `[a]` appends entry.** Removes the same-uid auto-allow, makes the prompt the source of session-grants. No CLI flag yet. Headless scripts break; we accept that for now and document the workaround as "use stage 2."
|
||||
2. **Stage 2 — `--preapprove` CLI flag.** Restores headless ergonomics for OS distributions like `n_OS_tr`.
|
||||
3. **Stage 3 — on-demand role derivation at the prompt.** Closes the user-visible `(unknown)` failure mode.
|
||||
4. **Stage 4 — audit-log labels and structured activity output.** Forensics + observability.
|
||||
5. **Stage 5 — `documents/SECURITY.md` rewrite.** After the code is settled.
|
||||
|
||||
Each stage is a self-contained release.
|
||||
|
||||
## 11. Open decisions to confirm before coding
|
||||
|
||||
- **Q1.** Keep the running-TUI `[a]` hotkey as a panic-allow under a different visual treatment, or remove it entirely? Plan recommends keep, capitalized, default-off.
|
||||
- **Q2.** Is `nostr_idx_<n>` an acceptable auto-naming convention, or do we want to ask the user for a name in the prompt? Plan recommends auto-name now, prompt-name later as iteration.
|
||||
- **Q3.** Should the prompt's "this will create a new identity" warning differ visually from a normal approval prompt? Plan recommends a clearly distinct extra line in the prompt body.
|
||||
- **Q4.** What is the ceiling for `ROLE_TABLE_MAX_ENTRIES`? Plan recommends 256 with a fail-loud overflow.
|
||||
- **Q5.** Do we want a TUI panel showing the active session approvals (helpful for the user to see what they've granted)? Plan recommends yes, smallest possible: caller + role + source label. Could be deferred to a later stage.
|
||||
|
||||
## 12. After this plan lands: deferred work
|
||||
|
||||
- Per-verb scoping at the prompt (the user wants `[a]` to mean "all verbs for this role" but a future `[c]` to mean "let me pick"). Deferred per the user's instruction.
|
||||
- Approval revocation hotkeys (`[k]` to clear all session-grants, `[K]` to clear one).
|
||||
- Approvals visible-and-editable as a TUI panel.
|
||||
- Persistent (cross-restart) approvals — explicitly out of scope; n_signer remains memory-only.
|
||||
- `role_path` auto-derivation under prompt — explicitly out of scope; the security argument in §5 stands.
|
||||
- Capability tokens for cross-machine transports (FIPS, USB serial) — separate plan, separate threat model.
|
||||
|
||||
## 13. References
|
||||
|
||||
- [`README.md`](../README.md) — authoritative behavior spec.
|
||||
- [`plans/nsigner.md`](nsigner.md) — root design plan; pre-registration rule at line 159.
|
||||
- [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md) — wire contract.
|
||||
- [`documents/SECURITY.md`](../documents/SECURITY.md) — security model document; will be rewritten when this plan lands.
|
||||
- [`src/policy.c`](../src/policy.c), [`src/server.c`](../src/server.c), [`src/dispatcher.c`](../src/dispatcher.c), [`src/role_table.c`](../src/role_table.c), [`src/selector.c`](../src/selector.c) — code that changes.
|
||||
Reference in New Issue
Block a user