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:
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user