repo scaffold + AGENTS.md + Phase 0 research
- Cargo scaffold: feature gates (quinn/tcp/acme), lean tokio subset, placeholder lib; Cargo.lock committed with time pinned to 0.3.36 so rust-version = 1.85 is actually satisfiable (rcgen's default time resolution requires 1.88 — alknet-tls fails the same check) - AGENTS.md adapted from alktunnels: TLS-crate conventions (behavior- preservation invariants, fail-closed verifier selection, one ACME state machine, config-construction scope boundary, no wasm target) - .opencode/agents: implementation-specialist conventions + coordinator prompt template + architect deferral examples updated for alktls - docs/research/phase-0.md: extraction inventory with verified invariants (line-referenced), spec-vs-code gaps (TlsError shape, for_tcp_tls, config-type ownership), rewrite requirements, OQ-TLS-01..07, MSRV verification record Verified: cargo test, clippy -D warnings, fmt --check, doc --no-deps, test --all-features, rustup run 1.85 cargo check
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target/
|
||||
node_modules/
|
||||
.worktrees/
|
||||
@@ -329,7 +329,7 @@ A decision should be `deferred(scope)` when:
|
||||
- The use case isn't concrete (e.g., "we don't know what the agent crate
|
||||
will need from the call protocol")
|
||||
- The options depend on something that doesn't exist yet (e.g.,
|
||||
"depends on the alknet-http crate spec")
|
||||
"depends on the alknet rewrite's dial-seam shape")
|
||||
- The trade-off requires data that can only come from implementation
|
||||
(e.g., "need performance benchmarks to choose between X and Y")
|
||||
- The decision is genuinely not needed for the current scope (e.g., "the
|
||||
@@ -373,10 +373,10 @@ A decision should be `deferred(unclear)` when:
|
||||
(implies it's decided).
|
||||
2. **State the blocking condition** (`deferred(scope)`) or
|
||||
**investigation target** (`deferred(unclear)`) — what specific thing
|
||||
would unblock this? Be concrete: "blocked on: alknet-agent crate spec
|
||||
exists" or "investigation: work through 2+ example outbound-dial use
|
||||
cases (hub→worker, worker→hub) to see how verifier-selection +
|
||||
provider + connector compose."
|
||||
would unblock this? Be concrete: "blocked on: the alknet rewrite's
|
||||
dial-seam shape exists" or "investigation: work through 2+ example
|
||||
outbound-dial use cases (hub→worker, worker→hub) to see how
|
||||
verifier-selection + provider + connector compose."
|
||||
3. **State the impacts** — what does this block downstream? Be
|
||||
specific: "blocks the first hub deployment because the hub dials
|
||||
workers" not "blocks the hub crate." This is the triage signal that
|
||||
|
||||
@@ -191,7 +191,7 @@ also include:
|
||||
Example prompt template:
|
||||
|
||||
```
|
||||
You are an implementation specialist for the @alkdev/alknet project.
|
||||
You are an implementation specialist for the @alkdev/alktls project.
|
||||
|
||||
Your task: {{task}}
|
||||
|
||||
@@ -204,13 +204,14 @@ Your task: {{task}}
|
||||
7. Push: git push origin $(git branch --show-current)
|
||||
8. Notify: worktree({action: "notify", args: {message: "Task completed: {{task}}. <brief summary>", level: "info"}})
|
||||
|
||||
Key project constraints (@alkdev/alknet):
|
||||
Key project constraints (@alkdev/alktls):
|
||||
- Rust: use cargo build, cargo clippy, cargo fmt, cargo test
|
||||
- No comments in code
|
||||
- anyhow::Result for application errors, thiserror for library error types
|
||||
- Feature flags for transports (tls, iroh, acme)
|
||||
- Async via tokio runtime
|
||||
- No panics in library code
|
||||
- thiserror for the library error type (`TlsError`, `#[non_exhaustive]`)
|
||||
- Feature gates for transports (quinn, tcp, acme)
|
||||
- Async via tokio runtime (no `features = ["full"]` in dependencies)
|
||||
- No panics in library code; no unwrap/expect outside tests
|
||||
- Behavior-preservation invariants are load-bearing (see AGENTS.md §5)
|
||||
```
|
||||
|
||||
### Partial Generation Spawning
|
||||
|
||||
@@ -211,69 +211,63 @@ Read `AGENTS.md` at project root for full details. Key rules:
|
||||
|
||||
1. **No comments in code** — Per project convention. Doc comments (`///`, `//!`)
|
||||
are fine and expected on public API. Inline `//` comments only when the user
|
||||
asks or when a non-obvious safety/correctness constraint would otherwise be
|
||||
missed (e.g., "a two-pump tunnel must shut down the opposite sink on pump
|
||||
completion — `try_join!` alone deadlocks").
|
||||
2. **Error handling** — `thiserror` for library error types (`TunnelError`;
|
||||
`HandlerError`/`StreamError` come from alkcall::core). No panics in library
|
||||
code. No `unwrap()` or `expect()` outside tests. For poisoned
|
||||
`RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())`.
|
||||
3. **`tokio` is the async runtime** — all I/O is async. The tunnel pumps, the
|
||||
channels integration, and the consumer session type are all async. Use
|
||||
`tokio::sync` primitives (`oneshot`, `mpsc`) for lifecycle correlation and
|
||||
per-direction data flow.
|
||||
4. **WASM target is load-bearing** — the default crate (protocol-only) MUST
|
||||
compile to `wasm32-unknown-unknown`. Use the wasm-clean tokio subset
|
||||
(`rt`, `sync`, `io-util`, `macros`, `time`); **do NOT use
|
||||
`features = ["full"]`**. Substrate backends (local TCP/UDP sockets, process
|
||||
listeners) are feature-gated and never imported from the
|
||||
shared/producer/consumer modules.
|
||||
5. **Wire format is stable** — the tunnel payload rides inside channels data
|
||||
channels as raw bytes (channels strips its 8-byte header transparently; the
|
||||
tunnel protocol owns whatever framing it puts inside the `BiStream`). Any
|
||||
negotiation/setup frame is self-contained (length-prefixed JSON per alktty
|
||||
ADR-006 precedent), not alkcall's `EventEnvelope` framing. Wire-format
|
||||
changes after the first consumer are additive-only.
|
||||
6. **Producer/consumer, not server/client** — both sides of a channels
|
||||
connection can initiate. Use "producer"/"consumer" or "accept side"/"connect
|
||||
side," not "server"/"client."
|
||||
7. **Substrate-agnostic by construction** — the protocol layer must not know
|
||||
whether bytes come from TCP, UDP, a Unix socket, or stdio. Target
|
||||
addressing, direction, and lifecycle bookkeeping must not hardcode a
|
||||
substrate.
|
||||
8. **Two-pump shutdown-on-completion is a contract** — each pump MUST shut
|
||||
down the opposite sink when it completes; `tokio::try_join!` alone
|
||||
deadlocks (POC-validated, alknet ADR-078). Emit EOF sentinels (zero-length
|
||||
chunks) on clean sink shutdown.
|
||||
9. **No forced local binding** — a tunnel must not require the producer (or
|
||||
consumer) to bind a local port. Support both SSH `-L` and `-R` style
|
||||
directions and unbound/listen-optional flows; the binding decision belongs
|
||||
to the caller.
|
||||
10. **Backpressure and limits are inherited** — bounded per-channel buffers,
|
||||
the 256-channel cap, monotonic IDs, and zero-length-sentinel EOF are
|
||||
alkcall channels invariants. Do not build a second demux/mux or re-derive
|
||||
limits.
|
||||
11. **Vendored core types come from alkcall** — `Connection`,
|
||||
`ProtocolHandler`, `BiStream`, `BidiStreamSource`, `AuthContext`,
|
||||
`Identity`, `IdentityProvider`, `AccessControl`, `OwnershipProvider`,
|
||||
`HandlerError`, `StreamError` come from `alkcall::core`. Do not vendor
|
||||
copies. Pin `alkcall = "0.4.0"`; bump deliberately; fix issues upstream.
|
||||
12. **BAST document for the wire format** — binary framing (if any beyond
|
||||
pass-through) carries a BAST document under `docs/architecture/`
|
||||
conforming to `https://alk.dev/bast/v1/schema`. alktunnels does not depend
|
||||
on alktype; hand-rolled codecs are fine for trivial formats.
|
||||
13. **Access control** — scope-gate tunnel opens (`TUNNEL_OPEN_SCOPE`, shape
|
||||
following alktty's `TTY_OPEN_SCOPE`); the channels path gets authorization
|
||||
via `ChannelCore::register_openable`, which runs the ACL before the open
|
||||
handler. Optionally consult `OwnershipProvider`
|
||||
(`provider.owns(id_ref, kind, &id, "tunnel")` — the 4-arg shape).
|
||||
14. **Naming conventions** — Rust standard: `snake_case` for functions/variables/
|
||||
asks or when a non-obvious security/correctness constraint would otherwise be
|
||||
missed (e.g., "the root store must never be empty — a container with no
|
||||
system CA bundle still needs to verify public X.509 remotes; alknet
|
||||
ADR-088 §5").
|
||||
2. **Error handling** — `thiserror` for the library error type (`TlsError`,
|
||||
`#[non_exhaustive]`, one variant per failure category — the alknet ADR-088
|
||||
shape). No panics in library code. No `unwrap()` or `expect()` outside
|
||||
tests. For poisoned `RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())`.
|
||||
3. **`tokio` is the async runtime** — all I/O is async. The ACME state machine
|
||||
is a spawned task; cert loading is sync file I/O behind an `async fn`
|
||||
signature for API uniformity. Use the wasm-clean tokio subset
|
||||
(`rt`, `sync`, `macros`); **do NOT use `features = ["full"]`** in
|
||||
`[dependencies]` (dev-dependencies may use `full`).
|
||||
4. **The default crate stays lean** — TLS setup and config types only.
|
||||
Transport-specific wrapping is feature-gated: `quinn` (the `for_quinn()`
|
||||
accessors), `tcp` (`tokio-rustls`), `acme` (the ACME state machine, a heavy
|
||||
dep). The `rustls` dep is always present. Wasm is not a load-bearing target
|
||||
here (crypto stacks and file I/O are platform code).
|
||||
5. **Behavior-preservation invariants are load-bearing** — `max_early_data_size
|
||||
= u32::MAX` on all server config paths (0-RTT); `aws_lc_rs::default_provider()`
|
||||
on all paths (alknet ADR-084); `AcceptAnyCertVerifier::supported_verify_schemes()`
|
||||
returns ED25519 + ECDSA P-256/P-384 + RSA PSS/PKCS1 verbatim; `acme-tls/1`
|
||||
ALPN appended by the crate for the ACME path only (alknet ADR-027 §7);
|
||||
non-empty root store (merge `webpki-roots` when the platform store is
|
||||
empty — alknet ADR-088 §5).
|
||||
6. **Fail closed** — verifier selection (fingerprint pin / CA / fail closed)
|
||||
must never silently downgrade. Known peer + fingerprint → pin; unknown +
|
||||
X.509 → CA; unknown + raw key → fail closed at handshake (alknet ADR-034).
|
||||
Client-auth cert presentation follows the local identity; `Acme` is a
|
||||
server-only identity (config error on the client path).
|
||||
7. **One identity, N transports; one ACME state machine** — `TlsServerConfig` /
|
||||
`TlsClientConfig` are built once and shared (the inner rustls config is
|
||||
Clone — Arc-shared resolvers). `TlsServerConfig` is not `Clone` (it holds
|
||||
the ACME task's `JoinHandle`); share via `Arc`. Never spawn a second ACME
|
||||
state machine for a domain already being served (alknet ADR-082 §The
|
||||
cert-reuse problem). This crate is the cert provider, not the accept loop.
|
||||
8. **TLS-crate scope boundary** — this crate owns config *construction*.
|
||||
Handshake-time outcomes flow through the transport's connector, not through
|
||||
`TlsError` (alknet ADR-088 §6). ACME runtime errors are stream events,
|
||||
logged in the spawned task. Do not grow `TlsError` to cover handshake
|
||||
outcomes.
|
||||
9. **Feature gates** — transport-specific deps are opt-in (`quinn`, `tcp`,
|
||||
`acme`). Verify `cargo test` (default) and `cargo test --all-features`
|
||||
both pass whenever features are touched.
|
||||
10. **Upstream posture** — this crate extracts working code from alknet
|
||||
(`crates/alknet-tls`, `crates/alknet-core` config/fingerprint). The alknet
|
||||
ADRs and the `crates/tls` README spec are the reference; where this crate
|
||||
deviates, record the deviation as an ADR here rather than silently
|
||||
diverging. Config types (`TlsIdentity`, `Ed25519SecretKey`) are expected
|
||||
to move here from `alknet-core`; do not re-import them from alknet.
|
||||
11. **Naming conventions** — Rust standard: `snake_case` for functions/variables/
|
||||
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants.
|
||||
15. **Module structure** — one module per file under `src/`, re-exported from
|
||||
`src/lib.rs`. Public API surface is `lib.rs` re-exports. Producer half
|
||||
(adapter / open-handler), consumer half (typed session/client), shared
|
||||
wire/target-addressing modules; backends feature-gated and never imported
|
||||
from shared/producer/consumer modules.
|
||||
12. **Module structure** — one module per file under `src/`, re-exported from
|
||||
`src/lib.rs`. Public API surface is `lib.rs` re-exports. The alknet shape
|
||||
is the reference: `server.rs` (server config + resolvers), `client.rs`
|
||||
(client config + verifiers), `pem.rs` (cert/key loading), `signing.rs`
|
||||
(shared signing helpers).
|
||||
|
||||
## Key Principles
|
||||
|
||||
|
||||
209
AGENTS.md
Normal file
209
AGENTS.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# AGENTS.md
|
||||
|
||||
Operating instructions for opencode agents working in this repo. opencode
|
||||
auto-loads this file as instructions, overriding the built-in defaults for
|
||||
this project. Custom agents in `.opencode/agents/` inherit these rules
|
||||
unless their own prompts say otherwise.
|
||||
|
||||
## Git Workflow
|
||||
|
||||
**Commit and push when reasonable.** When a change is complete and
|
||||
verified (build + lint + tests pass), commit and push to `origin/main`
|
||||
without asking. This overrides the built-in default of "only commit when
|
||||
explicitly asked."
|
||||
|
||||
The workflow:
|
||||
|
||||
1. Make the change
|
||||
2. Verify: `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check`, `cargo doc --no-deps` if docs changed
|
||||
3. Inspect `git status` and `git diff` before staging — stage only the
|
||||
intended files, never secrets
|
||||
4. Write a concise commit message matching the repo style (see `git log
|
||||
--oneline -10`). For multi-point changes, use a summary line plus a
|
||||
body with bullet points and a verification block.
|
||||
5. `git push origin main`
|
||||
6. Report the commit hash and the verification summary
|
||||
|
||||
Exceptions — **do not** commit or push without asking:
|
||||
|
||||
- The change is exploratory / speculative (you're not sure the user wants
|
||||
it kept)
|
||||
- The user is actively reviewing the diff and may ask for changes
|
||||
- The change touches a public API surface that consumers implement or
|
||||
compile against (one-way doors — see "Public API shapes are one-way
|
||||
doors" below; once consumers exist, those signatures are stable
|
||||
contracts)
|
||||
- You'd be force-pushing, amending a published commit, creating an empty
|
||||
commit, or skipping hooks
|
||||
|
||||
Never commit secrets, keys, or credentials. If a commit fails or hooks
|
||||
reject it, fix the issue and create a new commit — do not amend the
|
||||
failed one.
|
||||
|
||||
Git identity is preconfigured (`glm-5.3-flash <glm-5.3-flash@alk.dev>`). Do
|
||||
not change `git config`, skip hooks, or use `git commit -i`.
|
||||
|
||||
## Project Conventions (Rust / TLS crate)
|
||||
|
||||
This is the TLS crate — shared TLS setup types for the alk* stack:
|
||||
server and client `rustls` configs, cert resolvers, verifiers, and
|
||||
ACME state-machine wiring, transport-agnostic and shareable across
|
||||
transports (quinn, `tokio-rustls` TCP+TLS, and anything else that
|
||||
consumes a `rustls` config). It is the extraction of the TLS handling
|
||||
from alknet (alknet ADR-082/087/088 and the `crates/tls` spec are the
|
||||
prior art). The conventions below apply to all work in `src/` and
|
||||
`tests/`. They mirror `.opencode/agents/implementation-specialist.md`
|
||||
§Project Conventions and are repeated here so they apply to every
|
||||
session, not just spawned implementation agents.
|
||||
|
||||
1. **No comments in code** unless the user explicitly asks. This is a
|
||||
project-wide convention. Doc comments (`///`, `//!`) are fine and
|
||||
expected on public API. Inline `//` comments only when the user asks
|
||||
or when a non-obvious security/correctness constraint would otherwise
|
||||
be missed (e.g., "the root store must never be empty — a container
|
||||
with no system CA bundle still needs to verify public X.509 remotes;
|
||||
see alknet ADR-088 §5").
|
||||
|
||||
2. **Error handling** — `thiserror` for the library error type
|
||||
(`TlsError`, `#[non_exhaustive]`, one variant per failure category —
|
||||
the alknet ADR-088 shape). No panics in library code. No `unwrap()`
|
||||
or `expect()` outside tests. If you reach for `unwrap`, the error
|
||||
path wasn't specified — stop and decide what should actually happen.
|
||||
For poisoned `RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())`
|
||||
so a panic in one operation does not cascade to other operations.
|
||||
|
||||
3. **`tokio` is the async runtime** — all I/O is async. The ACME state
|
||||
machine is a spawned task; cert loading is sync file I/O behind an
|
||||
`async fn` signature for API uniformity. Use the wasm-clean tokio
|
||||
subset (`rt`, `sync`, `macros`) — **do NOT use `features = ["full"]`**
|
||||
in `[dependencies]` (dev-dependencies may use `full`).
|
||||
|
||||
4. **The default crate should stay lean** — TLS setup and config types
|
||||
only. Transport-specific wrapping is feature-gated: `quinn` (the
|
||||
`for_quinn()` accessors), `tcp` (`tokio-rustls`), `acme` (the ACME
|
||||
state machine, a heavy dep). The `rustls` dep is always present —
|
||||
it is the core library. Unlike the sibling protocol crates
|
||||
(alktunnels, alktty), wasm is not a load-bearing target here: the
|
||||
crypto stacks (`aws-lc-rs`) and file I/O are platform code. If a
|
||||
design decision ever makes the default crate heavier than "rustls +
|
||||
cert material + tokio spawn," that is an ADR-worthy decision, not an
|
||||
accident.
|
||||
|
||||
5. **Behavior-preservation invariants are load-bearing** — the extraction
|
||||
from alknet must preserve these TLS behaviors exactly. An
|
||||
implementation that omits any of these compiles and passes
|
||||
type-checks but silently changes TLS behavior (alknet ADR-082 §
|
||||
Behavior-preservation invariants):
|
||||
- **`max_early_data_size = u32::MAX`** on all server config paths —
|
||||
enables 0-RTT / early data. Omitting it silently breaks 0-RTT
|
||||
clients.
|
||||
- **`rustls::crypto::aws_lc_rs::default_provider()`** as the crypto
|
||||
provider on all paths (alknet ADR-084). Do not switch to `ring` or
|
||||
the process-default provider without an ADR.
|
||||
- **`AcceptAnyCertVerifier`'s `supported_verify_schemes()`** returns
|
||||
ED25519 + ECDSA P-256/P-384 + RSA PSS/PKCS1 (SHA256/384/512),
|
||||
verbatim.
|
||||
- **`acme-tls/1` ALPN append** for the ACME path only, done by the
|
||||
TLS crate, not the caller (alknet ADR-027 §7).
|
||||
- **Non-empty root store** — the client CA path merges
|
||||
`webpki-roots` when the platform store is empty (alknet ADR-088
|
||||
§5); a containerized deployment with no system CA bundle must
|
||||
still verify public X.509 remotes.
|
||||
|
||||
6. **Fail closed** — verifier selection (fingerprint pin / CA / fail
|
||||
closed) must never silently downgrade. An unknown raw-key remote
|
||||
fails at handshake, not to CA verification. Identity precedence and
|
||||
verifier selection follow the alknet ADR-034/ADR-091 shape: known
|
||||
peer + fingerprint → pin; unknown + X.509 → CA; unknown + raw key →
|
||||
fail closed. Client-auth cert presentation follows the local
|
||||
identity: raw key/X.509 present the cert, `None` presents nothing,
|
||||
`Acme` is a server-only identity (config error on the client path).
|
||||
|
||||
7. **One identity, N transports; one ACME state machine** — the central
|
||||
types are `TlsServerConfig` / `TlsClientConfig`, built once and
|
||||
shared (the inner `rustls` config is Clone — Arc-shared resolvers).
|
||||
`TlsServerConfig` is not `Clone` (it holds the ACME task's
|
||||
`JoinHandle`); share it via `Arc`. Never spawn a second ACME state
|
||||
machine for a domain already being served — duplicate orders risk
|
||||
Let's Encrypt rate limits and cert-cache divergence (alknet ADR-082
|
||||
§The cert-reuse problem). This crate is the cert provider, not the
|
||||
accept loop — transport accept loops live in the consumers
|
||||
(assembly layer / endpoint crates).
|
||||
|
||||
8. **TLS-crate scope boundary** — this crate owns config *construction*.
|
||||
Handshake-time outcomes (a rejected cert, the unknown-raw-key
|
||||
fail-closed) flow through the transport's connector, not through
|
||||
`TlsError` — `TlsError` is the config-construction error type (alknet
|
||||
ADR-088 §6). ACME state-machine runtime errors are stream events,
|
||||
logged in the spawned task, not `TlsError` variants. Do not grow
|
||||
`TlsError` to cover handshake outcomes.
|
||||
|
||||
9. **Feature gates** — transport-specific deps are opt-in (`quinn`,
|
||||
`tcp`, `acme`). The base crate compiles lean. Verify `cargo test`
|
||||
(default) and `cargo test --all-features` both pass whenever features
|
||||
are touched.
|
||||
|
||||
10. **Upstream posture** — this crate extracts working code from alknet
|
||||
(`crates/alknet-tls`, `crates/alknet-core` config/fingerprint). The
|
||||
alknet spec docs (ADR-082/083/084/086/087/088/089 and the
|
||||
`crates/tls` README) are the reference; where this crate deviates,
|
||||
record the deviation as an ADR here rather than silently diverging.
|
||||
Config types (`TlsIdentity`, `Ed25519SecretKey`) are expected to
|
||||
move here from `alknet-core` — this crate owns them after the
|
||||
rewrite; do not re-import them from alknet.
|
||||
|
||||
11. **Naming** — Rust standard: `snake_case` for functions/variables/
|
||||
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for
|
||||
constants.
|
||||
|
||||
12. **Module structure** — one module per file under `src/`, re-exported
|
||||
from `src/lib.rs`. Public API surface is `lib.rs` re-exports. The
|
||||
alknet shape is the reference: `server.rs` (server config +
|
||||
resolvers), `client.rs` (client config + verifiers), `pem.rs`
|
||||
(cert/key loading), `signing.rs` (shared signing helpers).
|
||||
|
||||
## Verification Commands
|
||||
|
||||
Run these before committing. All must pass.
|
||||
|
||||
```bash
|
||||
cargo test # full suite
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
cargo fmt --check
|
||||
cargo doc --no-deps # if docs changed
|
||||
cargo test --all-features # if features are added/touched
|
||||
cargo publish --dry-run --allow-dirty # before a release
|
||||
```
|
||||
|
||||
## Architecture Context
|
||||
|
||||
- `docs/research/phase-0.md` — the Phase 0 research findings (current
|
||||
phase). Phase 1 will produce `docs/architecture/` (overview, ADRs,
|
||||
open-questions tracker) per `docs/sdd_process.md`.
|
||||
- The prior art for this crate:
|
||||
- **alknet-tls** — `/workspace/@alkdev/alknet/crates/alknet-tls/` —
|
||||
the working extraction source (`server.rs`, `client.rs`, `pem.rs`,
|
||||
`signing.rs`); this crate supersedes it in the alknet rewrite.
|
||||
- **alknet docs** — `/workspace/@alkdev/alknet/docs/architecture/`:
|
||||
ADR-082 (extraction), ADR-083 (endpoint takes no TLS config),
|
||||
ADR-084 (aws-lc-rs provider), ADR-027 (identity model, `acme-tls/1`),
|
||||
ADR-086 (endpoint types / split ALPN lists), ADR-087
|
||||
(`TlsClientConfig`), ADR-088 (`TlsError` shape, root-store
|
||||
fallback), ADR-089 (client dial seam), ADR-034 (verifier
|
||||
selection), ADR-091 (`ConnectionCredentials`), and
|
||||
`docs/architecture/crates/tls/README.md` (the full crate spec —
|
||||
the most complete reference for what this crate must do).
|
||||
- Key open threads carried from alknet (Phase 0 must resolve or defer
|
||||
them — see `docs/research/phase-0.md`):
|
||||
- The `TlsError` in the extracted code is the simplified 3-variant
|
||||
enum; the ADR-088 six-variant `#[non_exhaustive]` shape is the
|
||||
recorded target and was never implemented.
|
||||
- The config types (`TlsIdentity`, `Ed25519SecretKey`,
|
||||
`ConnectionCredentials`) still live in `alknet-core`; where they
|
||||
land for the rewrite is an alktls-side decision.
|
||||
- ADR-029's fingerprint question (`029-callclient-tls-client-auth...`)
|
||||
and the client-auth/remote-identity verification seams.
|
||||
- If a TODO references a design direction that an ADR has since decided
|
||||
against, the TODO is stale — remove it and align with the ADR. Do not
|
||||
implement the rejected design.
|
||||
2097
Cargo.lock
generated
Normal file
2097
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
Cargo.toml
Normal file
43
Cargo.toml
Normal file
@@ -0,0 +1,43 @@
|
||||
[package]
|
||||
name = "alktls"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.85"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Shared TLS setup types: server and client rustls configs, cert resolvers, verifiers, and ACME state-machine wiring, transport-agnostic and shareable across transports."
|
||||
repository = "https://git.alk.dev/alkdev/alktls"
|
||||
keywords = ["tls", "rustls", "acme", "quinn", "network"]
|
||||
categories = ["network-programming", "cryptography", "asynchronous"]
|
||||
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/research/", "docs/plans/", "docs/sdd_process.md"]
|
||||
|
||||
[lib]
|
||||
name = "alktls"
|
||||
|
||||
[features]
|
||||
default = ["quinn"]
|
||||
quinn = ["dep:quinn"]
|
||||
tcp = ["dep:tokio-rustls"]
|
||||
acme = ["dep:rustls-acme"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", default-features = false, features = ["rt", "sync", "macros"] }
|
||||
rustls = { version = "0.23", features = ["aws_lc_rs"] }
|
||||
rustls-pki-types = "1"
|
||||
rustls-pemfile = "2"
|
||||
rustls-native-certs = "0.8"
|
||||
webpki-roots = "0.26"
|
||||
rcgen = "0.13"
|
||||
ed25519-dalek = "2"
|
||||
sha2 = "0.10"
|
||||
tracing = "0.1"
|
||||
thiserror = "2"
|
||||
|
||||
futures = { version = "0.3", optional = true }
|
||||
quinn = { version = "0.11", optional = true }
|
||||
tokio-rustls = { version = "0.26", optional = true }
|
||||
rustls-acme = { version = "0.12", optional = true, features = ["aws-lc-rs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full", "test-util", "macros"] }
|
||||
tempfile = "3"
|
||||
hex = "0.4"
|
||||
192
LICENSE-APACHE
Normal file
192
LICENSE-APACHE
Normal file
@@ -0,0 +1,192 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name), or refer to, the Work.
|
||||
(Note: Derivative Works shall not include works that remain separable from,
|
||||
or merely link (or bind by name) to the interfaces of, the Work and
|
||||
Derivative Works thereof.)
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by the Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2025-2026 Alk Development
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
21
LICENSE-MIT
Normal file
21
LICENSE-MIT
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025-2026 Alk Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
480
docs/research/phase-0.md
Normal file
480
docs/research/phase-0.md
Normal file
@@ -0,0 +1,480 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-09
|
||||
---
|
||||
|
||||
# alktls — Phase 0 Research Findings
|
||||
|
||||
This document captures Phase 0 (Exploration) findings and open design
|
||||
questions for the `alktls` crate. The objective of Phase 0 per
|
||||
`docs/sdd_process.md` is: *"Capture vision and guiding principles; research
|
||||
options; validate approaches; converge on a recommended approach."* It is the
|
||||
input to Phase 1 (Architecture), where the Architect will produce
|
||||
`docs/architecture/` specs, ADRs, and open questions.
|
||||
|
||||
Drafted 2026-09-09. The crate is the TLS layer of the alk* stack: shared TLS
|
||||
setup types — server and client `rustls` configs, cert resolvers, verifiers,
|
||||
and ACME state-machine wiring — transport-agnostic and shareable across
|
||||
transports (quinn, `tokio-rustls` TCP+TLS, and anything else that consumes a
|
||||
`rustls` config).
|
||||
|
||||
Unlike alktunnels (which was entirely new protocol code), this crate is an
|
||||
**extraction** of working, battle-tested TLS code from alknet. The prior art
|
||||
is unusually strong: `crates/alknet-tls` exists and runs in production today,
|
||||
and the alknet architecture docs (ADR-082/083/084/086/087/088/089 plus the
|
||||
`crates/tls` README spec) already record most of the design rationale. Phase
|
||||
0's job is therefore different in character from alktunnels': mostly
|
||||
**inventory and gap analysis** — what already works, what the spec pinned but
|
||||
the code never implemented, what must change when the config types move from
|
||||
`alknet-core` into this crate, and what the alknet rewrite needs this crate
|
||||
to own. Expect this document to be shorter on open questions than
|
||||
alktunnels' Phase 0 was.
|
||||
|
||||
## What is already settled
|
||||
|
||||
The design is ADR-pinned in alknet; this crate inherits it. It does not
|
||||
re-litigate these decisions — it implements them. The summaries below are
|
||||
pointers; read the referenced ADRs for the full rationale.
|
||||
|
||||
- **The extraction shape (alknet ADR-082).** TLS setup is a standalone
|
||||
crate because a `rustls::ServerConfig` built for one transport gets
|
||||
*consumed* into that transport's wrapper (e.g. `quinn::ServerConfig`),
|
||||
making the cert unreusable for a TCP+TLS listener. The fix: build the
|
||||
config once (`TlsServerConfig`), share it via `Arc`, clone the cheap
|
||||
inner rustls config per transport. One cert, one ACME state machine, N
|
||||
transports.
|
||||
- **The cert-reuse problem and the ACME worst case (ADR-082 §The
|
||||
cert-reuse problem).** Two ACME state machines for the same domain =
|
||||
duplicate Let's Encrypt orders (rate-limit risk), divergent cert
|
||||
caches, duplicate resolvers. One ACME state machine shared across
|
||||
transports is the only correct design; `TlsServerConfig` is not
|
||||
`Clone` (it holds the ACME task's `JoinHandle`) precisely so callers
|
||||
must share via `Arc`.
|
||||
- **The endpoint takes no TLS config (alknet ADR-083).** A hub serving
|
||||
native clients (raw key) and browsers (X.509/ACME) holds *two*
|
||||
`TlsServerConfig`s — there is no single "the TLS config" for an
|
||||
endpoint to take. The assembly layer builds the configs and the
|
||||
transports; the endpoint just dispatches. This crate is the cert
|
||||
provider, not the accept loop.
|
||||
- **aws-lc-rs as the crypto provider (alknet ADR-084).**
|
||||
`rustls::crypto::aws_lc_rs::default_provider()` on all server and
|
||||
client config paths. Matches iroh's `tls-aws-lc-rs` feature; FIPS-
|
||||
capable. Do not switch to `ring` or the process-default provider
|
||||
without a new ADR.
|
||||
- **The identity model (alknet ADR-027).** `TlsIdentity` is a four-
|
||||
variant enum: `X509 { cert, key }` (paths, loaded from disk),
|
||||
`RawKey(Ed25519SecretKey)` (RFC 7250 raw public key), `SelfSigned`
|
||||
(dev), and `Acme { domains, cache_dir, directory, contact }`
|
||||
(server-only). `Acme` on the client path is a config error.
|
||||
- **`TlsClientConfig` is a first-class deliverable (alknet ADR-087).**
|
||||
Not deferred behind the dial-seam extraction: the TLS config is a
|
||||
*prerequisite* for the dial, not a consequence of it. Centralizes
|
||||
ADR-034 verifier selection and ADR-084 provider wiring for all
|
||||
outbound dials.
|
||||
- **Verifier selection (alknet ADR-034 §3).** Exactly three outcomes,
|
||||
driven by `ConnectionCredentials.remote_identity`: `Some(fingerprint)`
|
||||
→ known peer → `FingerprintPinVerifier` (the fingerprint IS the trust
|
||||
anchor; handshake signatures still verified so a stolen fingerprint
|
||||
can't be replayed with a forged signature); `None` + X.509 → CA
|
||||
verification (`WebPkiServerVerifier`); `None` + raw key → fail closed
|
||||
at handshake (raw-key remotes are always known peers — there is no CA
|
||||
to fall back to). `None` is the public-X.509-endpoint state, **not**
|
||||
"skip verification."
|
||||
- **The `TlsError` shape (alknet ADR-088).** Single `#[non_exhaustive]`
|
||||
enum, one variant per failure category: `CertLoad`, `SelfSigned`,
|
||||
`Rustls`, `VerifierBuild`, `QuinnWrap` (quinn-gated), `AcmeConfig`.
|
||||
Scope boundary (§6): config-construction errors only — handshake-time
|
||||
outcomes flow through the transport's connector, ACME runtime errors
|
||||
are stream events logged in the spawned task.
|
||||
- **Root-store fallback (alknet ADR-088 §5).** The unknown-X.509-remote
|
||||
CA path loads the platform's native root certs; if that store is empty
|
||||
(containerized deployment, no system CA bundle), `webpki-roots` is
|
||||
merged in so the store is never empty. Native-certs *load* errors are
|
||||
logged, not returned.
|
||||
- **Split ALPN lists per endpoint type (alknet ADR-086 §3).** The native
|
||||
config advertises native ALPNs (`alknet/channels`, `alknet/call`); the
|
||||
web config advertises entry-point ALPNs (`h2`, `http/1.1`) +
|
||||
`alknet/channels`; `acme-tls/1` is appended automatically for the ACME
|
||||
path. The assembly layer filters per config — this crate just takes
|
||||
the list.
|
||||
- **The dial seam is elsewhere (alknet ADR-089).** `AlknetClient`
|
||||
(transport-polymorphic dial) consumes `TlsClientConfig` via
|
||||
`for_quinn()` / `into_rustls_config()`; this crate does not dial. The
|
||||
iroh transport is the key-not-config exception on both sides — iroh
|
||||
has its own TLS and takes an `iroh::SecretKey`; there is no
|
||||
`for_iroh()` here.
|
||||
- **Fingerprint normalization (alknet ADR-030 §6).** `ed25519:<hex>` for
|
||||
RFC 7250 raw keys, `SHA256:<hex>` for cert DER — identical across
|
||||
quinn, iroh, and TCP+TLS paths, so a `PeerEntry.fingerprints` entry
|
||||
matches regardless of transport.
|
||||
- **`ConnectionCredentials` (alknet ADR-091).** The dial's credential
|
||||
bundle: `local_identity: Option<TlsIdentity>` (client-auth
|
||||
presentation) + `remote_identity: Option<RemoteIdentity>` (verifier
|
||||
selection). Both `Option`s are load-bearing, not cosmetic — see the
|
||||
verifier-selection bullet above.
|
||||
|
||||
## Prior art: the extracted code (`crates/alknet-tls`)
|
||||
|
||||
The working implementation lives at
|
||||
`/workspace/@alkdev/alknet/crates/alknet-tls/` (~1.2k lines across four
|
||||
modules + tests). Module inventory, current state, and known deltas
|
||||
against the ADR-088 spec:
|
||||
|
||||
| Module | Contents | State |
|
||||
|--------|----------|-------|
|
||||
| `lib.rs` | `TlsError` — simplified 3-variant enum (`Config(String)`, `Io`, `Cert(String)`) | **Diverges from ADR-088** — see the gaps section |
|
||||
| `server.rs` | `TlsServerConfig` (`new`, `new_acme`, `for_quinn`), `build_rustls_server_config`, `RawKeyCertResolver`, `AcceptAnyCertVerifier`, `SelfSignedCert`/`generate_self_signed_cert` | Working; all behavior-preservation invariants present (verified below) |
|
||||
| `client.rs` | `TlsClientConfig` (`new`, `for_quinn`, `into_rustls_config`), `select_server_verifier`, `build_client_auth`, `RawKeyClientCertResolver`, `NoClientCertResolver`, `FingerprintPinVerifier`, `load_platform_root_cert_store` | Working; verifier selection and root-store fallback match the spec |
|
||||
| `pem.rs` | `load_cert_chain`, `load_private_key` | Working; shared by server + client |
|
||||
| `signing.rs` | `Ed25519SigningKey` (rustls `SigningKey` + `Signer` over `ed25519_dalek`) | Working; one copy shared by both sides |
|
||||
|
||||
Consumers in alknet today: `alknet-client` (the QUIC and TCP+TLS dials
|
||||
build a `TlsClientConfig` per dial), `alknet-endpoint` (TCP+TLS accept
|
||||
loop takes a `TlsAcceptor`), and the iroh dial (error-type reuse only).
|
||||
Server-side, the assembly wiring consumes `TlsServerConfig` and its
|
||||
`for_quinn()`; the spec's `for_tcp_tls()` accessor is **not implemented**
|
||||
in the extracted code — the TCP+TLS accept path currently receives a
|
||||
`TlsAcceptor` built elsewhere (the caller wraps
|
||||
`tokio_rustls::TlsAcceptor::from(Arc<rustls::ServerConfig>)` itself).
|
||||
That accessor belongs in this crate (spec §Architecture, ADR-082's API
|
||||
table) — see the gaps section.
|
||||
|
||||
### Behavior-preservation invariants — verified present in the code
|
||||
|
||||
All five load-bearing invariants (ADR-082 §Behavior-preservation
|
||||
invariants, ADR-088 §5) are confirmed in the extracted source:
|
||||
|
||||
- `config.max_early_data_size = u32::MAX` on every server config path —
|
||||
X509 (`server.rs:168`), RawKey (`server.rs:179`), SelfSigned
|
||||
(`server.rs:191`), and the ACME branch (`server.rs:83`). The client
|
||||
sets `config.enable_early_data = true` (`client.rs:36`).
|
||||
- `rustls::crypto::aws_lc_rs::default_provider()` on all paths — server
|
||||
(`server.rs:77`, `server.rs:155`) and client (`client.rs:26`).
|
||||
- `AcceptAnyCertVerifier::supported_verify_schemes()` returns ED25519 +
|
||||
ECDSA P-256/P-384 + RSA PSS (SHA256/384/512) + RSA PKCS1
|
||||
(SHA256/384/512) — nine schemes verbatim (`server.rs:284`).
|
||||
- `acme-tls/1` ALPN appended inside `new_acme` (`server.rs:86`) — the
|
||||
crate appends it, not the caller; only on the ACME path.
|
||||
- Non-empty root store — `load_platform_root_cert_store()` merges
|
||||
`webpki-roots` when the platform store is empty (`client.rs:128-149`);
|
||||
native-certs load errors are logged, not returned.
|
||||
|
||||
Phase 1's port must carry these over unchanged, and the port's test
|
||||
suite should assert them directly (the extracted code's tests already do:
|
||||
`max_early_data_size` assertions, the nine-scheme list, the empty-store
|
||||
fallback).
|
||||
|
||||
### Implementation notes worth carrying forward
|
||||
|
||||
Details discovered in the code that the spec glosses over but any
|
||||
implementation will trip on:
|
||||
|
||||
- **The ACME path is `rustls-acme`'s event stream**, not a bespoke state
|
||||
machine: `AcmeConfig::new(domains).cache(DirCache).directory(url).contact(...)`
|
||||
→ `state.resolver()` wired into the server config →
|
||||
`tokio::spawn`ed loop over `state.next()` matching
|
||||
`EventOk`/`EventError` variants to `tracing` log lines. `new` spawns
|
||||
and returns immediately (does not await the first cert) — handshakes
|
||||
fail transiently until the first order completes. Spec-consistent.
|
||||
- **`new_acme` swallows the resolver's cert absence by design.** The
|
||||
spawned task logs `Order` errors at `warn!` ("will retry") — ACME
|
||||
retry semantics live inside `rustls-acme`; the task exits only when
|
||||
the stream ends.
|
||||
- **`for_quinn()` consumes `self`, not `&self`** in the extracted code
|
||||
(both server and client). The spec sketches `&self` for the server
|
||||
accessor. Minor API question for Phase 1 (see OQ-TLS-04) — consuming
|
||||
`self` is safe for the one-config-per-endpoint-type assembly pattern
|
||||
but prevents building two transports from one `TlsServerConfig`
|
||||
without going through `rustls_config()` manually.
|
||||
- **`FingerprintPinVerifier` verifies handshake signatures.** It is not
|
||||
a "trust any cert with the right fingerprint" bypass: TLS 1.2/1.3
|
||||
signature verification still runs (`verify_tls12_signature` /
|
||||
`verify_tls13_signature`), routing Ed25519 SPKI certs through
|
||||
`verify_tls13_signature_with_raw_key` and everything else through the
|
||||
standard aws-lc-rs-backed verification. A pinned fingerprint without
|
||||
the private key fails the handshake.
|
||||
- **Client-auth resolver selection is identity-driven.**
|
||||
`RawKeyClientCertResolver` auto-detects the raw-key case from the
|
||||
cert DER (an Ed25519 SPKI ⇒ `only_raw_public_keys() == true`), the
|
||||
same resolver type serves X.509 chains (raw-keys flag false),
|
||||
`SelfSigned`/`None` present nothing (`NoClientCertResolver`), and
|
||||
`Acme` is rejected at config time.
|
||||
- **The 3-variant `TlsError` folds typed sources into strings.** Every
|
||||
`map_err(|e| TlsError::Config(e.to_string()))` in the extracted code
|
||||
is a place where ADR-088's typed variants (`Rustls`, `SelfSigned`,
|
||||
`VerifierBuild`, `QuinnWrap`) would preserve the `#[source]` chain.
|
||||
This is the single largest code delta against the spec.
|
||||
- **Tests are in-module `#[cfg(test)]` blocks** covering resolvers,
|
||||
verifier behavior, PEM error paths, signing, and the invariants —
|
||||
worth porting as the seed of this crate's `tests/`, plus integration
|
||||
tests here (a full-crate crate has no workspace to lean on for
|
||||
cross-crate coverage).
|
||||
|
||||
## Gaps: spec-pinned but never implemented
|
||||
|
||||
These are recorded decisions without code, or code diverging from the
|
||||
recorded decision. Phase 1 must close or explicitly re-decide each.
|
||||
|
||||
1. **`TlsError` is the 3-variant simplified enum, not the ADR-088
|
||||
shape.** The six-variant `#[non_exhaustive]` shape (`CertLoad`,
|
||||
`SelfSigned`, `Rustls`, `VerifierBuild`, `QuinnWrap`, `AcmeConfig`)
|
||||
with `#[from]` sources was specced in alknet ADR-088 and never
|
||||
implemented. The ADR-088 README notes the rewrite is a two-way-door
|
||||
change (no external match arms yet). This crate should ship the
|
||||
ADR-088 shape from day one — it is the single highest-value gap
|
||||
closure. Caveat: `CertLoad`'s `#[from] io::Error` relies on
|
||||
`rustls_pemfile` funnelling its non-`std::error::Error` error type
|
||||
into `io::Error` (ADR-088 §Gotchas #2) — verify that holds at the
|
||||
pinned rustls/pemfile versions.
|
||||
2. **`for_tcp_tls()` does not exist.** Both ADR-082's API table and the
|
||||
`crates/tls` README spec `for_tcp_tls() -> tokio_rustls::TlsAcceptor`
|
||||
(feature-gated on `tcp`); the extracted code never added it — the
|
||||
assembly layer wraps the acceptor itself. Decide in Phase 1 whether
|
||||
alktls owns the accessor (spec-conforming) or leaves acceptor
|
||||
wrapping to callers (the current de facto shape). Leaning: own it —
|
||||
it is infallible, tiny, and the spec is unambiguous.
|
||||
3. **The config types still live in `alknet-core`.** `TlsIdentity`,
|
||||
`Ed25519SecretKey` (`config.rs`), `ConnectionCredentials`/
|
||||
`RemoteIdentity` (`credentials.rs`), and `fingerprint.rs` are all in
|
||||
core. This crate is expected to own them after the rewrite
|
||||
(AGENTS.md convention 10) — but they are *config* types that
|
||||
`StaticConfig` and the vault integration also consume, so the
|
||||
ownership boundary needs a deliberate decision, not a mechanical
|
||||
move (OQ-TLS-01). The fingerprint helpers (SHA-256 + manual DER
|
||||
parsing, no rustls dep in production) are a candidate to move
|
||||
wholesale; `PeerEntry`/`AuthPolicy` are not TLS concerns and stay
|
||||
wherever identity resolution lands.
|
||||
4. **`SelfSigned` is server-only in practice.** The extracted client
|
||||
path maps `Some(TlsIdentity::SelfSigned)` to `NoClientCertResolver`
|
||||
(present nothing) — reasonable, but the identity enum doesn't encode
|
||||
the constraint the way `Acme` is encoded (client-path config error).
|
||||
Minor semantic wobble to resolve when the types move (OQ-TLS-02).
|
||||
5. **`tokio` features.** The extracted crate uses `features = ["full"]`
|
||||
(workspace style); this crate pins the lean subset (`rt`, `sync`,
|
||||
`macros`) per AGENTS.md convention 3. Verify `rustls-acme`'s
|
||||
transitive tokio needs don't force a wider set (it brings its own
|
||||
features regardless — the constraint is only on *this* crate's
|
||||
direct dep declaration).
|
||||
|
||||
## What the alknet rewrite needs from this crate
|
||||
|
||||
Context for scoping: alktls is one of the last extractions before the
|
||||
alknet rewrite begins. The rewrite's consumers (new endpoint, new
|
||||
client, assembly layer) will consume this crate instead of
|
||||
`alknet-tls`. Derived requirements:
|
||||
|
||||
- **The public API surface should be the spec's surface**: `new`,
|
||||
`for_quinn`, `for_tcp_tls` (gap #2), `rustls_config` on the server;
|
||||
`new`, `for_quinn`, `into_rustls_config` on the client; `TlsError` in
|
||||
the ADR-088 shape (gap #1). The rewrite is the consumer cutoff — get
|
||||
the surface right *before* it exists.
|
||||
- **No alknet imports.** The crate must compile without `alknet-core`;
|
||||
everything it needs (identity, credentials, fingerprint) either moves
|
||||
in or is re-decided (OQ-TLS-01). This is the structural difference
|
||||
from `alknet-tls`, which imports `alknet-core` throughout.
|
||||
- **Noalknet-net assumptions.** The extracted code is transport-agnostic
|
||||
already (the whole point of ADR-082), so no de-welding work is
|
||||
expected — but the rewrite should confirm nothing in the ACME path or
|
||||
PEM loading reaches for alknet types.
|
||||
- **ALPN naming is the caller's problem.** `alknet/channels`,
|
||||
`alknet/call`, `h2`, `http/1.1` are strings passed in by the assembly
|
||||
layer (ADR-086 §3 split lists); this crate owns only `acme-tls/1`
|
||||
(append-for-ACME). The rewrite's ALPN renames (`alknet/` → `alk/` or
|
||||
otherwise) do not touch this crate.
|
||||
- **Iroh stays key-not-config.** The rewrite's iroh transport reads the
|
||||
Ed25519 secret key directly; `for_iroh()` must not appear here
|
||||
(ADR-082 §Iroh is different).
|
||||
|
||||
## Version and dependency posture
|
||||
|
||||
The extracted crate pins: `rustls 0.23` (aws_lc_rs feature), `quinn
|
||||
0.11`, `tokio-rustls 0.26`, `rustls-acme 0.12` (aws-lc-rs feature),
|
||||
`rustls-native-certs 0.8`, `webpki-roots 0.26`, `rcgen 0.13`,
|
||||
`ed25519-dalek 2`, `thiserror 2`, `tokio 1`. As of 2026-09 the rustls
|
||||
0.23 line is still current (0.23.43, July 2026) — no forced major bump.
|
||||
Phase 1 should re-verify at packaging time and record any bump as a
|
||||
line in the dependency ADR. Dependency shape per the spec: `rustls`
|
||||
always present (the core library); `quinn`/`tcp`/`acme` feature-gated;
|
||||
`rustls-native-certs` + `webpki-roots` always present (the CA path is
|
||||
needed by any client dialing public X.509 endpoints regardless of
|
||||
transport); `futures` acme-gated (the event loop's `StreamExt`).
|
||||
|
||||
**MSRV verification (2026-09-09).** The sibling crates declare
|
||||
`rust-version = "1.85"`, but that claim was never compile-checked
|
||||
against the TLS dependency tree: with default resolution, `rcgen →
|
||||
yasna/time` pulls `time 0.3.5x` which requires rustc 1.88, and both
|
||||
this crate's fresh lock *and alknet's own `alknet-tls`* fail
|
||||
`rustup run 1.85 cargo check` for exactly that reason. Resolution here:
|
||||
pin `time` to `0.3.36` (the last release with an MSRV ≤ 1.85) in
|
||||
`Cargo.lock` and commit the lock — `cargo check` verified under the
|
||||
real 1.85 toolchain. Two standing consequences: (1) the lock is
|
||||
repo-state — `Cargo.toml` alone does not carry the MSRV guarantee, and
|
||||
a future `cargo update` that re-floats `time` silently breaks the
|
||||
1.85 claim; (2) the honest MSRV for the *dependency tree* is 1.85 only
|
||||
with the pin — if Phase 1 later decides `rust-version` should track
|
||||
reality (e.g. 1.88+), that is a one-line change plus unpinning. Either
|
||||
way, the decision is recorded here rather than inherited silently.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Numbered OQ-TLS-01.. so they can be promoted into
|
||||
`docs/architecture/open-questions.md` in Phase 1. Most are small — this
|
||||
is the gap list, not a research agenda.
|
||||
|
||||
### OQ-TLS-01: Where do the config types live, and what moves?
|
||||
|
||||
`TlsIdentity`, `Ed25519SecretKey`, `ConnectionCredentials`,
|
||||
`RemoteIdentity`, and the fingerprint helpers live in `alknet-core`
|
||||
today. AGENTS.md convention 10 says this crate owns them after the
|
||||
rewrite — but `StaticConfig` (core's config struct) holds a
|
||||
`TlsIdentity`, `PeerEntry` fingerprint resolution is auth-layer (not
|
||||
TLS), and the vault derives the local identity at startup.
|
||||
|
||||
Options:
|
||||
|
||||
- **A: move all five into alktls** — `StaticConfig` and the vault
|
||||
depend on alktls for the types. alknet-core grows a dep on alktls (or
|
||||
the rewrite's equivalent of core does). Cleanest ownership; couples
|
||||
core config to the TLS crate.
|
||||
- **B: move identity + fingerprint, keep credentials in core** —
|
||||
`ConnectionCredentials` is a dial-layer bundle (ADR-091); it may
|
||||
belong to the rewrite's dial seam instead.
|
||||
- **C: keep types in core-equivalent, alktls re-exports** — the
|
||||
`alknet-tls` shape exactly; minimal change but leaves the ownership
|
||||
question open forever.
|
||||
|
||||
Considerations: the rewrite is the window to decide this — after
|
||||
consumers exist, moving types is a breaking change. The fingerprint
|
||||
helpers are pure (sha2 + manual DER, no rustls dep) so they can live
|
||||
anywhere; `Ed25519SigningKey` (signing.rs) needs whichever type
|
||||
`Ed25519SecretKey` lands in.
|
||||
|
||||
**Status:** open — lean A (full move) for the identity types +
|
||||
fingerprint, with `ConnectionCredentials` decided alongside the
|
||||
rewrite's dial-seam design (B). Needs the rewrite's core-shape decision
|
||||
to finalize; do not let it block the rest of Phase 1.
|
||||
|
||||
### OQ-TLS-02: `SelfSigned` on the client path — encode or document?
|
||||
|
||||
The extracted code maps `Some(SelfSigned)` client-auth to "present
|
||||
nothing" (`NoClientCertResolver`). `Acme` is a client-path config
|
||||
*error* (encoded), but `SelfSigned`'s client-path meaning is implicit.
|
||||
Options: encode it (`SelfSigned` → config error on client, like Acme),
|
||||
keep present-nothing (documented), or make the dev path present the
|
||||
self-signed cert. Present-nothing matches current behavior and needs no
|
||||
code; encoding the constraint is a type-level nicety.
|
||||
|
||||
**Status:** open, small — lean "keep behavior, document it" unless
|
||||
Phase 1 wants the type-level enforcement.
|
||||
|
||||
### OQ-TLS-03: `for_tcp_tls()` — adopt the spec accessor?
|
||||
|
||||
Gap #2 above. The spec (ADR-082 API table, `crates/tls` README) pins
|
||||
`for_tcp_tls() -> tokio_rustls::TlsAcceptor` feature-gated on `tcp`; the
|
||||
extracted code never implemented it (callers wrap the acceptor
|
||||
themselves from `rustls_config()`).
|
||||
|
||||
**Status:** open, lean adopt (infallible, spec-conforming, one-line).
|
||||
Decide in Phase 1 before the API freezes.
|
||||
|
||||
### OQ-TLS-04: `for_quinn()` — `self` or `&self`?
|
||||
|
||||
The extracted code consumes `self` (server and client); the spec
|
||||
sketches `&self` for the server accessor. `&self` (plus the inner rustls
|
||||
config being `Clone`) allows one `TlsServerConfig` to feed both a quinn
|
||||
endpoint and a TCP+TLS acceptor directly — the ADR-082 story's most
|
||||
literal form. `self` forces callers through `rustls_config()` for the
|
||||
second transport. Note `TlsClientConfig::for_quinn(self)` is natural
|
||||
(one dial = one config build), so the two accessors need not agree.
|
||||
|
||||
**Status:** open, small — lean `&self` on the server accessor to match
|
||||
the spec sketch, `self` on the client (consumed by `into_rustls_config`
|
||||
pattern anyway).
|
||||
|
||||
### OQ-TLS-05: Test surface for the invariants
|
||||
|
||||
The behavior-preservation invariants deserve direct tests (the extracted
|
||||
code has them in-module). Questions: port the in-module tests as-is,
|
||||
split into `tests/` integration tests (no workspace consumers to cover
|
||||
here, so integration tests are the only cross-module surface), or both?
|
||||
Also: does the invariant test for `supported_verify_schemes` pin the
|
||||
exact nine-scheme list (regression-proof) or just non-empty + ED25519
|
||||
present (the current extracted test)?
|
||||
|
||||
**Status:** open, small — the alktunnels precedent (coverage
|
||||
weak-spot test commits late in the cycle) suggests porting in-module
|
||||
tests early and adding exact-list pins; not a blocker.
|
||||
|
||||
### OQ-TLS-06: Does the ACME state machine need a shutdown surface?
|
||||
|
||||
`TlsServerConfig` holds the ACME task's `JoinHandle` (that is why it is
|
||||
not `Clone`), but nothing in the extracted code ever aborts the task —
|
||||
the handle is stored and dropped (dropping a `JoinHandle` detaches).
|
||||
For a long-lived hub that is fine (the state machine should run for the
|
||||
process lifetime); for tests and graceful-shutdown stories, a
|
||||
`shutdown()` (abort + await) might be wanted. Spec is silent.
|
||||
|
||||
**Status:** open, small — decide whether v1 ships detached-only
|
||||
(document) or a shutdown surface (code). Lean detached-only for v1;
|
||||
revisit with the rewrite's graceful-shutdown design.
|
||||
|
||||
### OQ-TLS-07: iroh relationship in the rewrite
|
||||
|
||||
The extracted client's iroh dial only reuses `TlsError` for error
|
||||
shaping; the server-side iroh path reads `Ed25519SecretKey` directly.
|
||||
The rewrite may re-shape how iroh consumes the identity (especially if
|
||||
OQ-TLS-01 moves the types). No code needed here beyond keeping
|
||||
`Ed25519SecretKey` (or its successor) constructible from raw bytes and
|
||||
`Clone` — but the rewrite's iroh dial design should be checked against
|
||||
whatever OQ-TLS-01 decides.
|
||||
|
||||
**Status:** open, deferred-shaped — blocked on the rewrite's dial/iroh
|
||||
design landing; track so the identity type keeps iroh's needs
|
||||
(`as_bytes()`-style access) in view.
|
||||
|
||||
## Survey / prior-art list
|
||||
|
||||
All internal; this crate has no external research debt:
|
||||
|
||||
- `crates/alknet-tls/` — the extraction source (module inventory §Prior
|
||||
art). Authoritative for behavior; its tests are the seed test suite.
|
||||
- `crates/alknet-core/src/config.rs` — `TlsIdentity`,
|
||||
`Ed25519SecretKey`, `AcmeDirectory` (OQ-TLS-01 scope).
|
||||
- `crates/alknet-core/src/credentials.rs` — `ConnectionCredentials`,
|
||||
`RemoteIdentity` (ADR-091).
|
||||
- `crates/alknet-core/src/fingerprint.rs` — fingerprint computation +
|
||||
manual DER parsing (pure; moves cleanly).
|
||||
- `crates/alknet-client/src/dial/{quinn,tcp_tls,iroh}.rs` — the client
|
||||
consumption patterns (`TlsClientConfig` per dial; the iroh
|
||||
key-not-config exception).
|
||||
- `crates/alknet-endpoint/src/accept/tcp_tls.rs` — the server-side
|
||||
TCP+TLS accept loop (this crate provides the acceptor, not the loop).
|
||||
- alknet ADRs: 082 (extraction), 083 (endpoint takes no TLS config),
|
||||
084 (aws-lc-rs), 027 (identity model, `acme-tls/1`), 030 §6
|
||||
(fingerprint normalization), 086 §3 (split ALPN lists), 087
|
||||
(`TlsClientConfig`), 088 (`TlsError` shape, root-store fallback), 089
|
||||
(dial seam), 091 (`ConnectionCredentials`), 034 (verifier selection).
|
||||
- `docs/architecture/crates/tls/README.md` — the full crate spec; the
|
||||
closest thing to this crate's Phase 1 target document.
|
||||
|
||||
## Convergence checklist (what Phase 0 must produce)
|
||||
|
||||
- [x] Inventory of the extracted code with verified
|
||||
behavior-preservation invariants (§Prior art) — all five present,
|
||||
with line references
|
||||
- [x] Gap list: spec-pinned but unimplemented (§Gaps) — `TlsError`
|
||||
shape, `for_tcp_tls()`, config-type ownership, `SelfSigned`
|
||||
client semantics, tokio feature subset
|
||||
- [x] Rewrite-derived requirements (§What the alknet rewrite needs) —
|
||||
API surface freeze before consumers, no alknet imports,
|
||||
caller-owned ALPNs, iroh key-not-config
|
||||
- [x] Version/dependency posture recorded (§Version and dependency
|
||||
posture) — rustls 0.23 line current; feature-gate shape per spec
|
||||
- [x] Open questions enumerated (OQ-TLS-01..07) — all small; two are
|
||||
deferred-shaped (OQ-TLS-01 finalize, OQ-TLS-07); none require a
|
||||
POC (the code exists and runs; there is nothing to de-risk)
|
||||
- [ ] Open questions promoted to Phase 1
|
||||
`docs/architecture/open-questions.md` (Phase 1 work)
|
||||
- [ ] POC decision: none planned — extraction of working code does not
|
||||
need a POC; the OQ-TN-10-style validation step from alktunnels
|
||||
does not apply here
|
||||
27
src/lib.rs
Normal file
27
src/lib.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
//! alktls: shared TLS setup types — server and client rustls configs,
|
||||
//! cert resolvers, verifiers, and shared signing helpers.
|
||||
//!
|
||||
//! Phase 0 (exploration) — the crate shape is being established in
|
||||
//! `docs/research/phase-0.md`. This placeholder keeps the scaffold
|
||||
//! building until the crate skeleton lands in Phase 1.
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TlsError {
|
||||
#[error("TLS config error: {0}")]
|
||||
Config(String),
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("certificate error: {0}")]
|
||||
Cert(String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn error_type_constructs() {
|
||||
let e = TlsError::Config("placeholder".into());
|
||||
assert!(e.to_string().contains("placeholder"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user