Skip to content

Commit 3c5fd06

Browse files
authored
Merge pull request #9 from codeSamuraii/copilot/add-worker-logging
Add token-based signing for automated deployments
2 parents fa24a46 + 15022e3 commit 3c5fd06

9 files changed

Lines changed: 759 additions & 43 deletions

File tree

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,20 @@ See [Sandbox](docs/SANDBOX.md) for configuration and management.
5858

5959
## Signing
6060

61-
PIN-based pairing + HMAC-SHA256 — workers reject untrusted or tampered tasks:
61+
Pre-shared token or PIN-based pairing + HMAC-SHA256 — workers reject untrusted or tampered tasks:
6262

6363
```bash
64-
pyfuse worker --backend redis://localhost:6379 --pair # displays a 6-digit PIN
65-
pyfuse pair --backend redis://localhost:6379 # on client: enter the PIN
64+
# Token-based (recommended for CI/CD)
65+
pyfuse token generate # generate once
66+
export PYFUSE_SIGNING_TOKEN=<token> # set on client & worker
67+
pyfuse worker --backend redis://localhost:6379 --require-signing
68+
69+
# PIN-based pairing (interactive)
70+
pyfuse worker --backend redis://localhost:6379 --pair # displays a 6-digit PIN
71+
pyfuse pair --backend redis://localhost:6379 # on client: enter the PIN
6672
```
6773

68-
After pairing, tasks are signed automatically. No client-side code changes. See [Signing & Pairing](docs/SIGNING.md) for details.
74+
After setup, tasks are signed automatically. No client-side code changes. See [Signing & Pairing](docs/SIGNING.md) for details.
6975

7076
## Features
7177

@@ -80,7 +86,7 @@ After pairing, tasks are signed automatically. No client-side code changes. See
8086
| **Content-hash caching** | Same code = cache hit, regardless of client |
8187
| **Pluggable backends** | `redis://` (multi-machine) or `local://` (same-machine TCP) |
8288
| **Docker sandbox** | Container isolation, transparent to clients |
83-
| **Signed execution** | PIN-based pairing + HMAC-SHA256 task authentication |
89+
| **Signed execution** | Pre-shared token or PIN pairing + HMAC-SHA256 task authentication |
8490
| **Graceful shutdown** | Ctrl+C drains in-flight tasks; second Ctrl+C force-quits |
8591

8692
## Documentation

