Replace shared bearer token with registered Ed25519 client keys.

This commit is contained in:
alexveley
2026-09-22 06:54:43 -04:00
parent 535b3ce837
commit 43d94932fb
9 changed files with 412 additions and 100 deletions
+54 -33
View File
@@ -5,31 +5,61 @@ Use this when a script or coding agent needs to **create, update, move, or delet
## Preconditions
1. Native host installed: `powershell -NoProfile -File tools/install-native-host.ps1`
2. Extension loaded in the target Firefox profile (`about:debugging` temporary add-on, or a signed install)
3. Firefox **running** (the host process is started by the extension)
2. A **registered client key** for this tool (see Auth). The host has only the public half.
3. Extension loaded in the target Firefox profile (`about:debugging` temporary add-on, or a signed install)
4. Firefox **running** (the host process is started by the extension)
If `http://127.0.0.1:17634/health` fails, the host is not up — open Firefox and confirm the extension is loaded. If `/health` works but `/v1/ready` fails, the stdio pipe is down; reload the extension.
## Auth
Token file (created by the installer):
The loopback port is **not** an open local API. Each caller is a named Ed25519 client.
`%LOCALAPPDATA%\firefox-agent-bridge\token`
Register once per tool (human or agent operator does this; do not write the private key under `%LOCALAPPDATA%\firefox-agent-bridge`):
Send it on every request except `/health`:
```
Authorization: Bearer <token>
```text
python tools/register_client.py add --name cursor-agent --write-key %USERPROFILE%\.fab\cursor-agent.json
```
Override with env `FAB_TOKEN` or `FAB_URL` (default `http://127.0.0.1:17634`).
Give tooling **only** that key file:
```text
set FAB_KEY_FILE=%USERPROFILE%\.fab\cursor-agent.json
```
or `python tools/client.py --key PATH …`.
The host stores public keys in `%LOCALAPPDATA%\firefox-agent-bridge\clients.json`. Revoke with `python tools/register_client.py revoke NAME_OR_ID`. List with `… list`.
Every request except `/health` must be signed:
```
Authorization: FAB-ED25519 id=<client_id>
X-FAB-Timestamp: <unix seconds>
X-FAB-Nonce: <unique hex>
X-FAB-Signature: <base64 Ed25519 of canonical message>
```
Canonical message (UTF-8, newline-separated):
```text
v1
<client_id>
<timestamp>
<nonce>
<HTTP_METHOD>
<path>
<sha256 hex of raw body>
```
Skew allowance is 90 seconds. Nonces cannot be reused. `tools/client.py` builds this for you.
Do not use a shared bearer token. Do not commit private key bundles. Do not paste PEM material into chat.
## Preferred caller
From this repo:
```text
python tools/client.py METHOD [ARGS_JSON]
python tools/client.py --key KEYFILE METHOD [ARGS_JSON]
```
`ARGS_JSON` is a JSON **array** matching the WebExtension function arguments.
@@ -37,22 +67,11 @@ python tools/client.py METHOD [ARGS_JSON]
Examples:
```text
python tools/client.py meta.methods
python tools/client.py bookmarks.search "[{\"title\":\"10.132.x.x\"}]"
python tools/client.py bookmarks.getChildren "[\"FOLDER_ID\"]"
python tools/client.py bookmarks.create "[{\"parentId\":\"FOLDER_ID\",\"title\":\"NPM\",\"url\":\"http://10.132.99.80:81/\"}]"
python tools/client.py bookmarks.remove "[\"BOOKMARK_ID\"]"
python tools/client.py bookmarks.removeTree "[\"FOLDER_ID\"]"
python tools/client.py --key %USERPROFILE%\.fab\cursor-agent.json meta.methods
python tools/client.py --key %USERPROFILE%\.fab\cursor-agent.json bookmarks.search "[{\"title\":\"10.132.x.x\"}]"
```
HTTP equivalent:
```http
POST /v1/call
Content-Type: application/json
{"method": "bookmarks.search", "args": [{"title": "10.132.x.x"}]}
```
HTTP equivalent: `POST /v1/call` with the signature headers above and body `{"method":"bookmarks.search","args":[{"title":"10.132.x.x"}]}`.
## Allowlisted methods (v0.1)
@@ -74,11 +93,11 @@ Content-Type: application/json
Do not invent other `browser.*` names. Adding an API means editing `ALLOWED` in `extension/background.js` and reloading the extension.
REST aliases (same auth):
REST aliases (same signature):
| HTTP | Maps to |
|------|---------|
| `GET /health` | host process only (no token) |
| `GET /health` | host process only (no key) |
| `GET /v1/ready` | `meta.ping` |
| `GET /v1/methods` | `meta.methods` |
| `GET /v1/bookmarks/tree` | `bookmarks.getTree` |
@@ -98,7 +117,7 @@ REST aliases (same auth):
3. `bookmarks.remove` each bookmark; `bookmarks.removeTree` each child folder.
4. `bookmarks.create` each new item with `parentId` set.
`examples/replace_named_folder.py` does exactly that. It is an example, not a sync service.
`examples/replace_named_folder.py` does exactly that (`FAB_KEY_FILE` must be set). It is an example, not a sync service.
You cannot modify Firefox's bookmark root (`The bookmark root cannot be modified`). Operate on a named subfolder (toolbar / menu / a folder the user already created).
@@ -107,7 +126,9 @@ You cannot modify Firefox's bookmark root (`The bookmark root cannot be modified
| Symptom | Cause | What to do |
|---------|--------|------------|
| Connection refused on `:17634` | Firefox closed or extension not loaded | Open Firefox; load/reload the add-on |
| 401 | Missing/wrong bearer | Read the token file |
| 401 `signed FAB-ED25519 client required` | Old bearer token or unsigned curl | Use `tools/client.py` and a registered key |
| 401 `unknown client id` | Key revoked or host has no `clients.json` | `register_client.py list` / `add` |
| 401 `bad signature` / `replayed nonce` / skew | Wrong key, reused request, or clock drift | New request; check `FAB_KEY_FILE` |
| 502 `method not allowed` | Typo or API not in `ALLOWED` | Use `meta.methods` |
| 502 timeout | Extension died mid-call | Reload the add-on |
| Temporary add-on gone after restart | Unsigned on Firefox Release | Load again, or sign via AMO unlisted |
@@ -117,9 +138,9 @@ You cannot modify Firefox's bookmark root (`The bookmark root cannot be modified
- The extension does **not** listen on `runtime.onMessageExternal`. Other add-ons cannot call the dispatcher.
- The native host manifest `allowed_extensions` is pinned to `firefox-agent-bridge@easygoingaming.com`. A different add-on cannot `connectNative` to this host.
- HTTP is `127.0.0.1` only. Requests that carry a browser `Origin` header are rejected (pages cannot drive the API). `Host` must be `127.0.0.1:<port>` or `localhost:<port>`.
- A same-user process that can read `%LOCALAPPDATA%\firefox-agent-bridge\token` has the same power as this API. That is intentional for local agents. Do not copy the token into git, chat, or a world-readable file.
- Another add-on that already has the `bookmarks` permission does not need this bridge — Firefox already gave it Places. This project does not increase that add-on's capability.
- HTTP is `127.0.0.1` only. Requests that carry a browser `Origin` header are rejected. `Host` must be loopback.
- Local processes **without** a registered private key cannot edit bookmarks through this port. The public store is useless for impersonation.
- Another add-on that already has the `bookmarks` permission does not need this bridge.
- Do not add `tabs`, `history`, `<all_urls>`, or `onMessageExternal` without a new threat review.
## What this project is not