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
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:
@@ -10,7 +10,7 @@ Start the HTTPS tunnel listener. Binds an HTTPS server that accepts mTLS connect
|
||||
|
||||
Key arguments:
|
||||
- `--listen` — bind address (default `0.0.0.0:4180`)
|
||||
- `--advertise` — public address embedded in auto-generated connection keys
|
||||
- `--advertise` — public host added to the derived server cert and used in the printed connect hint
|
||||
- `--connection-key` — reusable key generated by `keygen` or a previous `listen` run
|
||||
- `--socks` — optional server-side SOCKS5 proxy address for connector-side network access
|
||||
- `--cert`, `--key`, `--ca-cert` — TLS material paths (required unless using a connection key)
|
||||
@@ -25,7 +25,7 @@ Connect to the listener and expose a local SOCKS5 proxy.
|
||||
|
||||
Key arguments:
|
||||
- `CONNECTION_KEY` — positional connection key (optional)
|
||||
- `--target` — listener address (default from connection key)
|
||||
- `--target` — listener address (required; not stored in the key)
|
||||
- `--connection-key` — connection key via flag or env `RUSTUNNEL_KEY`
|
||||
- `--socks` — local SOCKS5 proxy address (default `127.0.0.1:1180`)
|
||||
- `--cert`, `--key`, `--ca-cert` — TLS material paths (required unless using a connection key)
|
||||
@@ -39,7 +39,9 @@ Creates: `ca.pem`, `ca.key`, `server.crt`, `server.key`, `client.crt`, `client.k
|
||||
|
||||
### `keygen`
|
||||
|
||||
Generate a single reusable connection key string. This bundles all certificate material, the auth token, and the target address into a base64url-encoded, DEFLATE-compressed JSON blob prefixed with `rtun2.`.
|
||||
Print a fresh ~49-char connection key (prefix `rtun3.`). The key is a random 32-byte
|
||||
seed from which both endpoints derive identical TLS material; no certificates are
|
||||
shipped. Pass the target address separately on `connect`.
|
||||
|
||||
### `version`
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -13,7 +13,7 @@ The project uses a layered error strategy:
|
||||
- The `Redacted` wrapper in `src/redact.rs` must be used before logging any secret.
|
||||
- `Redacted::inner()` and `into_inner()` are marked `#[allow(dead_code)]` to discourage use, but available for file writes.
|
||||
- `is_sensitive()` detects PEM private key blocks. `is_auth_token()` detects long alphanumeric strings.
|
||||
- `is_connection_key()` detects strings starting with `rtun2.`.
|
||||
- `is_connection_key()` detects strings starting with `rtun3.`.
|
||||
|
||||
## Async patterns
|
||||
|
||||
|
||||
@@ -76,26 +76,27 @@ curl --proxy socks5h://127.0.0.1:1180 http://127.0.0.1:4181/
|
||||
|
||||
## Using a connection key
|
||||
|
||||
Instead of passing individual certificate paths, you can generate a single connection key:
|
||||
Instead of passing individual certificate paths, you can use a short connection key
|
||||
from which both ends derive identical credentials:
|
||||
|
||||
```sh
|
||||
./target/release/rustunnel keygen --target 127.0.0.1:4180
|
||||
./target/release/rustunnel keygen
|
||||
```
|
||||
|
||||
Then start the listener with the key (it auto-generates credentials):
|
||||
Start the listener with the key:
|
||||
|
||||
```sh
|
||||
./target/release/rustunnel listen --connection-key <key>
|
||||
./target/release/rustunnel listen --listen 0.0.0.0:4180 --connection-key <key>
|
||||
```
|
||||
|
||||
And connect with the same key:
|
||||
Connect with the same key, passing the listener address explicitly:
|
||||
|
||||
```sh
|
||||
./target/release/rustunnel connect <key>
|
||||
./target/release/rustunnel connect --target 127.0.0.1:4180 <key>
|
||||
```
|
||||
|
||||
## Defaults
|
||||
|
||||
- Listener bind address: `0.0.0.0:4180`
|
||||
- Connector target: required (or taken from connection key)
|
||||
- Connector target: required (not stored in the key)
|
||||
- Connector SOCKS5 proxy: `127.0.0.1:1180`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Glossary
|
||||
|
||||
**Connection key** — a base64url-encoded, DEFLATE-compressed JSON blob (prefix `rtun2.`) that bundles the CA certificate, server certificate, server key, client certificate, client key, target address, auth token, and version. Generated by `rustunnel keygen` and consumed by `rustunnel listen` and `rustunnel connect`.
|
||||
**Connection key** — a short (~49-char) base64url-encoded 32-byte seed (prefix `rtun3.`). Both endpoints derive an identical CA and ephemeral leaf certificates from the seed, so no certificates are ever shipped. Generated by `rustunnel keygen` and consumed by `rustunnel listen` and `rustunnel connect`.
|
||||
|
||||
**Connector** — the `rustunnel connect` side. Establishes an outbound HTTPS+mTLS connection to the listener, authenticates, then exposes a local SOCKS5 proxy for local applications.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ The tool is designed for local labs, development environments, and controlled te
|
||||
- **Listener** (`rustunnel listen`) — binds an HTTPS server that accepts mTLS connections from connectors, validates an auth token, then upgrades the connection to a persistent binary-framed tunnel.
|
||||
- **Connector** (`rustunnel connect`) — connects to the listener over HTTPS with mTLS, authenticates, then exposes a local SOCKS5 proxy that forwards traffic through the tunnel.
|
||||
- **Credential generation** (`rustunnel generate`) — creates a self-signed CA, server certificate, client certificate, and auth token for a local session.
|
||||
- **Connection keys** (`rustunnel keygen`) — bundles all credential material into a single compressed base64url string that can be copy-pasted between machines.
|
||||
- **Connection keys** (`rustunnel keygen`) — a short ~49-char seed from which both endpoints derive identical TLS material and which can be copy-pasted between machines.
|
||||
|
||||
## Quick links
|
||||
|
||||
|
||||
@@ -4,18 +4,13 @@
|
||||
|
||||
```rust
|
||||
pub struct ConnectionKey {
|
||||
pub version: u8, // must be 2
|
||||
pub target: String, // listener address
|
||||
pub ca_cert_pem: String,
|
||||
pub server_cert_pem: String,
|
||||
pub server_key_pem: String,
|
||||
pub client_cert_pem: String,
|
||||
pub client_key_pem: String,
|
||||
pub auth_token: String,
|
||||
seed: [u8; 32], // random seed from which all TLS material is derived
|
||||
}
|
||||
```
|
||||
|
||||
Encoded as: `rtun2.` + base64url(deflate(JSON)) — no padding.
|
||||
Encoded as: `rtun3.` + base64url(32-byte seed) — no padding (~49 chars). No
|
||||
certificates are stored in the key; both endpoints derive the same CA and
|
||||
ephemeral leaves from the seed (`src/keyderive.rs`).
|
||||
|
||||
## Config
|
||||
|
||||
|
||||
@@ -26,9 +26,8 @@
|
||||
| `hyper-util` | 0.1 | HTTP utilities |
|
||||
| `http-body-util` | 0.1 | HTTP body utilities |
|
||||
| `tower-service` | 0.3 | Service trait (Hyper ecosystem) |
|
||||
| `base64` | 0.22 | Connection key encoding |
|
||||
| `miniz_oxide` | 0.8 | DEFLATE compression for connection keys |
|
||||
| `sha2` | 0.10 | Certificate fingerprinting |
|
||||
| `base64` | 0.22 | Connection key seed encoding |
|
||||
| `sha2` | 0.10 | Auth-token derivation from the seed |
|
||||
| `bytes` | 1 | Byte buffers for framing |
|
||||
| `futures` | 0.3 | Future utilities |
|
||||
| `url` | 2 | URL parsing |
|
||||
|
||||
+11
-2
@@ -21,11 +21,20 @@ The E2E test `auth_required_in_addition_to_mtls` verifies that valid mTLS alone
|
||||
|
||||
## Auth token
|
||||
|
||||
- Generated as 32 random bytes encoded as 64 hex characters.
|
||||
- Derived from the connection key seed as `sha256(seed || "rustunnel-auth-token")`
|
||||
(first 16 bytes, hex), or 32 random bytes as 64 hex characters for the file-based path.
|
||||
- Compared with a constant-time XOR loop to prevent timing attacks.
|
||||
- Never logged in raw form. All log output uses the `Redacted` wrapper.
|
||||
- Auth failures are terminal: the connector's reconnect loop exits on auth failure rather than retrying.
|
||||
|
||||
## Connection key as seed
|
||||
|
||||
- The connection key is a 32-byte seed (prefix `rtun3.`). It confers full access —
|
||||
possession of the key lets you derive the same CA and ephemeral leaves, so treat it
|
||||
as a secret.
|
||||
- Both endpoints derive the same CA; a connector using a different seed cannot
|
||||
authenticate to a listener (verified by `e2e_wrong_seed_key_is_rejected`).
|
||||
|
||||
## Secret redaction
|
||||
|
||||
- `Redacted` in `src/redact.rs` wraps strings and displays `[REDACTED(len=N)]`.
|
||||
@@ -33,7 +42,7 @@ The E2E test `auth_required_in_addition_to_mtls` verifies that valid mTLS alone
|
||||
- `redact_config_json()` scrubs sensitive keys from JSON.
|
||||
- `is_sensitive()` detects PEM private key blocks.
|
||||
- `is_auth_token()` detects long alphanumeric strings.
|
||||
- `is_connection_key()` detects strings starting with `rtun2.`.
|
||||
- `is_connection_key()` detects strings starting with `rtun3.`.
|
||||
|
||||
The E2E test `ops_auth_failure_is_actionable_and_redacted` verifies that error messages do not contain raw token values.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user