docs/CONTEXT.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ pyfuse is a Python library for distributed function execution via automatic sour
1111
```
1212
pyfuse/
1313
├── __init__.py # Public API surface (trace, connect, serve, serialize, etc.)
14-
├── __main__.py # CLI: worker, run, info, serialize, reconstruct
14+
├── __main__.py # CLI: worker, run, info, serialize, reconstruct, token, pair
1515
├── _venv.py # Temporary virtual environment management (async)
1616
├── py.typed # PEP 561 typed package marker
1717
├── core/
1818
│ ├── task.py # Task dataclass: serializable envelope (graph + args + options)
1919
│ ├── models.py # FunctionNode and ImportInfo dataclasses, content hashing
2020
│ ├── version.py # _VERSION = "0.4.0"
2121
│ ├── errors.py # Error, WorkerError, RemoteError, DependencyError, TaskStalled, TaskCancelled
22+
│ ├── signing.py # HMAC-SHA256 task signing and verification
23+
│ ├── pairing.py # PIN-based pairing protocol for key exchange
24+
│ ├── token.py # Pre-shared token generation, persistence, key resolution
2225
│ └── progress.py # ProgressInfo dataclass, progress() function, context variable
2326
├── graph/
2427
│ ├── decorator.py # @trace: marks functions, adds .run()/.start()/.map()

docs/QUICK_START.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,20 @@ See [Sandbox](SANDBOX.md) for configuration and management.
112112

113113
## Signing
114114

115-
PIN-based pairing + HMAC-SHA256 — workers reject untrusted or tampered tasks:
115+
Pre-shared token or PIN-based pairing + HMAC-SHA256 — workers reject untrusted or tampered tasks:
116116

117117
```bash
118-
pyfuse worker --backend redis://localhost:6379 --pair # displays a 6-digit PIN
119-
pyfuse pair --backend redis://localhost:6379 # on client: enter the PIN
118+
# Token-based (recommended for CI/CD)
119+
pyfuse token generate # generate once
120+
export PYFUSE_SIGNING_TOKEN=<token> # set on client & worker
121+
pyfuse worker --backend redis://localhost:6379 --require-signing
122+
123+
# PIN-based pairing (interactive)
124+
pyfuse worker --backend redis://localhost:6379 --pair # displays a 6-digit PIN
125+
pyfuse pair --backend redis://localhost:6379 # on client: enter the PIN
120126
```
121127

122-
After pairing, tasks are signed automatically. No client-side code changes. See [Signing & Pairing](SIGNING.md) for details.
128+
After setup, tasks are signed automatically. No client-side code changes. See [Signing & Pairing](SIGNING.md) for details.
123129

124130
## Backends
125131

docs/SIGNING.md

Lines changed: 187 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,67 @@
11
# Signing & Pairing
22

3-
Cryptographically sign serialized tasks so that workers only execute code from trusted clients. The signing system uses a PIN-based pairing protocol — no manual key management required.
3+
Cryptographically sign serialized tasks so that workers only execute code from trusted clients. Two key distribution methods are available:
4+
5+
- **Token-based** (recommended for automation): Generate a shared token offline, distribute via environment variables or secrets management. No real-time coordination needed.
6+
- **PIN-based pairing** (recommended for interactive use): A client and worker exchange keys via a 6-digit PIN displayed on one side and entered on the other.
7+
8+
Both methods use the same underlying HMAC-SHA256 signing — they differ only in how the shared secret is established.
49

510
## Overview
611

712
By default, pyfuse workers execute any task they receive from the backend. When signing is enabled:
813

9-
1. A client and worker **pair** once using a short PIN code.
10-
2. Both sides derive a shared HMAC key from the PIN.
11-
3. The client **signs** every task with HMAC-SHA256 before submitting it.
12-
4. The worker **verifies** the signature before executing the task.
14+
1. A client and worker share a cryptographic key (via **token** or **pairing**).
15+
2. The client **signs** every task with HMAC-SHA256 before submitting it.
16+
3. The worker **verifies** the signature before executing the task.
1317

1418
Tasks with missing or invalid signatures are rejected.
1519

16-
## Quick start
20+
## Quick start — Token (recommended for CI/CD)
21+
22+
### 1. Generate a token
23+
24+
```bash
25+
pyfuse token generate
26+
```
27+
28+
```
29+
Token generated and saved to ~/.pyfuse/token
30+
31+
Token: a1b2c3d4e5f6...
32+
33+
Set this on both client and worker:
34+
export PYFUSE_SIGNING_TOKEN=a1b2c3d4e5f6...
35+
```
36+
37+
### 2. Distribute the token
38+
39+
Copy the token to both the client and worker machines. The recommended method is an environment variable:
40+
41+
```bash
42+
# On both client and worker
43+
export PYFUSE_SIGNING_TOKEN=a1b2c3d4e5f6...
44+
```
45+
46+
For CI/CD, store the token as a secret in your CI provider (GitHub Actions secrets, GitLab CI variables, etc.) and inject it as `PYFUSE_SIGNING_TOKEN`.
47+
48+
Alternatively, copy the `~/.pyfuse/token` file to both machines.
49+
50+
### 3. Start the worker with signing
51+
52+
```bash
53+
pyfuse worker --backend redis://localhost:6379 --require-signing
54+
```
55+
56+
### 4. Run tasks (no changes needed)
57+
58+
```bash
59+
python examples/remote_execution.py
60+
```
61+
62+
The client automatically loads the token and signs tasks before submission. No code changes are needed.
63+
64+
## Quick start — PIN-based pairing
1765

1866
### 1. Start a worker with pairing
1967

@@ -80,6 +128,41 @@ pyfuse worker --backend redis://localhost:6379 --require-signing
80128

81129
## How it works
82130

131+
### Key resolution
132+
133+
When signing is enabled, both client and worker resolve the signing key using the following precedence order:
134+
135+
1. **`PYFUSE_SIGNING_TOKEN` environment variable** — hex-encoded token (highest priority)
136+
2. **`~/.pyfuse/token` file** — hex-encoded token written by `pyfuse token generate`
137+
3. **`~/.pyfuse/{client,worker}.key` file** — raw bytes from PIN-based pairing
138+
139+
This means you can migrate from pairing to tokens without disruption: set the environment variable and it takes precedence over any existing pairing key.
140+
141+
### Token signing
142+
143+
```
144+
Generate (once) Distribute
145+
────────────── ──────────
146+
pyfuse token generate Copy token to CI secrets,
147+
│ env vars, or config
148+
└─→ random 32-byte token │
149+
saved to ~/.pyfuse/token │
150+
151+
Client Worker
152+
────── ──────
153+
Load token Load token
154+
│ │
155+
├── HMAC-SHA256(key, task_json) │
156+
├── attach signature │
157+
├── submit to backend ──────────────→ │
158+
│ ├── extract signature
159+
│ ├── HMAC-SHA256(key, task_json)
160+
│ ├── constant-time compare
161+
│ │
162+
│ ├── match? → execute
163+
│ └── mismatch? → reject
164+
```
165+
83166
### Pairing protocol
84167

85168
The pairing protocol is inspired by SPAKE2 and SAS-based verification:
@@ -123,7 +206,7 @@ Enter PIN: 482913 Enter PIN: 482913
123206

124207
### Task signing
125208

126-
Once paired, the client signs tasks with HMAC-SHA256:
209+
Once a shared key is established (via token or pairing), the client signs tasks with HMAC-SHA256:
127210

128211
```
129212
Client Worker
@@ -144,13 +227,41 @@ The signature covers the entire task payload — graph JSON, function name, argu
144227

