feat: replace connection keys with short seed-derived rtun3 keys
CI / cargo fmt (push) Canceled after 0s
CI / cargo clippy (macos-latest) (push) Canceled after 0s
CI / cargo clippy (ubuntu-latest) (push) Canceled after 0s
CI / cargo clippy (windows-latest) (push) Canceled after 0s
CI / cargo test (macos-latest) (push) Canceled after 0s
CI / cargo test (ubuntu-latest) (push) Canceled after 0s
CI / cargo test (windows-latest) (push) Canceled after 0s
CI / cargo build (macos-latest) (push) Canceled after 0s
CI / cargo build (ubuntu-latest) (push) Canceled after 0s
CI / cargo build (windows-latest) (push) Canceled after 0s
CI / cargo build --release (macos-latest) (push) Canceled after 0s
CI / cargo build --release (ubuntu-latest) (push) Canceled after 0s
CI / cargo build --release (windows-latest) (push) Canceled after 0s
CI / CLI smoke (macos-latest) (push) Canceled after 0s
CI / CLI smoke (ubuntu-latest) (push) Canceled after 0s
CI / CLI smoke (windows-latest) (push) Canceled after 0s
CI / Minimal E2E (macos-latest) (push) Canceled after 0s
CI / Minimal E2E (ubuntu-latest) (push) Canceled after 0s
CI / Minimal E2E (windows-latest) (push) Canceled after 0s

Connection keys are now a ~49-char seed (rtun3.) instead of a bundled
~1740-char certificate blob. Both endpoints deterministically derive an
identical Ed25519 CA from the seed and mint ephemeral server/client leaves
at startup (keyderive.rs); the app-layer auth token is derived from the seed.
Target is passed separately on connect (resocks-style).

- connkey.rs: rtun3 seed parse/format
- keyderive.rs: CA/server/client/token derivation
- keygen takes no args; connect requires --target
- remove miniz_oxide; TLS layer unchanged
- add determinism + key-based e2e + wrong-seed-rejected tests
- update wiki, README, design spec, CI smoke
This commit is contained in:
bzuccaro
2026-08-02 12:31:53 -06:00
parent 6bc3c00b64
commit 2bf8da739b
20 changed files with 616 additions and 338 deletions
+37 -41
View File
@@ -1,69 +1,65 @@
# Connection keys
A connection key bundles all credential material and the target address into a single copy-pasteable string.
## Purpose
Simplify distribution of tunnel credentials between machines. Instead of transferring eight separate files, a user can generate one key and paste it into the `listen` and `connect` commands.
A connection key is a short random **seed** from which both endpoints derive
identical TLS material on the spot. It is the only value a user must copy/paste
to stand up a tunnel.
## Format
Connection keys start with the prefix `rtun2.` followed by base64url-encoded (no
padding) **DEFLATE-compressed** JSON:
Connection keys are ~49 characters: the prefix `rtun3.` followed by a
base64url-encoded 32-byte seed:
```
rtun2.<base64url(deflate(json))>
rtun3.OH2TI3p1bM2dYjBcHxnLmZT9qKjW8tVvXQ0N7y4E3wA
```
The JSON payload is compressed with DEFLATE (`miniz_oxide`) before base64url-encoding
to keep the key short enough to copy and paste comfortably. Example payload:
The key ships **no certificates**. Instead, both endpoints derive an identical CA
from the seed and mint ephemeral leaf certificates locally (`src/keyderive.rs`):
```json
{
"version": 2,
"target": "198.51.100.10:4180",
"ca_cert_pem": "-----BEGIN CERTIFICATE-----...",
"server_cert_pem": "-----BEGIN CERTIFICATE-----...",
"server_key_pem": "-----BEGIN PRIVATE KEY-----...",
"client_cert_pem": "-----BEGIN CERTIFICATE-----...",
"client_key_pem": "-----BEGIN PRIVATE KEY-----...",
"auth_token": "a1b2c3..."
}
```
- **CA** — Ed25519 keypair from the seed, self-signed and deterministic, so any two
endpoints that share the seed produce the same CA.
- **Server leaf** — generated at `listen` startup, signed by the derived CA, with
SANs for the bind address (and optional `--advertise` host).
- **Client leaf** — generated at `connect` startup, signed by the derived CA.
- **Auth token** — derived as `sha256(seed || "rustunnel-auth-token")` (first 16
bytes hex) and checked in the existing app-layer handshake.
Because the CA is derived from the seed, a connector holding a different seed cannot
authenticate to a listener — possession of the key is what grants access.
## Key abstractions
| Type | File | Description |
| ---- | ---- | ----------- |
| `ConnectionKey` | `src/connkey.rs` | Struct with all fields, version check, validation |
| `ConnectionKey::encode` | `src/connkey.rs` | Serialize to JSON, DEFLATE-compress, base64url-encode, prepend prefix |
| `ConnectionKey::decode` | `src/connkey.rs` | Strip prefix, base64url-decode, DEFLATE-decompress, deserialize, validate |
| `looks_like_connection_key` | `src/connkey.rs` | Quick check if a string starts with `rtun2.` |
| Type/Function | File | Description |
| ------------- | ---- | ----------- |
| `ConnectionKey` | `src/connkey.rs` | Seed value, `new`/`encode`/`decode` (rtun3) |
| `derive_server_material` | `src/keyderive.rs` | CA + server leaf PEM for `listen` |
| `derive_client_material` | `src/keyderive.rs` | CA + client leaf PEM for `connect` |
| `derive_auth_token` | `src/keyderive.rs` | App-layer token derived from the seed |
| `looks_like_connection_key` | `src/connkey.rs` | Quick check if a string starts with `rtun3.` |
## Validation
`decode` validates:
`ConnectionKey::decode` validates:
- Prefix must be `rtun2.`
- Base64 decoding must succeed
- DEFLATE decompression must succeed
- JSON deserialization must succeed
- Version must be exactly `2`
- All string fields must be non-empty after trimming
Legacy `rtun1.` keys are **not** accepted.
- Prefix must be `rtun3.` (legacy `rtun1.`/`rtun2.` are rejected)
- Base64url decode succeeds and yields exactly 32 bytes
- The seed is not all zeros
## Integration
`src/main.rs` uses `ConnectionKey::decode` when the `--connection-key` flag or positional argument is provided. The decoded material is passed to `ServerTlsMaterial::Pem` or `ClientTlsMaterial::Pem` variants, which bypass file loading and use the embedded PEM strings directly.
`src/main.rs` decodes a seed key (positional arg, `--connection-key`, or
`RUSTUNNEL_KEY`), derives the appropriate material at startup, and feeds the PEMs
into `ServerTlsMaterial::Pem` / `ClientTlsMaterial::Pem`. The TLS layer
(`src/tls.rs`) is unchanged. `connect` requires an explicit `--target`.
## Entry points for modification
- To change the key format or add versioning: modify `src/connkey.rs`.
- To add encryption: consider extending the encode/decode pipeline in `ConnectionKey`.
- To change derivation or signing parameters: modify `src/keyderive.rs`.
- To change the seed format or versioning: modify `src/connkey.rs`.
## Key source files
| File | Purpose |
| ---- | ------- |
| `src/connkey.rs` | Connection key struct, encoding, decoding, validation |
| `src/connkey.rs` | Seed key struct, encoding, decoding |
| `src/keyderive.rs` | CA + leaf + token derivation from the seed |