Files
n_signer/plans/role_path_authorization.md

229 lines
11 KiB
Markdown

# Plan: Role + Path Authorization Model
## Status: Finalized — ready for implementation
## Hardened vs unhardened derivation paths
BIP-32 derivation paths use `'` (or `h`) to mark **hardened** segments. The presence or absence of `'` changes the math and produces completely different keys.
- **Hardened** (`m/44'/1237'/0'/0'/0'`): requires the parent private key; compromising one child key does NOT compromise siblings. Best for agent isolation.
- **Unhardened** (`m/44'/1237'/0/0/0`): can derive public keys from the parent public key alone; but compromising one child private key + the extended public key compromises all siblings.
### Recommendation for multi-agent setups
Use **all-hardened** paths like `m/44'/1237'/0-99'/0'/0'` for 100 agents. This gives full isolation — if agent #5 is compromised, agents #0-4 and #6-99 are safe. Since n_signer always holds the private key, there's no benefit from unhardened derivation's "watching-only" capability.
### NIP-06 compatibility
NIP-06 defines `m/44'/1237'/<account>'/0/0` — the account segment is hardened, the last two are unhardened. If you need NIP-06 compatibility (keys work with standard Nostr tools), use `m/44'/1237'/0-99'/0/0`. If you don't care about NIP-06, harden everything.
### Current code support
The signer already supports arbitrary hardened paths. The path parser in [`src/key_store.c`](src/key_store.c:684) (`parse_derivation_path`) handles both `'` and `h`/`H` as hardened markers and sets the `0x80000000` bit accordingly. The secp256k1 derivation uses standard BIP-32 (`nostr_bip32_derive_path`). Non-secp256k1 curves (ed25519, x25519, PQ algorithms) already use all-hardened SLIP-0010 paths. So you can use `m/44'/1237'/0-99'/0'/0'` (all hardened) right now — no code changes needed for the derivation itself.
### Role preset menu implications
The role preset menu in the wizard should offer both NIP-06-compatible and all-hardened options:
- "Standard Nostr (NIP-06): secp256k1, m/44'/1237'/0'/0/0"
- "Standard Nostr hardened: secp256k1, m/44'/1237'/0'/0'/0'"
- "Nostr agent range (NIP-06): secp256k1, m/44'/1237'/0-99'/0/0"
- "Nostr agent range (hardened): secp256k1, m/44'/1237'/0-99'/0'/0'"
## Problem
The current selector model has three independent selectors (`nostr_index`, `role`, `role_path`) that are mutually exclusive and confusing:
- `nostr_index` bypasses the role system entirely — the server doesn't know which role's encryption scheme applies.
- `role_path` bypasses the role system for authorization.
- `index` is ambiguous when a role template has multiple variable segments (e.g. `m/44'/1237'/0-10'/0-10/0-10` — which "index"?).
- The `--allow-index` path whitelist is a separate authorization mechanism that duplicates what roles already do.
## New model
### Core principle
**Every request specifies both a role and a full path.** The role authorizes the request (acts as a password) and determines the encryption scheme. The path selects the specific key to derive. No exceptions, no backward compatibility for the old selectors.
### Client-side selectors (`n_signer_client`)
| Flag | Meaning |
|---|---|
| `--role <name> --path <full-bip44-path>` | **The only selector.** Both required for `nostr_*` verbs. Sends `{"role":"<name>","role_path":"<path>"}`. |
| `--nostr-index <N>` | **Removed.** |
| `--index <N>` | **Removed.** |
| `--role-path <path>` | **Removed** (replaced by `--path`). |
Example commands:
```bash
# Standard Nostr key (role "main", path m/44'/1237'/0'/0/0)
n_signer_client --role main --path "m/44'/1237'/0'/0/0" get-public-key
# A specific key from role1
n_signer_client --role role1 --path "m/44'/1237'/1'/1/0" get-public-key
# Sign an event
echo '{"kind":1,"content":"hello","tags":[],"created_at":1700000000}' \
| n_signer_client --role main --path "m/44'/1237'/0'/0/0" sign-event | nak publish
```
### Server-side authorization (`nsigner`)
#### Role-as-password with per-role approval flag
Each role defined in the wizard has a property: **`requires_approval`** (boolean, default `true`).
- **`requires_approval = false`**: Knowing the role name is sufficient authorization. If the client provides a valid role name and a path that matches the role's registered template, the request is authorized immediately — no interactive prompt. This is "role-as-password."
- **`requires_approval = true`**: The role name identifies the request, but the human attendant must approve each request interactively (existing approval flow). Use this for roles given to agents where you want to see and approve everything they do.
#### Path verification
When the client sends `{"role":"<name>","role_path":"<path>"}`:
1. Look up the role by name. If not found → `1002 unknown_role`.
2. Check if the requested path matches the role's registered template (substitute the variable segments and compare). If it doesn't match → `2003 path_not_allowed`.
3. If `requires_approval = false` → authorize immediately.
4. If `requires_approval = true` → prompt the human attendant (existing approval flow).
#### What's removed
- **`--allow-index`** flag and path whitelist — removed entirely. All access goes through roles now.
- **`--index`** selector option — removed. Use `--path` with the full path.
- **`--nostr-index`** selector — removed. Use `--role main --path "m/44'/1237'/N'/0/0"`.
- **`--role-path`** as a standalone selector (without `--role`) — removed. Use `--role` + `--path` together.
- **No backward compatibility** for `{"nostr_index":N}` or `{"role":"<name>","index":N}` — these are rejected with an error message explaining the new model.
#### `--allow-all` behavior
`--allow-all` still works for same-uid callers without a role — but only for the algorithm-based verbs (sign, verify, derive, etc.) that use `--algorithm` + `--index` (the algorithm index, not the nostr index). For `nostr_*` verbs, `--role` + `--path` are always required.
### Wizard changes
#### Mandatory role creation
Upon startup, the user is **required** to create at least one role. If no role is created, the signer exits with an error message: "At least one role must be defined."
#### Role preset menu
Instead of starting from a blank template, the wizard presents a menu of common presets:
```
Define a role:
1. Standard Nostr role (secp256k1, m/44'/1237'/0'/0/0)
2. Standard Nostr role with range (secp256k1, m/44'/1237'/0-100'/0/0)
3. SSH role (ed25519, m/44'/102001'/0'/0/0)
4. Age/x25519 role (x25519, m/44'/102002'/0'/0/0)
5. ML-DSA-65 role (post-quantum signatures, m/44'/102003'/0'/0/0)
6. SLH-DSA-128s role (post-quantum signatures, m/44'/102004'/0'/0/0)
7. ML-KEM-768 role (post-quantum KEM, m/44'/102005'/0'/0/0)
8. Custom path
Select [1]:
```
After selecting a preset, the user can:
- Edit the role name (default: `main` for option 1, `ssh` for option 3, etc.)
- Edit the path template (pre-filled from the preset)
- Set `requires_approval` (default: `true`)
Then the wizard loops: "Define another role? [y/N]"
#### Default role
The first role created is the default role. If the user selects preset 1 and keeps the name `main`, that becomes the default. The default role is used when a client sends a request without specifying a role — but since the new model requires both `--role` and `--path`, the "default role" concept only applies to the `--allow-all` algorithm-verb path.
### Verb-level granularity
**Not implemented.** All verbs within a role have the same authorization level. Future expansion.
## What changes in the code
### `client/n_signer_client.c`
- Add `--path <path>` flag (replaces `--role-path`).
- Remove `--nostr-index` flag.
- Remove `--index` flag (for nostr verbs; keep it for algorithm verbs where it's the algorithm derivation index).
- Remove `--role-path` flag.
- For `nostr_*` verbs: require both `--role` and `--path`. Error if either is missing.
- Update `--help` text and examples.
- Update `client/n_signer_client_README.md`.
### `src/role_table.c` / `src/role_table.h`
- Add `requires_approval` field to the role entry struct.
- Add role preset menu to the wizard.
- Make role creation mandatory (at least one role).
- Add function to check a path against a role's template (path matching).
### `src/selector.c`
- When both `role` and `role_path` are present: look up the role, verify the path matches the template, set the role index for key derivation.
- When `nostr_index` is present: reject with error (removed).
- When `index` is present without `--algorithm`: reject with error (removed for nostr verbs).
- When only `role_path` is present (no role): reject with error.
- When only `role` is present (no path): reject with error (unless the role has a fixed single path with no variable segments — in that case, use the role's default path).
### `src/policy.c`
- Add "role-as-password" authorization: if the role is known, the path matches, and `requires_approval = false`, allow without prompting.
- If `requires_approval = true`, use the existing approval flow.
- Remove `--allow-index` handling and the path whitelist.
### `src/main.c`
- Remove `--allow-index` flag parsing.
- Remove `--nostr-index` references in help text.
- Update the wizard to use the preset menu and prompt for `requires_approval`.
- Make role creation mandatory.
### `src/dispatcher.c`
- Update selector resolution to use the new role+path model.
- Remove the old `--allow-index` path whitelist checks.
- Reject `nostr_index` and `index` (for nostr verbs) with clear error messages.
### Tests
- Update `tests/test_n_signer_client.sh` to use `--role` + `--path` instead of `--nostr-index` / `--index`.
- Update `tests/test_integration.c` to use the new selector model.
- Remove or repurpose `tests/test_path_whitelist.c` (path whitelist is gone).
- Add tests for the `requires_approval` flag (both true and false).
- Add tests for the role preset menu.
### Documentation
- Update `README.md` §4.6 (role-based selectors) to describe the new model.
- Update `client/n_signer_client_README.md`.
- Update `client/n_signer_client_PLAN.md`.
- Update `documents/CLIENT_IMPLEMENTATION.md`.
## Mermaid: new authorization flow
```mermaid
flowchart TD
A[Client request with role + path] --> B{Role known?}
B -- No --> E[Reject: unknown_role]
B -- Yes --> C{Path matches role template?}
C -- No --> F[Reject: path_not_allowed]
C -- Yes --> D{requires_approval?}
D -- No --> G[Authorize — no prompt]
D -- Yes --> H[Prompt human attendant]
H -- allow --> G
H -- deny --> I[Reject: unauthorized]
```
## Mermaid: wizard role creation
```mermaid
flowchart TD
A[Startup] --> B[Show role preset menu]
B --> C[User selects preset]
C --> D[Pre-fill path template]
D --> E[User edits role name]
E --> F[User edits path template]
F --> G[User sets requires_approval]
G --> H[Register role]
H --> I{Define another role?}
I -- Yes --> B
I -- No --> J{At least one role defined?}
J -- No --> K[Error: at least one role required]
J -- Yes --> L[Continue to transport selection]
```
## Out of scope
- Verb-level granularity (can-see-pubkey-but-cant-sign) — future expansion.
- Role revocation / rotation — not needed yet.
- Role names as actual cryptographic tokens (currently just plain text names) — future hardening.
- Backward compatibility for `nostr_index` / `index` — intentionally removed.