145228
## CLI reference
146229

230+
### `pyfuse token generate`
231+
232+
```bash
233+
pyfuse token generate [--force]
234+
```
235+
236+
Generates a random 32-byte signing token and saves it to `~/.pyfuse/token`. Prints the hex-encoded token and usage instructions.
237+
238+
| Flag | Description |
239+
|------|-------------|
240+
| `--force` | Overwrite an existing token |
241+
242+
### `pyfuse token show`
243+
244+
```bash
245+
pyfuse token show
246+
```
247+
248+
Displays the current token source (environment variable or file) and a truncated preview.
249+
250+
### `pyfuse token clear`
251+
252+
```bash
253+
pyfuse token clear
254+
```
255+
256+
Removes the saved `~/.pyfuse/token` file.
257+
147258
### `pyfuse worker --pair`
148259

149260
```bash
150261
pyfuse worker --backend URL --pair
151262
```
152263

153-
Generates a PIN, pairs with a client, then starts serving with signing automatically enabled. This is the recommended way to set up a signed worker.
264+
Generates a PIN, pairs with a client, then starts serving with signing automatically enabled. This is the recommended way to set up a signed worker interactively.
154265

155266
### `pyfuse pair`
156267

@@ -173,7 +284,7 @@ pyfuse pair --backend URL [--pin PIN] [--timeout SECS] [--force] [--clear]
173284
pyfuse worker --backend URL --require-signing
174285
```
175286

176-
When `--require-signing` is set, the worker loads `~/.pyfuse/worker.key` and rejects any task that is unsigned or has an invalid signature. If no key file is found, the worker exits with an error.
287+
When `--require-signing` is set, the worker loads signing key material using the standard resolution order (env var → token file → pairing key) and rejects any task that is unsigned or has an invalid signature. If no key material is found, the worker exits with an error.
177288

178289
## Programmatic usage
179290

@@ -182,7 +293,7 @@ When `--require-signing` is set, the worker loads `~/.pyfuse/worker.key` and rej
182293
```python
183294
from pyfuse.core.signing import derive_key, compute_signature, verify_signature
184295

185-
# After pairing, both sides have the same shared_key
296+
# After pairing or with a token, both sides have the same shared_key
186297
signing_key = derive_key(shared_key)
187298

