docs(adr-027): TLS identity redesign — ACME + RawKey decoupling

ADR-027 resolves the architectural gap surfaced when ACME integration
became a concrete target:

1. TlsIdentity::Acme variant — static config data (domains, cache_dir,
   directory, contact) with async AcmeState constructed at endpoint
   setup via two-phase TlsSetup (not stuffed into the Clone-able enum).

2. TlsIdentity::RawKey decoupled from the iroh feature — uses
   Ed25519SecretKey (alknet-core-owned wrapper over ed25519_dalek)
   instead of iroh::SecretKey. Raw-key TLS identity (RFC 7250, the
   default for most alknet nodes) now works in quinn-only builds.
   iroh transport converts via SecretKey::from_bytes.

3. ACME feature-gated behind new acme feature (rustls-acme optional
   dep). Non-ACME builds don't compile it.

4. dispatch_quinn guard for acme-tls/1 challenge connections — TLS-ALPN-01
   is handled at the rustls cert resolver layer during the handshake;
   the guard closes challenge connections gracefully instead of logging
   a misleading "no handler" warning.

Research confirmed QUIC (quinn) handles ACME challenges differently than
TCP (reverse-proxy): quinn gives no ClientHello peek hook, but the
challenge is fully answered at the cert resolution step before the
connection surfaces to the application. No handler registration needed.

Spec updates: config.md, endpoint.md, open-questions.md (OQ-12),
overview.md + README.md (ADR index), ADR-010 (cross-ref).

Tasks: core/rawkey-decouple-from-iroh (gen 1, no deps),
core/acme-integration (gen 2, depends on rawkey). Graph: 36 tasks.
This commit is contained in:
2026-06-24 12:29:24 +00:00
parent 97216764ea
commit d94d7a132a
9 changed files with 669 additions and 17 deletions

View File

