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:
2026-09-09 16:25:55 +00:00
parent dbc77af3d3
commit a570bee0fe
11 changed files with 3140 additions and 73 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
target/
node_modules/
.worktrees/

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

43
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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"));
}
}