phase 1: architecture spec — overview, server/client, ADR-001..006
- ADR-001: inherit the alknet TLS design as the baseline; deviations recorded as alktls ADRs - ADR-002: TlsError ships the ADR-088 six-variant shape from day one (typed #[from] sources; NoqWrap; no string catch-all) - ADR-003: the QUIC feature is noq (iroh's extracted fork), pre- consumer rename; default = [] per the lean-crate convention (corrects the extracted code's default = ["quinn"]) - ADR-004: complete accessors — for_tcp_tls() adopted, rustls_config() adopted; server accessors borrow (&self), client accessors consume - ADR-005: identity + credentials + fingerprint types move into alktls; auth layer stays out - ADR-006: eight-module layout; seed tests + integration invariant pins (exact nine-scheme list, client enable_early_data) - specs: overview (transport picture, terminology), server.md (ACME lifecycle, invariants), client.md (verifier selection matrix, root- store fallback); open-questions.md promotes OQ-TLS-01..08 (all resolved at entry) - Cargo.toml: quinn feature -> noq (per ADR-003); AGENTS.md aligned Architecture review pass done: 0 critical, 2 major (ADR-002 AcmeConfig doc comment contradiction; ADR-003 unrecorded default deviation) and 8 minors all addressed; cross-references verified against alknet ADRs, rustls/noq/iroh sources. Verified: cargo test, test --all-features, clippy -D warnings, fmt --check, doc --no-deps
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Architecture
|
||||
|
||||
The authoritative architecture spec for the alktls crate (Phase 1 of
|
||||
the SDD process). All docs are **Draft** pending the architecture
|
||||
review pass; statuses update here as docs advance.
|
||||
|
||||
All docs follow the SDD process conventions: specs reference ADRs and
|
||||
OQs by number, ADRs explain WHY, `open-questions.md` tracks what is
|
||||
unresolved.
|
||||
|
||||
## Documents
|
||||
|
||||
| Doc | Status | Scope |
|
||||
|-----|--------|-------|
|
||||
| [overview.md](overview.md) | Draft | Purpose, transport picture, API surface, ADR/OQ index |
|
||||
| [server.md](server.md) | Draft | `TlsServerConfig`, resolvers, ACME path, server invariants |
|
||||
| [client.md](client.md) | Draft | `TlsClientConfig`, verifier selection, client auth, root-store fallback |
|
||||
| [open-questions.md](open-questions.md) | live | The authoritative OQ tracker (all Phase 0 OQs resolved at entry) |
|
||||
|
||||
## ADRs
|
||||
|
||||
| ADR | Status | Decision |
|
||||
|-----|--------|----------|
|
||||
| [001](decisions/001-inherit-alknet-tls-design.md) | Accepted | Inherit the alknet TLS design as the baseline; deviations recorded as alktls ADRs |
|
||||
| [002](decisions/002-tlserror-shape.md) | Accepted | `TlsError`: the ADR-088 six-variant shape from day one; config-construction scope boundary |
|
||||
| [003](decisions/003-noq-replaces-quinn.md) | Accepted | The QUIC feature is `noq` (iroh's extracted fork), not `quinn`; iroh stays key-not-config |
|
||||
| [004](decisions/004-accessor-surface.md) | Accepted | Complete accessors: `for_tcp_tls()` adopted; server borrows, client consumes |
|
||||
| [005](decisions/005-config-types-move-into-alktls.md) | Accepted | Identity + credentials + fingerprint types move into alktls; auth layer stays out |
|
||||
| [006](decisions/006-module-layout-and-tests.md) | Accepted | Eight-module layout; seed tests + integration invariant pins |
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Docs move `Draft` → `Reviewed` when their open questions are resolved
|
||||
and the architecture review reports zero critical issues; ADRs are
|
||||
Accepted at write time and never revert (supersede instead).
|
||||
`open-questions.md` is the authoritative tracker; the Phase 0 doc's
|
||||
OQ statuses are the historical record.
|
||||
|
||||
## Phase status
|
||||
|
||||
- **Phase 0** (complete, 2026-09-10): `docs/research/phase-0.md` —
|
||||
extraction inventory, verified invariants, gaps, noq investigation,
|
||||
OQ-TLS-01..08.
|
||||
- **Phase 1** (this directory): all Phase 0 OQs resolved at entry —
|
||||
six via ADR-001..006, two as documented behavior (OQ-TLS-02,
|
||||
OQ-TLS-06); review pass pending.
|
||||
- **Phase 2** (next): decomposition into `tasks/` — the port guided by
|
||||
ADR-006's module map and seed tests.
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Client side
|
||||
|
||||
`TlsClientConfig` and its verifiers: the client-side TLS setup,
|
||||
extracted from alknet (`crates/alknet-tls/src/client.rs`) and ported
|
||||
per the ADRs. Decisions referenced here: see
|
||||
[open-questions.md](open-questions.md) and the index in
|
||||
[overview.md](overview.md).
|
||||
|
||||
## `TlsClientConfig`
|
||||
|
||||
Built **per dial** from a `ConnectionCredentials` + ALPN (the
|
||||
dial-seam pattern, alknet ADR-089). Consumed by its accessors — a
|
||||
`TlsClientConfig` is not reused across dials (ADR-004).
|
||||
|
||||
```rust
|
||||
pub struct TlsClientConfig {
|
||||
rustls_config: rustls::ClientConfig,
|
||||
}
|
||||
|
||||
impl TlsClientConfig {
|
||||
pub fn new(credentials: &ConnectionCredentials, alpn: &[u8])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(self) -> Result<noq::ClientConfig, TlsError>;
|
||||
|
||||
pub fn into_rustls_config(self) -> rustls::ClientConfig;
|
||||
}
|
||||
```
|
||||
|
||||
`new` is sync and infallible-free aside from config construction
|
||||
(cert file loading for an X.509 local identity is the only I/O).
|
||||
Every config sets `enable_early_data = true` (the client half of the
|
||||
0-RTT invariant) and carries the aws-lc-rs provider (alknet ADR-084).
|
||||
|
||||
## The two credential dimensions
|
||||
|
||||
`ConnectionCredentials` (ADR-005, moved from alknet ADR-091) carries
|
||||
exactly the two inputs the client config consumes:
|
||||
|
||||
1. **`local_identity: Option<TlsIdentity>`** — the client-auth cert
|
||||
presentation:
|
||||
|
||||
| Local identity | Presented |
|
||||
|----------------|-----------|
|
||||
| `RawKey` | RFC 7250 raw public key (SPKI DER; `only_raw_public_keys()` auto-detected from the DER) |
|
||||
| `X509` | the cert chain + key, loaded from disk |
|
||||
| `SelfSigned` / `None` | nothing (`NoClientCertResolver`) — documented, resolved in OQ-TLS-02 |
|
||||
| `Acme` | **config error** (`TlsError::AcmeConfig`) — server-only identity |
|
||||
|
||||
2. **`remote_identity: Option<RemoteIdentity>`** — the verifier
|
||||
selection matrix (below). Both `Option`s are load-bearing, not
|
||||
cosmetic: `Some` means "pin this", `None` means "trust the CA or
|
||||
fail" — never a placeholder default.
|
||||
|
||||
## Verifier selection (alknet ADR-034 §3)
|
||||
|
||||
Exactly three outcomes, driven by `remote_identity`:
|
||||
|
||||
| `remote_identity` | Remote cert | Verifier | Outcome |
|
||||
|-------------------|-------------|----------|---------|
|
||||
| `Some(fingerprint)` | any | `FingerprintPinVerifier` | pin match required |
|
||||
| `None` | X.509 | `WebPkiServerVerifier` (CA) | CA verification against the platform root store |
|
||||
| `None` | Ed25519 raw key | (the CA verifier fails) | **fail closed at handshake** |
|
||||
|
||||
`None` is the public-X.509-endpoint state, **not** "skip
|
||||
verification". An unknown raw-key remote fails at handshake — it must
|
||||
never silently downgrade to CA verification. Verifier selection
|
||||
happens at config construction; the fail-closed *manifests* at
|
||||
handshake time (the config carries the CA verifier; the raw-key
|
||||
remote simply cannot satisfy it) — this is the ADR-002 scope
|
||||
boundary in action.
|
||||
|
||||
**Fail-closed is structural**: known peer + fingerprint → pin;
|
||||
unknown + X.509 → CA; unknown + raw key → fail. No fourth path.
|
||||
|
||||
## `FingerprintPinVerifier`
|
||||
|
||||
The known-peer path. The fingerprint IS the trust anchor: for
|
||||
`ed25519:<hex>` remotes the raw Ed25519 key is extracted from the
|
||||
presented cert (SPKI) and matched; for `SHA256:<hex>` remotes the
|
||||
full cert DER is hashed and matched. No CA verification, no name
|
||||
verification — only the pin.
|
||||
|
||||
Handshake signatures are still verified (TLS 1.2/1.3, aws-lc-rs
|
||||
algorithms; Ed25519 SPKI certs route through
|
||||
`verify_tls13_signature_with_raw_key`): a stolen fingerprint cannot
|
||||
be replayed with a forged signature — the presenter must prove
|
||||
possession of the corresponding private key.
|
||||
|
||||
## The root-store fallback (alknet ADR-088 §5)
|
||||
|
||||
The `None` + X.509 CA path loads the platform's native root certs
|
||||
(`rustls-native-certs`). If the platform store is empty — a
|
||||
containerized deployment with no system CA bundle — the built-in
|
||||
`webpki-roots` are merged in so the store is **never empty**.
|
||||
Native-certs *load* errors are logged, not returned; the fallback
|
||||
guarantees non-emptiness regardless. This makes `NoRootAnchors`
|
||||
unreachable in practice (the `VerifierBuild` variant exists for the
|
||||
builder API's completeness, not a reachable path — ADR-002).
|
||||
|
||||
The root store is built unconditionally for the CA path — not
|
||||
feature-gated on `tcp` or `noq` — because any client dialing a public
|
||||
X.509 endpoint needs it regardless of transport.
|
||||
|
||||
## What the client side does NOT do
|
||||
|
||||
- No dial: the dial seam (`AlknetClient` in alknet; the rewrite's
|
||||
dial crate) consumes the config via `for_noq()` /
|
||||
`into_rustls_config()` and owns `TcpStream::connect`,
|
||||
`Endpoint::connect`, SNI, and SOCKS5 proxying (alknet ADR-089;
|
||||
SOCKS5 proxy seam per alknet ADR-090).
|
||||
Handshake failures are the dial's errors (`ClientDialError`),
|
||||
not `TlsError`.
|
||||
- No iroh: the iroh dial takes the 32-byte Ed25519 key directly
|
||||
(key-not-config; ADR-003). iroh's built-in verifier (NodeId check)
|
||||
is iroh's own application of the same fail-closed rule.
|
||||
- No per-request auth: a call-protocol `auth_token` is a call-layer
|
||||
concept; it never reaches `TlsClientConfig`.
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](overview.md) — the index; [server.md](server.md) —
|
||||
the server side
|
||||
- alknet `crates/tls/README.md` §Client side — the full client-side
|
||||
spec this doc mirrors
|
||||
- alknet ADR-034 (verifier selection), ADR-088 §5 (root-store
|
||||
fallback), ADR-091 (`ConnectionCredentials`)
|
||||
- ADR-002 (`TlsError`), ADR-004 (accessors), ADR-005 (credential
|
||||
types), ADR-006 (tests — the verifier-selection matrix)
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-001: Inherit the alknet TLS design; alktls as the rewrite's TLS crate
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
alktls is the extraction of the TLS handling from alknet
|
||||
(`crates/alknet-tls` + the config types in `crates/alknet-core`), the
|
||||
last crate extracted before the alknet rewrite begins. alknet already
|
||||
recorded the full TLS design as ADRs (082/083/084/086/087/088/089/
|
||||
091, plus ADR-027's identity model and ADR-034's verifier selection)
|
||||
and a complete crate spec (`docs/architecture/crates/tls/README.md`).
|
||||
The extracted code runs in production today and every
|
||||
behavior-preservation invariant was verified present in Phase 0
|
||||
(`docs/research/phase-0.md` §Prior art).
|
||||
|
||||
Phase 1 must decide how this crate relates to that body of decisions:
|
||||
re-litigate, inherit wholesale, or inherit with recorded deviations.
|
||||
|
||||
## Decision
|
||||
|
||||
alktls **inherits** the alknet TLS design. The alknet ADRs and the
|
||||
`crates/tls` README are adopted as this crate's authoritative prior
|
||||
decisions; Phase 1 does not re-litigate them. This ADR records that
|
||||
inheritance as alktls' own baseline, so this repo's ADR numbering
|
||||
starts from a coherent baseline instead of dangling references into
|
||||
another repo's architecture.
|
||||
|
||||
Concretely, alktls adopts:
|
||||
|
||||
- `TlsServerConfig` / `TlsClientConfig` as the central types, built
|
||||
once, shared across transports via `Arc` (alknet ADR-082). The inner
|
||||
rustls config is `Clone` (Arc-shared resolvers); `TlsServerConfig`
|
||||
is not `Clone` (it holds the ACME task's `JoinHandle`).
|
||||
- The config-construction scope boundary: this crate builds configs;
|
||||
handshake outcomes flow through the transport's connector; ACME
|
||||
runtime errors are stream events logged in the spawned task
|
||||
(alknet ADR-088 §6).
|
||||
- The behavior-preservation invariants verbatim: `max_early_data_size
|
||||
= u32::MAX` on all server paths **plus `enable_early_data = true`
|
||||
on every client config** (the client half of the 0-RTT invariant),
|
||||
`aws_lc_rs::default_provider()` on
|
||||
all paths, `AcceptAnyCertVerifier`'s nine-scheme
|
||||
`supported_verify_schemes()`, crate-side `acme-tls/1` append for the
|
||||
ACME path only, non-empty root store via `webpki-roots` fallback
|
||||
(alknet ADR-082, ADR-084, ADR-088 §5).
|
||||
- The identity model: `TlsIdentity` four variants, `Acme` server-only,
|
||||
client-auth presentation follows the local identity, verifier
|
||||
selection fail-closed (alknet ADR-027, ADR-034, ADR-091).
|
||||
- One ACME state machine per domain, shared across transports; never a
|
||||
second order for a served domain (alknet ADR-082 §The cert-reuse
|
||||
problem).
|
||||
- iroh stays key-not-config: no `for_iroh()`; the identity type
|
||||
exposes 32-byte Ed25519 access for `iroh_base::SecretKey`
|
||||
(alknet ADR-082 §Iroh is different).
|
||||
|
||||
Where alktls deviates from or refines the inherited design, the
|
||||
deviation is recorded as an alktls ADR (001.. this series) rather than
|
||||
silently diverging. The known deviations/corrections at time of
|
||||
writing: the `TlsError` shape (ADR-002), the QUIC feature (ADR-003,
|
||||
which also corrects the extracted code's `default = ["quinn"]` back
|
||||
to the spec's `default = []`), the API surface completion (ADR-004),
|
||||
and config-type ownership (ADR-005).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- The crate implements proven code, not a new design; Phase 2 is a
|
||||
port with a verified behavior checklist, not an invention.
|
||||
- Design rationale is already written and battle-tested; this repo's
|
||||
ADR series stays small and points at the alknet reasoning where the
|
||||
full argument lives.
|
||||
- The alknet rewrite consumes one crate with one consistent ADR
|
||||
lineage instead of reverse-engineering decisions from scattered
|
||||
alknet ADRs.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- alktls ADRs are derivative — a reader must consult alknet's ADRs for
|
||||
the full rationale of the inherited posture (mitigated: each
|
||||
alktls ADR restates the decision it pins, and the Phase 0 doc
|
||||
carries the verified line references).
|
||||
- If alknet's rewrite later re-decides an inherited posture, both
|
||||
series need a superseding note.
|
||||
|
||||
## References
|
||||
|
||||
- `docs/research/phase-0.md` — the Phase 0 inventory (verified
|
||||
invariants, gaps, noq investigation)
|
||||
- alknet ADR-082 (extraction), ADR-083 (endpoint takes no TLS config),
|
||||
ADR-084 (aws-lc-rs), ADR-027 (identity model), ADR-034 (verifier
|
||||
selection), ADR-086 §3 (split ALPN lists), ADR-087
|
||||
(`TlsClientConfig`), ADR-088 (`TlsError`, root-store fallback), 089
|
||||
(dial seam), 091 (`ConnectionCredentials`)
|
||||
- alknet `docs/architecture/crates/tls/README.md` — the full crate
|
||||
spec this crate implements
|
||||
- ADR-002 — the `TlsError` shape (gap #1)
|
||||
- ADR-003 — the QUIC feature: noq replaces quinn
|
||||
- ADR-004 — the API surface (gap #2, accessor shapes)
|
||||
- ADR-005 — config-type ownership (gap #3)
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-002: `TlsError` — the ADR-088 six-variant shape from day one
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The extracted code (`crates/alknet-tls/src/lib.rs`) carries a
|
||||
simplified 3-variant `TlsError`: `Config(String)`, `Io(io::Error)`,
|
||||
`Cert(String)`. alknet ADR-088 recorded the target shape — a single
|
||||
`#[non_exhaustive]` enum, one variant per failure category, typed
|
||||
`#[from]` sources — but it was never implemented; the extracted code
|
||||
folds every typed failure into `Config(e.to_string())` strings, losing
|
||||
the `#[source]` chain and the category distinction.
|
||||
|
||||
Phase 0 identified this as gap #1, the highest-value gap closure: this
|
||||
crate is pre-consumer, so the enum shape is still a two-way door; once
|
||||
the alknet rewrite consumes it, it is one-way.
|
||||
|
||||
Two mechanics from ADR-088's "Gotchas" needed verification before
|
||||
pinning the shape:
|
||||
|
||||
- `CertLoad`'s `#[from] io::Error` relies on `rustls_pemfile`
|
||||
funnelling its own non-`std::error::Error` error type into
|
||||
`io::Error`. Confirmed at the pinned versions: `rustls_pemfile`
|
||||
2.x returns `Result<T, io::Error>` from its iterator and read APIs
|
||||
(`certs`, `private_key`), so PEM parse failures surface as
|
||||
`io::Error` — the `CertLoad(#[from] io::Error)` variant is sound.
|
||||
- `VerifierBuild` wraps `rustls::webpki::VerifierBuilderError`
|
||||
(confirmed present in rustls 0.23.44: `NoRootAnchors` +
|
||||
`InvalidCrl(CertRevocationListError)`, `#[non_exhaustive]`).
|
||||
With the ADR-088 §5 root-store fallback in place,
|
||||
`NoRootAnchors` is unreachable in practice — the variant exists for
|
||||
the builder API's completeness, not a reachable failure path.
|
||||
|
||||
## Decision
|
||||
|
||||
`TlsError` ships as the alknet ADR-088 shape, unmodified:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum TlsError {
|
||||
/// Cert or key file read / PEM parse. `rustls_pemfile` funnels its
|
||||
/// own error type into `io::Error`, so one `io::Error` source covers
|
||||
/// the whole loading path.
|
||||
#[error("loading cert/key material: {0}")]
|
||||
CertLoad(#[from] std::io::Error),
|
||||
|
||||
/// Self-signed cert generation (rcgen). Server `SelfSigned` path.
|
||||
#[error("generating self-signed cert: {0}")]
|
||||
SelfSigned(#[from] rcgen::Error),
|
||||
|
||||
/// rustls server or client config construction
|
||||
/// (`with_safe_default_protocol_versions`, `with_single_cert`,
|
||||
/// `CertifiedKey::from_der`, `RootCertStore::add`).
|
||||
#[error("building rustls config: {0}")]
|
||||
Rustls(#[from] rustls::Error),
|
||||
|
||||
/// `WebPkiServerVerifier::builder(_with_provider)..build()` — the
|
||||
/// unknown-X.509-remote client path.
|
||||
#[error("building webpki verifier: {0}")]
|
||||
VerifierBuild(#[from] rustls::webpki::VerifierBuilderError),
|
||||
|
||||
/// QUIC config wrapping — the one path where `for_noq()` fails
|
||||
/// (`NoInitialCipherSuite`, not a `rustls::Error`). noq-gated.
|
||||
#[cfg(feature = "noq")]
|
||||
#[error("wrapping rustls config for noq: {0}")]
|
||||
NoqWrap(#[from] noq_proto::crypto::rustls::NoInitialCipherSuite),
|
||||
|
||||
/// Config-mismatch errors that are not wrapped third-party
|
||||
/// errors: ACME feature not enabled but `Acme` configured
|
||||
/// (server), or `Acme` identity used for client auth. A config
|
||||
/// error, not a wrapped third-party error.
|
||||
#[error("TLS config error: {0}")]
|
||||
AcmeConfig(String),
|
||||
}
|
||||
```
|
||||
|
||||
Notes on the shape, per ADR-088's rationale:
|
||||
|
||||
- **Single enum, not a thin wrapper.** ADR-088's three findings stand:
|
||||
the QUIC-wrap failure is `NoInitialCipherSuite` (not a
|
||||
`rustls::Error`), `rustls_pemfile`'s error is not a
|
||||
`std::error::Error`, and `WebPkiServerVerifier::build()` returns its
|
||||
own `VerifierBuilderError`. One enum with `#[from]` sources models
|
||||
the actual call sites.
|
||||
- **The quinn-gated variant becomes `NoqWrap`** (noq-gated), renaming
|
||||
ADR-088's `QuinnWrap` per ADR-003. Same failure type name
|
||||
(`NoInitialCipherSuite`), different crate path.
|
||||
- **`AcmeConfig(String)` keeps string payloads.** The two Acme
|
||||
mismatches are genuinely configuration mistakes, not wrapped
|
||||
upstream errors; there is no third-party error type to preserve.
|
||||
The extracted code's residual `Config(String)` call sites
|
||||
(e.g. `CertifiedKey::from_der` string fallbacks in the client-auth
|
||||
builder) map to this variant — see the refinement below.
|
||||
- **Scope boundary holds (alknet ADR-088 §6).** `TlsError` is the
|
||||
config-construction error type. Handshake outcomes (a rejected
|
||||
cert, the unknown-raw-key fail-closed) flow through the transport's
|
||||
connector; ACME state-machine runtime errors are stream events
|
||||
logged in the spawned task. No handshake variants, ever.
|
||||
- **`#[non_exhaustive]` from day one.** The enum is crate-local
|
||||
today, but the rewrite compiles against it; new variants are
|
||||
additive for consumers that match with a wildcard arm.
|
||||
|
||||
**Refinement over the extracted code:** the extracted code uses
|
||||
`TlsError::Config(e.to_string())` at `rustls::Error` call sites that
|
||||
ADR-088 assigns to `Rustls` (e.g. `with_single_cert`,
|
||||
`CertifiedKey::from_der` — both return `rustls::Error`). This crate
|
||||
maps those call sites to the typed variants. The only genuinely
|
||||
stringy sites are the Acme mismatches, which land in `AcmeConfig`
|
||||
(matching the sketch above — `AcmeConfig` holds exactly the two
|
||||
config-mismatch cases, nothing else). No `Config(String)` catch-all
|
||||
survives — a variant-per-category enum with a string catch-all would
|
||||
reintroduce the fold this ADR removes.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- `#[source]` chains survive for programmatic inspection (the
|
||||
rewrite's config plumbing can distinguish a missing file from a bad
|
||||
PEM from a bad config without string parsing).
|
||||
- The crate ships the recorded target shape — no later migration.
|
||||
- The noq rename rides along; `TlsError` never has a `QuinnWrap`
|
||||
variant to deprecate.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- Six variants is more surface than three; consumers must match
|
||||
more arms (mitigated by `#[non_exhaustive]` + wildcard arms).
|
||||
- `rcgen::Error` in the public API couples the error type to rcgen's
|
||||
error churn (accepted — rcgen 0.13 is stable and the coupling is
|
||||
what ADR-088 chose).
|
||||
|
||||
## References
|
||||
|
||||
- alknet ADR-088 — the recorded target shape and its rationale
|
||||
- alknet ADR-088 §5 — the root-store fallback that keeps
|
||||
`NoRootAnchors` unreachable
|
||||
- `docs/research/phase-0.md` §Gaps #1 — the gap analysis
|
||||
- ADR-001 — the inheritance baseline
|
||||
- ADR-003 — the noq feature (the `NoqWrap` rename)
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-003: The QUIC feature is `noq`, not `quinn`
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The extracted crate's `quinn` feature (`for_quinn()` accessors,
|
||||
quinn-gated wrap) predates the noq split: iroh has extracted its
|
||||
internal quinn fork into the standalone, published `noq` project
|
||||
(`noq` / `noq-proto` / `noq-udp`; noq 1.2.0 on crates.io), and iroh
|
||||
1.1.0 is built on noq 1.2.0, re-exporting `noq::*` as public API.
|
||||
The alknet rewrite will standardize on noq for QUIC — both the bare
|
||||
QUIC transport and iroh's internal one — eliminating the duplicate
|
||||
QUIC implementations.
|
||||
|
||||
Phase 0 verified the seam is API-compatible with quinn 0.11
|
||||
(`docs/research/phase-0.md` §Prior art: noq):
|
||||
|
||||
- `noq_proto::crypto::rustls::QuicServerConfig::try_from(
|
||||
rustls::ServerConfig)` / `QuicClientConfig::try_from(
|
||||
rustls::ClientConfig)` — same constructor shape, same
|
||||
`NoInitialCipherSuite` failure type, different crate path.
|
||||
- noq 1.2 pins `rustls ^0.23.33`; iroh 1.1 pins `rustls 0.23.33` —
|
||||
the same 0.23 line this crate pins. One rustls tree across all
|
||||
paths.
|
||||
- noq's `rustls-aws-lc-rs` feature selects the aws-lc-rs provider;
|
||||
iroh's `tls-aws-lc-rs` does the same on the iroh side. The ADR-084
|
||||
posture composes unchanged.
|
||||
- noq's MSRV is 1.88 — the ecosystem floor (adopted 2026-09-10),
|
||||
so no MSRV tension remains.
|
||||
|
||||
The `quinn` feature has no published consumer: alktls is
|
||||
pre-consumer, and the alknet rewrite (its only planned consumer)
|
||||
targets noq. Carrying both features would double the accessor surface
|
||||
with nothing to transition.
|
||||
|
||||
## Decision
|
||||
|
||||
**The QUIC feature is `noq`.** The `quinn` feature never existed in a
|
||||
published alktls, so the rename is pre-consumer and free:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
default = []
|
||||
noq = ["dep:noq", "dep:noq-proto"]
|
||||
tcp = ["dep:tokio-rustls"]
|
||||
acme = ["dep:rustls-acme"]
|
||||
|
||||
[dependencies]
|
||||
noq = { version = "1.2", optional = true, default-features = false, features = ["rustls"] }
|
||||
noq-proto = { version = "1.2", optional = true, default-features = false }
|
||||
```
|
||||
|
||||
`default = []` — all transport features opt-in, matching the
|
||||
inherited alknet spec's feature-gate shape and AGENTS.md convention 9
|
||||
(the default crate compiles lean; a consumer that never runs QUIC
|
||||
should not pull `noq` + `noq-proto`). Note the extracted code's
|
||||
`Cargo.toml` carries `default = ["quinn"]` — a silent deviation from
|
||||
the alknet spec that this port corrects rather than carries forward.
|
||||
The `noq` dep is enabled with `default-features = false,
|
||||
features = ["rustls"]` so noq's provider-selection features do not
|
||||
fight the crate's explicit provider posture; `noq-proto` rides along
|
||||
for the `NoqWrap` error source.
|
||||
|
||||
- `TlsServerConfig::for_noq(&self) -> Result<noq::ServerConfig,
|
||||
TlsError>` (feature-gated on `noq`) —
|
||||
`noq::ServerConfig::with_crypto(Arc::new(
|
||||
noq::crypto::rustls::QuicServerConfig::try_from(inner)?))`.
|
||||
- `TlsClientConfig::for_noq(self) -> Result<noq::ClientConfig,
|
||||
TlsError>` (feature-gated on `noq`) — the client-side mirror.
|
||||
- `TlsError::NoqWrap(#[from]
|
||||
noq_proto::crypto::rustls::NoInitialCipherSuite)` (ADR-002).
|
||||
- There is no `for_quinn()`, no `quinn` dependency, no quinn feature.
|
||||
- iroh remains key-not-config (ADR-001): iroh's endpoint is fed the
|
||||
Ed25519 secret key; the `noq` feature does not attempt to serve
|
||||
iroh's TLS, and iroh does not appear in this crate's dependency
|
||||
tree.
|
||||
- The `tcp` feature (`tokio-rustls`) and `acme` feature are
|
||||
unaffected.
|
||||
|
||||
Provider wiring on the QUIC path: the rustls configs this crate builds
|
||||
already carry the aws-lc-rs provider (every path constructs its own
|
||||
provider via `builder_with_provider`); noq's `rustls` feature
|
||||
consumes that provider from the config.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- One QUIC implementation across the bare-QUIC and iroh transports;
|
||||
no duplicate quinn/noq stacks in the rewrite's binaries.
|
||||
- `TlsError` never has a dead `QuinnWrap` variant.
|
||||
- The crate's MSRV floor (1.88) is honest for the whole tree rather
|
||||
than feature-dependent.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- If a future consumer needs quinn (none is known), that is a new
|
||||
feature alongside `noq` — additive, cheap, and only paid if real.
|
||||
- noq is young (1.2.0, first release Feb 2026); its API may churn
|
||||
faster than quinn 0.11's frozen line (accepted — n0-computer owns
|
||||
both noq and iroh, and the rewrite rides their stack; pin `noq =
|
||||
"1.2"` and bump deliberately).
|
||||
|
||||
## References
|
||||
|
||||
- `docs/research/phase-0.md` §Prior art: noq — the verified seam facts
|
||||
- `/workspace/noq`, `/workspace/iroh` — the upstream clones inspected
|
||||
- ADR-001 — the inheritance baseline (quinn-era accessors inherited
|
||||
with deviations recorded here)
|
||||
- ADR-002 — `TlsError` (`NoqWrap`)
|
||||
- ADR-004 — accessor shapes (`&self` vs `self`)
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-004: Complete the accessors — `for_tcp_tls()`, `rustls_config()`, and borrow-vs-consume
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The alknet spec (ADR-082's API table and the `crates/tls` README)
|
||||
pins four server-side accessors and three client-side accessors. The
|
||||
extracted code implements a subset and diverges in shape:
|
||||
|
||||
| Accessor | Spec | Extracted code |
|
||||
|----------|------|----------------|
|
||||
| `TlsServerConfig::new` | `async`, `(&TlsIdentity, &[Vec<u8>])` | same |
|
||||
| `TlsServerConfig::for_quinn` | `&self` | `self` (consumes) |
|
||||
| `TlsServerConfig::for_tcp_tls` | `-> TlsAcceptor`, infallible | **missing** — callers wrap `tokio_rustls::TlsAcceptor::from(Arc::new(cfg.rustls_config.clone()))` themselves |
|
||||
| `TlsServerConfig::rustls_config` | `&self -> &ServerConfig` | **missing** — the field is `pub(crate)` |
|
||||
| `TlsClientConfig::new` | sync, `(&ConnectionCredentials, &[u8])` | same |
|
||||
| `TlsClientConfig::for_quinn` | `self` (consumes) | `self` |
|
||||
| `TlsClientConfig::into_rustls_config` | `self` (consumes) | same |
|
||||
|
||||
The missing server accessors force the assembly layer to reach into
|
||||
crate internals (`pub(crate)` field access is impossible for external
|
||||
consumers) or re-derive the acceptor wrap. The `self`-consuming
|
||||
server accessor prevents the literal ADR-082 story — one
|
||||
`TlsServerConfig` feeding both a QUIC endpoint and a TCP+TLS acceptor —
|
||||
without contortions.
|
||||
|
||||
## Decision
|
||||
|
||||
The public API surface is the spec's surface, with borrow-vs-consume
|
||||
decided per accessor:
|
||||
|
||||
```rust
|
||||
impl TlsServerConfig {
|
||||
pub async fn new(identity: &TlsIdentity, alpns: &[Vec<u8>])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
/// `&self` — the inner rustls config is Clone (Arc-shared
|
||||
/// resolvers); one TlsServerConfig can feed a noq endpoint AND a
|
||||
/// TCP+TLS acceptor without contortions.
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(&self) -> Result<noq::ServerConfig, TlsError>;
|
||||
|
||||
/// Infallible — `TlsAcceptor::from(Arc<ServerConfig>)` cannot
|
||||
/// fail. Feature-gated on `tcp`.
|
||||
#[cfg(feature = "tcp")]
|
||||
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor;
|
||||
|
||||
/// Borrow the inner config for transport wrappers the crate does
|
||||
/// not cover.
|
||||
pub fn rustls_config(&self) -> &rustls::ServerConfig;
|
||||
}
|
||||
|
||||
impl TlsClientConfig {
|
||||
pub fn new(credentials: &ConnectionCredentials, alpn: &[u8])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
/// Consumes — one dial = one config build; a `TlsClientConfig` is
|
||||
/// not reused across dials.
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(self) -> Result<noq::ClientConfig, TlsError>;
|
||||
|
||||
/// Consumes — the TCP+TLS dial wraps the returned config in a
|
||||
/// `TlsConnector` itself.
|
||||
pub fn into_rustls_config(self) -> rustls::ClientConfig;
|
||||
}
|
||||
```
|
||||
|
||||
Rationale per accessor:
|
||||
|
||||
- **Server accessors take `&self`.** The ADR-082 story is "one
|
||||
identity, N transports": the assembly layer builds one
|
||||
`TlsServerConfig` per endpoint type and hands it to every transport
|
||||
that endpoint serves. `&self` plus the Clone inner config makes the
|
||||
multi-transport sharing direct; the extracted `self`-consuming shape
|
||||
is a vestige of the quinn-only extraction era (the TCP path was
|
||||
re-derivable only because the field happened to be `pub(crate)` in
|
||||
the same workspace).
|
||||
- **Client accessors consume `self`.** A `TlsClientConfig` is built
|
||||
per dial (`dial_quic`, `dial_tcp_tls` build fresh configs per
|
||||
ADR-089's pattern); nothing reuses it. Consuming makes
|
||||
`into_rustls_config` zero-cost (no clone behind the scenes) and
|
||||
keeps the API honest about reuse.
|
||||
- **`for_tcp_tls()` is adopted** (Phase 0 gap #2). It is one line,
|
||||
infallible, and the spec is unambiguous; leaving it out would keep
|
||||
the de facto "callers wrap the acceptor" shape and force the rewrite
|
||||
to duplicate it.
|
||||
- **`rustls_config()` is adopted** for any transport wrapper beyond
|
||||
`for_noq` / `for_tcp_tls` (iroh does not need it — key-not-config —
|
||||
but the escape hatch costs nothing and stays true to the spec).
|
||||
- **`new` stays `async fn`** for API uniformity with the ACME path
|
||||
(which spawns the state-machine task); the non-ACME paths have no
|
||||
await point (sync file I/O) — this is the spec's recorded posture,
|
||||
and the uniform signature is worth more than the await-free purity.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- The assembly layer builds configs and transports without reaching
|
||||
into crate internals or hand-rolling acceptor wraps.
|
||||
- The API freeze matches the spec the rewrite was specced against —
|
||||
zero translation for the rewrite's consumers.
|
||||
- The multi-transport story (one config → noq + TCP+TLS) is directly
|
||||
expressible.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- `&self` server accessors require the inner rustls config to stay
|
||||
`Clone`-able — already true and load-bearing (Arc-shared
|
||||
resolvers); a future config shape that is not Clone would break the
|
||||
accessor contract (acceptable: Clone is structural to the
|
||||
sharing story).
|
||||
|
||||
## References
|
||||
|
||||
- alknet ADR-082 API table and the `crates/tls` README §Architecture —
|
||||
the spec surface this ADR adopts
|
||||
- `docs/research/phase-0.md` §Gaps #2, OQ-TLS-03/OQ-TLS-04 — the gap
|
||||
and the accessor-shape question
|
||||
- ADR-002 — `TlsError` (the `for_noq` failure variant)
|
||||
- ADR-003 — the `noq` feature
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-005: Config types move into alktls; alktls owns the identity, credentials, and fingerprint
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The extracted code imports four type groups from `alknet-core`:
|
||||
|
||||
| Group | Types | Used by |
|
||||
|-------|-------|---------|
|
||||
| Identity | `TlsIdentity`, `Ed25519SecretKey`, `AcmeDirectory` | server + client config construction |
|
||||
| Credentials | `ConnectionCredentials`, `RemoteIdentity` | `TlsClientConfig::new` |
|
||||
| Fingerprint | `fingerprint_from_cert_der`, `extract_ed25519_raw_key_from_spki` | `FingerprintPinVerifier`, server-side extraction (endpoint) |
|
||||
| Auth (not TLS) | `PeerEntry`, `AuthPolicy`, `Identity`, `IdentityProvider` | fingerprint → peer-id resolution (endpoint/auth layer) |
|
||||
|
||||
AGENTS.md convention 10 says this crate owns the config types after
|
||||
the rewrite; but they are *config* types that `StaticConfig`, the
|
||||
vault, and the auth layer also consume. Phase 0's OQ-TLS-01 framed
|
||||
three options: move everything (A), move identity + fingerprint and
|
||||
leave credentials at the dial seam (B), or keep in core with
|
||||
re-exports (C).
|
||||
|
||||
The structural constraint: **alktls must compile without alknet**. It
|
||||
is the rewrite's TLS crate; a dependency on the rewrite's core would
|
||||
invert the layering (core always depends on TLS types, never the
|
||||
reverse). Re-exports (C) would leave alktls importing from a crate
|
||||
that is itself being rewritten — the coupling this extraction exists
|
||||
to break.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option A with one carve-out: alktls owns the identity, credentials,
|
||||
and fingerprint types; the auth layer stays out.**
|
||||
|
||||
Moving into alktls (`src/`):
|
||||
|
||||
- `identity.rs` — `TlsIdentity`, `Ed25519SecretKey`, `AcmeDirectory`
|
||||
(from alknet-core `config.rs`). `Ed25519SecretKey` keeps its exact
|
||||
surface — `generate()`, `from_bytes(&[u8; 32])`, `as_bytes() ->
|
||||
[u8; 32]`, `public()`, `sign()` — because iroh's
|
||||
`iroh_base::SecretKey` consumes the 32 raw bytes (OQ-TLS-07's
|
||||
requirement, verified against iroh 1.1 in Phase 0) and the
|
||||
`Ed25519SigningKey` helper signs through it.
|
||||
- `credentials.rs` — `ConnectionCredentials`, `RemoteIdentity` (from
|
||||
alknet-core `credentials.rs`), including the load-bearing `Option`
|
||||
semantics in their doc comments: `Some(fingerprint)` = pin,
|
||||
`None` = CA-or-fail-closed, never a placeholder default.
|
||||
- `fingerprint.rs` — `fingerprint_from_cert_der`,
|
||||
`extract_ed25519_raw_key_from_spki`, and the manual DER parser
|
||||
(from alknet-core). Production code stays `sha2` + manual DER —
|
||||
no rustls dep needed by the module itself (the crate has rustls
|
||||
anyway, but the module's purity keeps it testable standalone).
|
||||
|
||||
Staying out of alktls:
|
||||
|
||||
- `PeerEntry`, `AuthPolicy`, `Identity`, `IdentityProvider`,
|
||||
fingerprint → peer-id resolution — these are the auth/identity
|
||||
layer (peer records, tokens, scopes), not TLS. The rewrite's core
|
||||
(or its auth crate) owns them. alktls' server side hands the
|
||||
extracted fingerprint string to the caller; the caller resolves it.
|
||||
This keeps the TLS-crate scope boundary clean: cert-level identity
|
||||
in, peer-level identity out.
|
||||
|
||||
`StaticConfig`'s `tls_identity` field follows the type: the rewrite's
|
||||
config struct depends on alktls for `TlsIdentity` (one-way: config →
|
||||
alktls, never alktls → config). The vault derives `Ed25519SecretKey`
|
||||
from key material and hands it in — alktls has no vault dep.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- alktls compiles standalone; the rewrite's core depends on alktls
|
||||
for TLS types in one direction only. No cycle risk when the
|
||||
rewrite's module map settles.
|
||||
- The credential bundle (`ConnectionCredentials`) and the verifier
|
||||
selection it drives (ADR-034) live in the same crate — the
|
||||
semantics are coupled (the `Option` dance is meaningless outside
|
||||
the verifier contract), so co-locating them prevents semantic
|
||||
drift between the bundle and the verifier.
|
||||
- The fingerprint helpers move with their consumers
|
||||
(`FingerprintPinVerifier`); the normalized `ed25519:<hex>` /
|
||||
`SHA256:<hex>` formats stay with the code that computes them.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- alktls' public API grows by the moved types (identity +
|
||||
credentials + fingerprint). This is the point — the types are TLS
|
||||
types — but it widens the API-freeze review surface.
|
||||
- `alknet-core` (in the rewrite's form) must take the alktls dep for
|
||||
`TlsIdentity` in its config struct. If the rewrite's core wants to
|
||||
stay dependency-light, the type could be re-exported rather than
|
||||
consumed deeply (implementation detail of the rewrite, not this
|
||||
crate's concern).
|
||||
|
||||
## References
|
||||
|
||||
- AGENTS.md convention 10 — the recorded ownership expectation
|
||||
- `docs/research/phase-0.md` §Gaps #3, OQ-TLS-01 — the framing
|
||||
- alknet ADR-091 — `ConnectionCredentials` semantics
|
||||
- alknet ADR-030 §6 — fingerprint normalization
|
||||
- ADR-001 — the inheritance baseline
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# ADR-006: Module layout and test surface
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-10)
|
||||
|
||||
## Context
|
||||
|
||||
The extracted crate is organized as four modules plus the error
|
||||
(`lib.rs`, `server.rs`, `client.rs`, `pem.rs`, `signing.rs`) with all
|
||||
tests as in-module `#[cfg(test)]` blocks. alktls adds three modules of
|
||||
moved types (ADR-005) and, per the repo's conventions, needs a
|
||||
decided module map and test layout before the port starts.
|
||||
|
||||
Two shape questions need answers: where the moved types live
|
||||
relative to the config constructors, and how the seed tests port
|
||||
(no external workspace exists here, so in-module tests are the only
|
||||
cross-module surface a full-crate crate has).
|
||||
|
||||
## Decision
|
||||
|
||||
**Module layout** (one module per file, re-exported from `lib.rs`):
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib.rs — crate docs, TlsError (ADR-002), re-exports
|
||||
├── identity.rs — TlsIdentity, Ed25519SecretKey, AcmeDirectory (ADR-005)
|
||||
├── credentials.rs — ConnectionCredentials, RemoteIdentity (ADR-005)
|
||||
├── fingerprint.rs — fingerprint_from_cert_der, extract_ed25519_raw_key_from_spki,
|
||||
│ DER parser (ADR-005)
|
||||
├── server.rs — TlsServerConfig, build_rustls_server_config,
|
||||
│ RawKeyCertResolver, AcceptAnyCertVerifier,
|
||||
│ SelfSignedCert / generate_self_signed_cert
|
||||
├── client.rs — TlsClientConfig, select_server_verifier,
|
||||
│ build_client_auth, RawKeyClientCertResolver,
|
||||
│ NoClientCertResolver, FingerprintPinVerifier,
|
||||
│ load_platform_root_cert_store
|
||||
├── pem.rs — load_cert_chain, load_private_key
|
||||
└── signing.rs — Ed25519SigningKey (rustls SigningKey + Signer)
|
||||
```
|
||||
|
||||
Public API surface is `lib.rs` re-exports only (AGENTS.md convention
|
||||
12); modules are `pub` for discoverability but the re-export block is
|
||||
the documented surface.
|
||||
|
||||
**Feature gates stay on types and accessors, not modules.** The
|
||||
`noq` feature gates `for_noq()` + `NoqWrap`; the `tcp` feature gates
|
||||
`for_tcp_tls()`; the `acme` feature gates the ACME branch of
|
||||
`TlsServerConfig::new` + the spawned task + the `futures` dep. Unlike
|
||||
the alktunnels/alktty backends, there are no substrate modules to
|
||||
gate — the transport-specific code is accessor-shaped.
|
||||
|
||||
**Test surface:**
|
||||
|
||||
- Port the extracted crate's in-module tests as the seed (they assert
|
||||
the behavior-preservation invariants: `max_early_data_size` on the
|
||||
server paths, `enable_early_data` on the client config, the
|
||||
nine-scheme list, resolver behavior, PEM error paths, signing).
|
||||
- Add `tests/` integration tests for the cross-module surfaces a
|
||||
single crate has: `TlsServerConfig::new` → `for_noq()` /
|
||||
`for_tcp_tls()` round-trips per identity variant;
|
||||
`TlsClientConfig` verifier selection matrix (`Some`/`None` ×
|
||||
raw-key/X.509); the root-store fallback against an empty platform
|
||||
store; `acme-tls/1` append on the ACME path only.
|
||||
- Pin the exact nine-scheme list in an integration test
|
||||
(Phase 0 OQ-TLS-05's regression-proof shape — the extracted
|
||||
in-module test only checks membership of two schemes), and pin
|
||||
`enable_early_data = true` on the client config (the client half
|
||||
of the 0-RTT invariant).
|
||||
- `TlsIdentity::Acme`'s `unreachable!` in
|
||||
`build_rustls_server_config` becomes a config error (the extracted
|
||||
code relies on an internal dispatch invariant; the port replaces
|
||||
the `unreachable!` with the `AcmeConfig` variant, keeping
|
||||
no-panics-in-library-code).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- The port is mechanically guided: module map + seed tests make the
|
||||
Phase 2 port a diff against the extracted source.
|
||||
- The invariant assertions move from "documented" to "tested" at both
|
||||
unit and integration level.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- Integration tests that wrap real QUIC endpoints are out of scope
|
||||
(they belong to transport crates); the seam tests assert config
|
||||
construction, not handshakes — the scope boundary holds.
|
||||
|
||||
## References
|
||||
|
||||
- AGENTS.md conventions 1, 9, 12 — comments, feature gates, module
|
||||
structure
|
||||
- `docs/research/phase-0.md` §Gaps #5, OQ-TLS-05 — the test-surface
|
||||
question
|
||||
- ADR-002 — `TlsError` (no `Config(String)` catch-all)
|
||||
- ADR-005 — the moved modules
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# Open Questions
|
||||
|
||||
Centralized tracker for alktls. Promoted from Phase 0
|
||||
(`docs/research/phase-0.md` OQ-TLS-01..08) on 2026-09-10. Statuses here
|
||||
are authoritative; the Phase 0 doc's statuses are the historical record.
|
||||
|
||||
## Statuses at a glance
|
||||
|
||||
| OQ | Topic | Status | Priority |
|
||||
|----|-------|--------|----------|
|
||||
| OQ-TLS-01 | Config-type ownership | **resolved** (ADR-005) | high |
|
||||
| OQ-TLS-02 | `SelfSigned` on the client path | resolved (documented behavior kept) | low |
|
||||
| OQ-TLS-03 | `for_tcp_tls()` adoption | **resolved** (ADR-004) | medium |
|
||||
| OQ-TLS-04 | Accessor borrow-vs-consume | **resolved** (ADR-004) | medium |
|
||||
| OQ-TLS-05 | Test surface | **resolved** (ADR-006) | low |
|
||||
| OQ-TLS-06 | ACME task shutdown surface | resolved (detached-only for v1) | low |
|
||||
| OQ-TLS-07 | iroh key surface | **resolved** (ADR-005, byte access pinned) | low |
|
||||
| OQ-TLS-08 | `quinn` → `noq` feature rename | **resolved** (ADR-003) | high |
|
||||
|
||||
## Identity & types
|
||||
|
||||
### OQ-TLS-01: Where do the config types live, and what moves?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Gaps #3
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: high
|
||||
- **Resolution**: alktls owns the identity types (`TlsIdentity`,
|
||||
`Ed25519SecretKey`, `AcmeDirectory`), the credential bundle
|
||||
(`ConnectionCredentials`, `RemoteIdentity`), and the fingerprint
|
||||
helpers. The auth layer (`PeerEntry`, `AuthPolicy`,
|
||||
`IdentityProvider`) stays out — peer-level identity is not TLS.
|
||||
Decision recorded in [ADR-005](decisions/005-config-types-move-into-alktls.md).
|
||||
- **Cross-references**: ADR-001, ADR-005
|
||||
|
||||
### OQ-TLS-02: `SelfSigned` on the client path — encode or document?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Gaps #4
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: low
|
||||
- **Resolution**: keep the current behavior (present nothing via
|
||||
`NoClientCertResolver`) and document it on the identity type and in
|
||||
the [client spec](client.md). Type-level enforcement (config error,
|
||||
like `Acme`) was rejected: `SelfSigned` as a *local* identity
|
||||
meaning "present nothing" is coherent — the dev cert exists for the
|
||||
server side, and presenting a self-signed client cert would add
|
||||
nothing the fingerprint path uses.
|
||||
- **Consequences**: a future dev client-auth use case would need a
|
||||
type-level change (additive, not a one-way door).
|
||||
- **Cross-references**: ADR-001 (identity model), client spec
|
||||
|
||||
### OQ-TLS-07: iroh relationship in the rewrite
|
||||
|
||||
- **Origin**: docs/research/phase-0.md OQ-TLS-07
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: low
|
||||
- **Resolution**: the requirement is pinned and verified —
|
||||
`Ed25519SecretKey` keeps 32-byte raw access (`from_bytes` /
|
||||
`as_bytes`) with the same byte-level surface iroh's
|
||||
`iroh_base::SecretKey` consumes (`from_bytes` / `to_bytes`; 32 raw
|
||||
bytes in/out — verified against iroh 1.1 in Phase 0). iroh stays
|
||||
key-not-config; no `for_iroh()`. The rewrite's iroh dial consumes
|
||||
the type alktls owns (ADR-005); no alktls-side work remains.
|
||||
- **Cross-references**: ADR-003, ADR-005
|
||||
|
||||
## API surface
|
||||
|
||||
### OQ-TLS-03: `for_tcp_tls()` — adopt the spec accessor?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Gaps #2
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: medium
|
||||
- **Resolution**: adopted — `for_tcp_tls(&self) ->
|
||||
tokio_rustls::TlsAcceptor`, feature-gated on `tcp`, infallible.
|
||||
Decision and rationale in [ADR-004](decisions/004-accessor-surface.md).
|
||||
- **Cross-references**: ADR-004
|
||||
|
||||
### OQ-TLS-04: `for_quinn()` — `self` or `&self`?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md OQ-TLS-04
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: medium
|
||||
- **Resolution**: server accessors take `&self` (one config feeds N
|
||||
transports — the ADR-082 story, directly expressible); client
|
||||
accessors consume `self` (per-dial build, zero-cost
|
||||
`into_rustls_config`). Full rationale in
|
||||
[ADR-004](decisions/004-accessor-surface.md).
|
||||
- **Cross-references**: ADR-003, ADR-004
|
||||
|
||||
### OQ-TLS-08: `quinn` feature vs `noq` feature — and the MSRV floor
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Prior art: noq
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: high
|
||||
- **Resolution**: the feature is `noq` (no `quinn` feature ever
|
||||
published; rename is pre-consumer and free). noq 1.2 pinned
|
||||
(`"1.2"`, default-features off, `rustls` feature), bump
|
||||
deliberately. The MSRV half of the OQ was separately resolved the
|
||||
same day (ecosystem floor 1.88; `time` pin dropped). Decision in
|
||||
[ADR-003](decisions/003-noq-replaces-quinn.md).
|
||||
- **Cross-references**: ADR-002, ADR-003, ADR-004
|
||||
|
||||
## Quality / process
|
||||
|
||||
### OQ-TLS-05: Test surface for the invariants
|
||||
|
||||
- **Origin**: docs/research/phase-0.md §Gaps #5
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: low
|
||||
- **Resolution**: port the in-module seed tests AND add `tests/`
|
||||
integration tests for the cross-module surfaces; pin the exact
|
||||
nine-scheme list (regression-proof). Decision in
|
||||
[ADR-006](decisions/006-module-layout-and-tests.md).
|
||||
- **Cross-references**: ADR-006
|
||||
|
||||
### OQ-TLS-06: Does the ACME state machine need a shutdown surface?
|
||||
|
||||
- **Origin**: docs/research/phase-0.md OQ-TLS-06
|
||||
- **Status**: resolved (2026-09-10)
|
||||
- **Priority**: low
|
||||
- **Resolution**: detached-only — the handle is stored (keeping
|
||||
`TlsServerConfig` non-Clone) and never aborted; the ACME task runs
|
||||
for the process lifetime. Documented on the type.
|
||||
- **Consequences**: a `shutdown()` (abort + await) surface would be
|
||||
additive and cheap if the rewrite's graceful-shutdown design wants
|
||||
one later — not a one-way door.
|
||||
- **Cross-references**: ADR-001 (one-ACME-machine rule),
|
||||
[server.md](server.md)
|
||||
|
||||
## Deferred / Blocked
|
||||
|
||||
(none — all promoted OQs are resolved; new OQs added during review
|
||||
land here with their deferral half per `docs/sdd_process.md`)
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Overview
|
||||
|
||||
## Purpose
|
||||
|
||||
alktls 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. It is the extraction of the TLS handling from alknet
|
||||
(`crates/alknet-tls` + the identity/credential/fingerprint types in
|
||||
`crates/alknet-core`), and it is the TLS crate the alknet rewrite
|
||||
consumes.
|
||||
|
||||
The crate owns **config construction**: given an identity and an ALPN
|
||||
list, produce a `rustls::ServerConfig` or `rustls::ClientConfig`, and
|
||||
hand it to whichever transport wrapper the deployment runs. It does
|
||||
not dial, accept, dispatch, or resolve peer identities — those are
|
||||
the dial seam's, the accept loop's, and the auth layer's jobs
|
||||
(alknet ADR-083/089; the scope boundary in ADR-001).
|
||||
|
||||
**Terminology.** The **assembly layer** is the deployment binary that
|
||||
builds the `TlsServerConfig`s / `TlsClientConfig`s, builds the
|
||||
transports from them, and wires the results together (alknet
|
||||
ADR-014's term — in practice, the hub/worker/endpoint binary). The
|
||||
dial seam is the outbound connection point the assembly layer (or a
|
||||
client crate) consumes; the accept loop is the inbound counterpart.
|
||||
alktls is the cert provider for both, never the loops themselves.
|
||||
|
||||
## The transport picture
|
||||
|
||||
A deployment (endpoint / hub) assembles a subset of three transports;
|
||||
alktls serves the first two directly and feeds the third a key:
|
||||
|
||||
| Transport | Stack | What alktls provides |
|
||||
|-----------|-------|----------------------|
|
||||
| TCP+TLS | `tokio-rustls` | `TlsServerConfig::for_tcp_tls()` → `TlsAcceptor` (feature `tcp`); `TlsClientConfig::into_rustls_config()` → `TlsConnector` (ungated) |
|
||||
| QUIC | `noq` (iroh's extracted quinn fork) | `for_noq()` on both configs (feature `noq`) |
|
||||
| iroh | iroh's own TLS | nothing — `Ed25519SecretKey` 32-byte access feeds `iroh_base::SecretKey` (key-not-config) |
|
||||
|
||||
One identity, N transports: the inner rustls config is Clone
|
||||
(Arc-shared resolvers); one `TlsServerConfig` feeds every transport an
|
||||
endpoint runs. One ACME state machine per domain, shared across
|
||||
transports — duplicate orders risk Let's Encrypt rate limits and
|
||||
cert-cache divergence (ADR-001).
|
||||
|
||||
## What the crate is
|
||||
|
||||
The public API surface (ADR-004, ADR-005):
|
||||
|
||||
- `TlsServerConfig` — built once from a `TlsIdentity` + ALPN list;
|
||||
accessors `for_noq()`, `for_tcp_tls()`, `rustls_config()`. Not
|
||||
`Clone` (holds the ACME task's `JoinHandle`); share via `Arc`.
|
||||
- `TlsClientConfig` — built per dial from `ConnectionCredentials` +
|
||||
ALPN; accessors `for_noq()`, `into_rustls_config()`.
|
||||
- `TlsError` — the ADR-088 six-variant `#[non_exhaustive]`
|
||||
config-construction error type (ADR-002).
|
||||
- Identity types — `TlsIdentity` (`X509` / `RawKey` / `SelfSigned` /
|
||||
`Acme`), `Ed25519SecretKey`, `AcmeDirectory` (ADR-005).
|
||||
- Credential bundle — `ConnectionCredentials`, `RemoteIdentity`
|
||||
(ADR-005); drives the verifier selection matrix (alknet ADR-034).
|
||||
- Fingerprint helpers — `fingerprint_from_cert_der`,
|
||||
`extract_ed25519_raw_key_from_spki` (ADR-005); normalized
|
||||
`ed25519:<hex>` / `SHA256:<hex>` formats.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [001](decisions/001-inherit-alknet-tls-design.md) | Inherit the alknet TLS design | The alknet ADRs + crate spec are the baseline; deviations recorded as alktls ADRs |
|
||||
| [002](decisions/002-tlserror-shape.md) | `TlsError` — ADR-088 shape from day one | Six typed variants, `#[non_exhaustive]`, no string catch-all; config-construction scope boundary |
|
||||
| [003](decisions/003-noq-replaces-quinn.md) | The QUIC feature is `noq` | noq 1.2 (iroh's extracted fork) replaces quinn pre-consumer; iroh stays key-not-config |
|
||||
| [004](decisions/004-accessor-surface.md) | Complete the accessors | `for_tcp_tls()` adopted; server accessors borrow (`&self`), client accessors consume |
|
||||
| [005](decisions/005-config-types-move-into-alktls.md) | Config types move into alktls | Identity + credentials + fingerprint move in; auth layer stays out |
|
||||
| [006](decisions/006-module-layout-and-tests.md) | Module layout and test surface | Eight modules; in-module seed tests + integration invariant pins |
|
||||
|
||||
## Open Questions
|
||||
|
||||
Open questions are tracked in [open-questions.md](open-questions.md).
|
||||
All Phase 0 questions (OQ-TLS-01..08) were resolved at Phase 1 entry
|
||||
(2026-09-10) — six via ADR-001..006, two resolved as documented
|
||||
behavior (OQ-TLS-02, OQ-TLS-06); the tracker records the resolutions
|
||||
with pointers.
|
||||
|
||||
## Components
|
||||
|
||||
- [server.md](server.md) — `TlsServerConfig`, resolvers, the ACME
|
||||
path, and the server-side invariants
|
||||
- [client.md](client.md) — `TlsClientConfig`, verifier selection,
|
||||
client-auth presentation, the root-store fallback
|
||||
|
||||
## Relationship to the alknet docs
|
||||
|
||||
The full design rationale lives in alknet's architecture docs
|
||||
(`/workspace/@alkdev/alknet/docs/architecture/`): ADR-082 (why a
|
||||
standalone TLS crate — the cert-reuse problem), ADR-083 (why the
|
||||
endpoint takes no TLS config), ADR-084 (why aws-lc-rs), ADR-027 (the
|
||||
identity model), ADR-034 (verifier selection), ADR-086 §3 (split ALPN
|
||||
lists), ADR-087 (`TlsClientConfig`), ADR-088 (`TlsError`, root-store
|
||||
fallback), ADR-089 (the dial seam), ADR-091 (`ConnectionCredentials`),
|
||||
and `crates/tls/README.md` (the full crate spec). alktls' ADR series
|
||||
records its own posture and points at the alknet reasoning rather
|
||||
than duplicating it.
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# alktls — Server side
|
||||
|
||||
`TlsServerConfig` and its resolvers: the server-side TLS setup,
|
||||
extracted from alknet (`crates/alknet-tls/src/server.rs`) and ported
|
||||
per the ADRs. Statuses of the decisions referenced here: see
|
||||
[open-questions.md](open-questions.md) and the ADR index in
|
||||
[overview.md](overview.md).
|
||||
|
||||
## `TlsServerConfig`
|
||||
|
||||
The central server-side type. Built once from a `TlsIdentity` + ALPN
|
||||
list, shared across transports via `Arc` (not `Clone` — it holds the
|
||||
ACME task's `JoinHandle`).
|
||||
|
||||
```rust
|
||||
pub struct TlsServerConfig {
|
||||
rustls_config: rustls::ServerConfig, // Clone-safe — Arc internally
|
||||
acme_handle: Option<tokio::task::JoinHandle<()>>, // acme-gated
|
||||
}
|
||||
|
||||
impl TlsServerConfig {
|
||||
pub async fn new(identity: &TlsIdentity, alpns: &[Vec<u8>])
|
||||
-> Result<Self, TlsError>;
|
||||
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(&self) -> Result<noq::ServerConfig, TlsError>;
|
||||
|
||||
#[cfg(feature = "tcp")]
|
||||
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor;
|
||||
|
||||
pub fn rustls_config(&self) -> &rustls::ServerConfig;
|
||||
}
|
||||
```
|
||||
|
||||
Construction dispatch (ADR-001's identity model):
|
||||
|
||||
- `X509 { cert, key }` — loads the chain + key from disk (`pem.rs`),
|
||||
`with_single_cert`.
|
||||
- `RawKey(Ed25519SecretKey)` — `RawKeyCertResolver` presents the
|
||||
Ed25519 key as an RFC 7250 raw public key server cert.
|
||||
- `SelfSigned` — `generate_self_signed_cert()` (rcgen), in-memory.
|
||||
- `Acme { domains, cache_dir, directory, contact }` — the ACME path
|
||||
(below). **Server-only**: on the client path it is a config error.
|
||||
|
||||
## Behavior-preservation invariants
|
||||
|
||||
These are load-bearing (ADR-001); an implementation that omits any of
|
||||
them compiles but silently changes TLS behavior. Each is asserted by
|
||||
test (ADR-006):
|
||||
|
||||
- **`max_early_data_size = u32::MAX`** on every server config path —
|
||||
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). Never `ring`, never the
|
||||
process-default provider, without a new ADR.
|
||||
- **`AcceptAnyCertVerifier::supported_verify_schemes()`** returns
|
||||
ED25519 + ECDSA P-256/P-384 + RSA PSS (SHA256/384/512) + RSA PKCS1
|
||||
(SHA256/384/512) — nine schemes, verbatim, pinned by an exact-list
|
||||
integration test.
|
||||
- **`acme-tls/1` ALPN append** for the ACME path only, done by the
|
||||
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 (see [client.md](client.md)).
|
||||
|
||||
## `AcceptAnyCertVerifier`
|
||||
|
||||
The server-side client-cert verifier: **request-but-don't-require**.
|
||||
It asks for a client cert (X.509 or RFC 7250 raw key) so the caller
|
||||
can extract the fingerprint via `peer_identity()`, but does not
|
||||
require one and does not verify the presented cert against a CA. The
|
||||
fingerprint is matched against peer records by the auth layer
|
||||
(`IdentityProvider::resolve_from_fingerprint`) *outside* this crate —
|
||||
the TLS crate hands over the fingerprint string; peer resolution is
|
||||
not a TLS concern (ADR-005).
|
||||
|
||||
Server-side only: this must not be reused as a client-side
|
||||
`ServerCertVerifier` — client-side verification is alknet ADR-034's
|
||||
selection matrix (see [client.md](client.md)).
|
||||
|
||||
## `RawKeyCertResolver`
|
||||
|
||||
Presents an `Ed25519SecretKey` as an RFC 7250 raw public key server
|
||||
certificate: the SPKI DER (Ed25519 OID + 32-byte key) is the "cert",
|
||||
`only_raw_public_keys() == true`, and the signing key is the shared
|
||||
`Ed25519SigningKey` helper (`signing.rs`).
|
||||
|
||||
## The ACME path
|
||||
|
||||
For `TlsIdentity::Acme`, `new` (feature `acme`):
|
||||
|
||||
1. Builds `rustls_acme::AcmeConfig` — the upstream builder, distinct
|
||||
from `TlsError::AcmeConfig` (domains, `DirCache` cache dir,
|
||||
directory URL from `AcmeDirectory`, contacts).
|
||||
2. Wires `state.resolver()` as the cert resolver into the server
|
||||
config.
|
||||
3. Appends `acme-tls/1` to the ALPN list (TLS-ALPN-01 challenge).
|
||||
4. Spawns the event-loop task (`tokio::spawn`) matching
|
||||
`EventOk`/`EventError` variants to `tracing` logs, and returns
|
||||
**immediately** — it does not await the first certificate.
|
||||
|
||||
Lifecycle semantics:
|
||||
|
||||
- The returned config is usable right away; handshakes fail
|
||||
transiently until the first order completes (or a cached cert
|
||||
deploys).
|
||||
- Order errors log at `warn!` and retry inside `rustls-acme`; the
|
||||
task exits only when the event stream ends.
|
||||
- The task is detached: the stored `JoinHandle` is never aborted;
|
||||
ACME runs for the process lifetime (OQ-TLS-06 resolved
|
||||
detached-only; a `shutdown()` surface would be additive later if
|
||||
the rewrite's graceful-shutdown design wants one).
|
||||
- **One state machine per domain** — never spawn a second ACME task
|
||||
for an already-served domain (duplicate orders = Let's Encrypt
|
||||
rate-limit risk + cert-cache divergence). This is why
|
||||
`TlsServerConfig` is not `Clone`.
|
||||
- Runtime errors are stream events, not `TlsError` variants
|
||||
(ADR-002's scope boundary). `TlsError::AcmeConfig` covers
|
||||
config-mismatch mistakes ("ACME feature not enabled but `Acme`
|
||||
configured" — and on the client path, `Acme` used for client
|
||||
auth), not runtime failures.
|
||||
|
||||
## What the server side does NOT do
|
||||
|
||||
- No accept loop: `for_tcp_tls()` yields a `TlsAcceptor`; the
|
||||
`TcpListener::accept` → `TlsAcceptor::accept` → dispatch loop
|
||||
belongs to the caller (alknet ADR-083 — the endpoint takes no TLS
|
||||
config; the assembly layer builds configs and transports).
|
||||
- No ALPN policy: the ALPN list is a parameter (the assembly layer
|
||||
filters per endpoint type — alknet ADR-086 §3); the crate appends
|
||||
only `acme-tls/1` on the ACME path.
|
||||
- No handshake: verifier selection and handshake outcomes on the
|
||||
*server* side are `AcceptAnyCertVerifier` + the caller's
|
||||
fingerprint extraction; a rejected handshake is the transport's
|
||||
error, not `TlsError`.
|
||||
- No peer resolution: the extracted fingerprint string goes to the
|
||||
caller; `PeerEntry`/`AuthPolicy` live in the auth layer.
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](overview.md) — the index; [client.md](client.md) —
|
||||
the client side
|
||||
- alknet `crates/tls/README.md` §Architecture — the full server-side
|
||||
spec this doc mirrors
|
||||
- ADR-001 (invariants), ADR-002 (`TlsError`), ADR-003 (`for_noq`),
|
||||
ADR-004 (accessors), ADR-005 (identity types), ADR-006 (modules,
|
||||
tests)
|
||||
@@ -614,8 +614,10 @@ All internal; this crate has no external research debt:
|
||||
- [x] Open questions enumerated (OQ-TLS-01..08) — 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)
|
||||
- [x] Open questions promoted to Phase 1
|
||||
`docs/architecture/open-questions.md` with statuses — DONE
|
||||
2026-09-10 (OQ-TLS-01..08 promoted; all resolved at Phase 1
|
||||
entry via ADR-001..006 or documented behavior)
|
||||
- [ ] 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
|
||||
Reference in New Issue
Block a user