@@ -41,15 +41,24 @@ pub enum TlsIdentity {
/// RFC 7250 raw Ed25519 public key.
/// No domain, no CA, no cert renewal. Key = identity.
/// Same model as iroh's NodeId, but for direct QUIC connections.
/// `SecretKey` is `iroh::SecretKey` (Ed25519) — re-exported from iroh,
/// which alknet-core already depends on (feature-gated, ADR-010). The
/// key can be derived from alknet-vault at the assembly layer
/// (endpoint.md) or generated fresh. See OQ-12, W14.
RawKey(iroh::SecretKey),
/// Uses `Ed25519SecretKey` (alknet-core-owned wrapper over
/// `ed25519_dalek::SigningKey`) — not coupled to the `iroh` feature.
/// Available in quinn-only builds. See ADR-027.
RawKey(Ed25519SecretKey),
/// Self-signed X.509 cert for development.
/// Generated on startup, not validated by external clients.
SelfSigned,
/// ACME auto-provisioning via Let's Encrypt (rustls-acme).
/// Produces X.509 certs at runtime; handles TLS-ALPN-01 challenges
/// and automatic renewal. Feature-gated behind `acme`. See ADR-027.
Acme {
domains: Vec<String>,
cache_dir: PathBuf,
directory: AcmeDirectory, // Production, Staging, Custom(url)
contact: Vec<String>, // e.g. ["mailto:admin@example.com"]
},
}
```
@@ -57,7 +66,52 @@ pub enum TlsIdentity {
TLS identity in alknet has two distinct use cases, not one. The original `tls_cert: Option<PathBuf>` / `tls_key: Option<PathBuf>` assumed X.509 was the only TLS identity model. RFC 7250 raw public keys (used by iroh, supported by rustls) provide a fundamentally different mode: Ed25519 key as identity, no X.509, no CA, no domain. This is the default for most alknet nodes — it works natively with SSH auth and git. X.509 certs are for domain-hosted services and browser/WebTransport clients, which don't support RFC 7250.
The `TlsIdentity` enum captures both use cases plus a development mode. See OQ-12 for the full rationale.
The `TlsIdentity` enum captures all four modes. See OQ-12 for the use-case
rationale and [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md)
for the ACME + RawKey decoupling design.
### `Ed25519SecretKey`
A thin alknet-core-owned wrapper over `ed25519_dalek::SigningKey`. Not
feature-gated — available in all builds. Used by `TlsIdentity::RawKey`
for RFC 7250 raw public key TLS identity. When the `iroh` transport is
configured, `build_iroh_endpoint` converts to `iroh::SecretKey::from_bytes`
(see ADR-027, Decision 4).
### `AcmeDirectory`
```rust
pub enum AcmeDirectory {
Production, // Let's Encrypt production
Staging, // Let's Encrypt staging
Custom(String), // custom ACME directory URL
}
```
### Construction examples (updated)
```rust
// P2P / key-based identity (default for most nodes) — no iroh dep needed
let p2p_config = StaticConfig {
listen_addr: Some("0.0.0.0:4433".parse()?),
tls_identity: Some(TlsIdentity::RawKey(Ed25519SecretKey::generate())),
iroh_relay: None,
drain_timeout: Duration::from_secs(2),
};
// Domain-hosted service with ACME auto-provisioning
let acme_config = StaticConfig {
listen_addr: Some("0.0.0.0:443".parse()?),
tls_identity: Some(TlsIdentity::Acme {
domains: vec!["relay.alk.dev".to_string()],
cache_dir: "/var/lib/alknet/acme".into(),
directory: AcmeDirectory::Production,
contact: vec!["mailto:admin@alk.dev".to_string()],
}),
iroh_relay: None,
drain_timeout: Duration::from_secs(2),
};
```
### Key differences from reference implementation
@@ -80,12 +134,12 @@ The reference `StaticConfig` (in `alknet-main/crates/alknet-core/src/config/stat
// P2P / key-based identity (default for most nodes)
let p2p_config = StaticConfig {
listen_addr: Some("0.0.0.0:4433".parse()?),
tls_identity: Some(TlsIdentity::RawKey(iroh::SecretKey::generate())),
tls_identity: Some(TlsIdentity::RawKey(Ed25519SecretKey::generate())),
iroh_relay: None,
drain_timeout: Duration::from_secs(2),
};
// Domain-hosted service (relays, public services, browsers)
// Domain-hosted service (relays, public services, browsers) — manual certs
let domain_config = StaticConfig {
listen_addr: Some("0.0.0.0:4433".parse()?),
tls_identity: Some(TlsIdentity::X509 {

View File

@@ -206,7 +206,7 @@ This mode works natively with SSH auth (same key type) and git (SSH key-based au
Nodes that serve browser/WebTransport clients, or nodes with public domain names, use X.509 certificates. This has two sub-cases:
- **Manual**: Provide cert/key file paths via `TlsIdentity::X509`. The endpoint loads them at startup and builds a standard `rustls::ServerConfig`.
- **ACME auto-provisioning**: Let's Encrypt via `rustls-acme`. The reverse-proxy project (`/workspace/@alkdev/reverse-proxy`) demonstrates the complete pattern: per-listener ACME state machine, `ResolvesServerCertAcme` rustls integration, TLS-ALPN-01 challenge handling, automatic renewal. This is a proven, solved implementation pattern. It will be adapted to alknet's `AlknetEndpoint` context as an additional `TlsIdentity` variant or `ResolvesServerCert` implementation.
- **ACME auto-provisioning**: Let's Encrypt via `rustls-acme`. `TlsIdentity::Acme { domains, cache_dir, directory, contact }` carries the static config; the endpoint constructs the `AcmeState` async state machine and `ResolvesServerCertAcme` at setup time (ADR-027). The `acme` feature gate keeps `rustls-acme` out of non-ACME builds. See [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) for the full design.
`TlsIdentity::SelfSigned` is for development only — the endpoint generates a self-signed cert on startup. External clients will not trust it.
@@ -219,10 +219,17 @@ The iroh endpoint does not need TLS certificate configuration — it uses `NodeI
| Path | Identity model | Client compatibility | Use case |
|------|---------------|---------------------|----------|
| quinn + `TlsIdentity::RawKey` | RFC 7250 Ed25519 raw key | alknet-native, SSH, git | Personal nodes, P2P, most deployments |
| quinn + `TlsIdentity::X509` | X.509 domain certificate | All clients including browsers | Relays, public services, WebTransport |
| quinn + `TlsIdentity::X509` | X.509 domain certificate (manual) | All clients including browsers | Relays, public services, WebTransport |
| quinn + `TlsIdentity::Acme` | X.509 via ACME auto-provisioning | All clients including browsers | Public relays, domain-hosted services |
| quinn + `TlsIdentity::SelfSigned` | X.509 self-signed cert | None (dev only) | Local development |
| iroh | NodeId (Ed25519, RFC 7250 built-in) | alknet-native, iroh clients | NAT traversal, home servers |
Note: `TlsIdentity::RawKey` uses `Ed25519SecretKey` (alknet-core-owned,
backed by `ed25519-dalek`), not `iroh::SecretKey`. It is available in
quinn-only builds without the `iroh` feature. When the iroh transport is
also configured, `build_iroh_endpoint` converts the key to
`iroh::SecretKey::from_bytes` (ADR-027).
## Graceful Shutdown
```rust
@@ -294,4 +301,4 @@ See [open-questions.md](../../open-questions.md) for full details.
- **OQ-04**: Resolved — HandlerRegistry is static at startup.
- **OQ-05**: Resolved — multi-connectivity endpoint with quinn + iroh, both feature-gated.
- **OQ-12**: Resolved — two distinct TLS identity use cases: RFC 7250 raw keys (default, P2P) and X.509 certs (domain-hosted, browsers). ACME is a proven pattern from the reverse-proxy project, not speculative future work.
- **OQ-12**: Resolved — two distinct TLS identity use cases: RFC 7250 raw keys (default, P2P) and X.509 certs (domain-hosted, browsers). ACME auto-provisioning designed in [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md); RawKey decoupled from the `iroh` feature (available in quinn-only builds).