188299
# Sign
@@ -208,6 +319,33 @@ signed_json = task.to_json(signing_key=key)
208319
task = Task.from_json(signed_json, signing_key=key) # raises SignatureError on failure
209320
```
210321

322+
### Resolving keys programmatically
323+
324+
```python
325+
from pyfuse.core.token import resolve_signing_key
326+
327+
# Resolves from env var → token file → pairing key
328+
key = resolve_signing_key("client") # or "worker"
329+
if key is not None:
330+
signed_json = task.to_json(signing_key=key)
331+
```
332+
333+
### Token management
334+
335+
```python
336+
from pyfuse.core.token import generate_token, save_token, load_token, clear_token
337+
338+
# Generate and save
339+
token = generate_token()
340+
save_token(token)
341+
342+
# Load (checks env var first, then file)
343+
token = load_token()
344+
345+
# Clean up
346+
clear_token()
347+
```
348+
211349
### Pairing programmatically
212350

213351
```python
@@ -232,38 +370,64 @@ save_shared_key(result.shared_key, "client")
232370

233371
| File | Purpose |
234372
|------|---------|
235-
| `~/.pyfuse/client.key` | Client's shared key (32 bytes) |
236-
| `~/.pyfuse/worker.key` | Worker's shared key (32 bytes) |
237-
238-
Both files are created with `0600` permissions (owner-only read/write).
373+
| `~/.pyfuse/token` | Pre-shared signing token (hex-encoded, 64 chars) |
374+
| `~/.pyfuse/client.key` | Client's pairing key (32 bytes, from `pyfuse pair`) |
375+
| `~/.pyfuse/worker.key` | Worker's pairing key (32 bytes, from `pyfuse pair`) |
376+
377+
All files are created with `0600` permissions (owner-only read/write).
378+
379+
| Environment variable | Purpose |
380+
|---------------------|---------|
381+
| `PYFUSE_SIGNING_TOKEN` | Hex-encoded signing token (overrides file) |
382+
383+
### CI/CD example (GitHub Actions)
384+
385+
```yaml
386+
# .github/workflows/deploy.yml
387+
jobs:
388+
run-task:
389+
runs-on: ubuntu-latest
390+
env:
391+
PYFUSE_SIGNING_TOKEN: ${{ secrets.PYFUSE_SIGNING_TOKEN }}
392+
PYFUSE_BACKEND: redis://your-redis-host:6379
393+
steps:
394+
- uses: actions/checkout@v4
395+
- run: pip install pyfuse[redis]
396+
- run: python my_task.py
397+
```
239398
240-
To re-pair, use `--force`:
399+
### Regenerating tokens
241400
242401
```bash
243-
pyfuse worker --backend redis://localhost:6379 --pair # (or --force on 'pyfuse pair')
244-
pyfuse pair --backend redis://localhost:6379 --force
402+
pyfuse token generate --force
245403
```
246404

247-
To remove keys:
405+
Then update the `PYFUSE_SIGNING_TOKEN` secret in your CI provider and restart workers.
406+
407+
### Clearing credentials
248408

249409
```bash
410+
# Token
411+
pyfuse token clear
412+
413+
# Pairing keys
250414
pyfuse pair --clear
251415
pyfuse pair --role worker --clear
252416
```
253417

254418
## Troubleshooting
255419

256-
**"Signing is enabled but no shared key found"**
257-
- Run `pyfuse worker --pair` or `pyfuse pair --role worker` first to establish a shared key.
420+
**"Signing is enabled but no key material found"**
421+
- Set `PYFUSE_SIGNING_TOKEN`, run `pyfuse token generate`, or run `pyfuse pair` to establish key material.
258422

259423
**"Task is unsigned but signing is enabled"**
260-
- The client is not signing tasks. Ensure `~/.pyfuse/client.key` exists (run `pyfuse pair`).
424+
- The client is not signing tasks. Ensure the token is set via `PYFUSE_SIGNING_TOKEN` or `~/.pyfuse/token`, or that `~/.pyfuse/client.key` exists (from pairing).
261425

262426
**"Task signature verification failed"**
263-
- The client and worker have different keys. Re-pair both sides.
427+
- The client and worker have different keys. Ensure both sides use the same token or re-pair.
264428

265429
**"PIN mismatch — pairing failed"**
266430
- The PINs entered on client and worker don't match. Try again.
267431

268432
**"Pairing timed out"**
269-
- Both sides must run pairing within the timeout window (default: 60s).
433+
- Both sides must run pairing within the timeout window (default: 60s). Consider using tokens instead for automated setups.

0 commit comments

Comments
 (0)