Three tasks implementing ADR-027: 1. core/rawkey-decouple-from-iroh: TlsIdentity::RawKey now uses Ed25519SecretKey (alknet-core-owned wrapper over ed25519_dalek) instead of iroh::SecretKey. RawKeyCertResolver and Ed25519SigningKey un-gated from #[cfg(all(quinn, iroh))] to #[cfg(quinn)] only. Quinn-only builds (default) now support RFC 7250 raw-key identity. iroh transport converts via iroh::SecretKey::from_bytes. 2. core/endpoint-request-client-cert: replaced with_no_client_auth() with AcceptAnyCertVerifier — a custom ClientCertVerifier that requests client certs but doesn't require them or verify against a CA. alknet's identity model is fingerprint-based (the authorized_fingerprints set is the trust anchor), not PKI-based. Peer certs are extracted at the TLS layer for fingerprinting; peers without certs connect normally. 3. core/acme-integration: TlsIdentity::Acme variant (domains, cache_dir, directory, contact) + AcmeDirectory enum. TlsSetup two-phase construction: synchronous for X509/RawKey/SelfSigned, async for Acme (spawns AcmeState event loop, builds ServerConfig with ResolvesServerCertAcme). acme-tls/1 ALPN added when ACME is active; dispatch_quinn guard closes challenge connections gracefully (challenge is TLS-layer-handled). acme feature gate keeps rustls-acme out of non-ACME builds. Workspace: build/test/clippy green across all 3 feature configs (quinn-only, quinn+iroh, quinn+acme, all-features). 331 tests, 0 failures, 0 warnings.
7.8 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| core/acme-integration | Add ACME auto-provisioning via rustls-acme (ADR-027) | completed |
|
moderate | medium | component | implementation |
Description
Implement ACME auto-provisioning (Let's Encrypt) for alknet endpoints,
following ADR-027. Adds TlsIdentity::Acme, a new acme feature gate,
a two-phase server-config construction (TlsSetup), and a
dispatch_quinn guard for acme-tls/1 challenge connections.
The reverse-proxy project (/workspace/@alkdev/reverse-proxy/src/tls/)
demonstrates the proven pattern: AcmeConfig, AcmeState event loop,
ResolvesServerCertAcme, TLS-ALPN-01 challenge handling, DirCache for
cert persistence. This task adapts that pattern to alknet's quinn-based
endpoint.
Implementation steps
-
Add
acmefeature to alknet-coreCargo.toml:[features] acme = ["dep:rustls-acme"] [dependencies] rustls-acme = { version = "0.12", optional = true, features = ["aws-lc-rs"] }Use the same version as reverse-proxy (
=0.12.1or compatible). Confirm the exact version against the latest available and the reverse-proxy'sCargo.toml. -
Add
TlsIdentity::Acmevariant and supporting types inconfig.rs:pub enum TlsIdentity { X509 { cert: PathBuf, key: PathBuf }, RawKey(Ed25519SecretKey), SelfSigned, Acme { domains: Vec<String>, cache_dir: PathBuf, directory: AcmeDirectory, contact: Vec<String>, }, } pub enum AcmeDirectory { Production, Staging, Custom(String), }Acmeholds only static,Clone/Debug-safe data. NoAcmeState. -
Introduce
TlsSetupinendpoint.rs— the two-phase construction (ADR-027 Decision 2):struct TlsSetup { server_config: rustls::ServerConfig, acme_state_handle: Option<tokio::task::JoinHandle<()>>, } impl TlsSetup { async fn new( tls_identity: &TlsIdentity, alpns: &[Vec<u8>], ) -> Result<Self, EndpointError> { match tls_identity { TlsIdentity::X509 { .. } | TlsIdentity::SelfSigned | TlsIdentity::RawKey(_) => { // synchronous path (current build_rustls_server_config) let config = build_rustls_server_config(tls_identity, alpns)?; Ok(Self { server_config: config, acme_state_handle: None }) } TlsIdentity::Acme { domains, cache_dir, directory, contact } => { #[cfg(feature = "acme")] { Self::new_acme(domains, cache_dir, directory, contact, alpns).await } #[cfg(not(feature = "acme"))] { Err(EndpointError::TlsConfig(io::Error::other("ACME feature not enabled"))) } } } } } -
Implement
TlsSetup::new_acme(#[cfg(feature = "acme")]):- Build
AcmeConfig::new(domains)withDirCache::new(cache_dir), directory URL (fromAcmeDirectory), and contact. - Get
state = acme_config.state()andresolver = state.resolver(). - Build
rustls::ServerConfigwithwith_cert_resolver(resolver)(NOTwith_single_cert). - Append
b"acme-tls/1"toalpn_protocolsalongside handler ALPNs. - Spawn the
AcmeStateevent loop as a tokio task (pattern fromreverse-proxy/src/tls/acme.rs:spawn_acme_state). LogDeployedCachedCert,DeployedNewCert, and error events. - Return
TlsSetup { server_config, acme_state_handle: Some(handle) }.
- Build
-
Wire
TlsSetupinto the endpoint construction: replace the directbuild_quinn_server_configcall in the accept loop setup withTlsSetup::new(...).await?. Theacme_state_handleis stored onAlknetEndpoint(or the accept loop context) so it can be aborted on shutdown. -
Add
acme-tls/1guard indispatch_quinn(ADR-027 Decision 5):if alpn == b"acme-tls/1" { debug!("acme-tls/1 challenge connection completed at TLS layer; closing"); connection.close(0u32.into(), b"acme done"); return; }Place this before the
handlers.get(&alpn)lookup. This is#[cfg(feature = "acme")]— without the feature, the guard is absent andacme-tls/1is never in the ALPN list. -
Shutdown: abort the
acme_state_handleJoinHandle inAlknetEndpoint::shutdown()alongside the existing shutdown logic.
ACME challenge handling (from research)
The ResolvesServerCertAcme resolver intercepts TLS-ALPN-01 challenges
at the cert resolution step — during the TLS handshake, before the
connection surfaces to the application. The challenge cert (with the
SHA-256 key authorization in its SAN) is served by the resolver; the CA
validates it during the handshake. By the time dispatch_quinn runs,
the challenge already succeeded. The acme-tls/1 guard just closes the
connection gracefully instead of logging a misleading "no handler"
warning.
Key constraint: ACME requires with_cert_resolver, not
with_single_cert. The acme-tls/1 ALPN must be in
alpn_protocols or the challenge handshake aborts with
no_application_protocol.
What NOT to change
TlsIdentity::X509,RawKey,SelfSignedconstruction paths — unchanged (the RawKey decoupling is done by the predecessor task).- iroh endpoint — ACME is quinn-only (iroh uses its own TLS).
endpoint-request-client-cert— independent task, can proceed in parallel.
Acceptance Criteria
acmefeature added to alknet-core withrustls-acmeas optional depTlsIdentity::Acmevariant exists withdomains,cache_dir,directory,contactAcmeDirectoryenum exists (Production, Staging, Custom)TlsSetuptwo-phase construction: synchronous for X509/RawKey/SelfSigned, async for Acme- ACME path uses
with_cert_resolver(ResolvesServerCertAcme), notwith_single_cert acme-tls/1added toalpn_protocolswhen ACME is configureddispatch_quinnhasacme-tls/1guard (closes silently, no "no handler" warning)- ACME state machine spawned as tokio task, aborted on endpoint shutdown
TlsIdentity::Acmewithoutacmefeature returns a clear error at endpoint construction- Unit test:
AcmeDirectoryresolves to correct Let's Encrypt URLs (staging vs production) - Unit test:
TlsSetup::newwithX509/RawKey/SelfSignedreturnsacme_state_handle: None cargo build -p alknet-core --features quinn(no acme) succeeds — no rustls-acme compiledcargo build -p alknet-core --features "quinn acme"succeedscargo test -p alknet-core --all-featuressucceedscargo clippy -p alknet-core --all-features --all-targetscleancargo clippy -p alknet-core --features quinn --all-targetsclean (no acme, no warnings)
References
- ADR-027 — full design (two-phase construction, challenge handling, feature gate)
- /workspace/@alkdev/reverse-proxy/src/tls/acme.rs —
AcmeTlsConfig,spawn_acme_state(proven pattern) - /workspace/@alkdev/reverse-proxy/src/tls/acceptor.rs —
build_acme_server_config,acme-tls/1ALPN - crates/alknet-core/src/endpoint.rs:286-314 —
dispatch_quinn(guard insertion site) - crates/alknet-core/src/endpoint.rs:464-509 —
build_rustls_server_config(TlsSetup replaces this for Acme) - crates/alknet-core/src/config.rs:33-41 —
TlsIdentityenum (new Acme variant)
Notes
Depends on
core/rawkey-decouple-from-irohbecause both modifyTlsIdentityandbuild_rustls_server_config. The decoupling task cleans up the enum shape first; this task adds the Acme variant on top. Theacmefeature gate is critical — it keepsrustls-acmeand its deps out of non-ACME builds. The reverse-proxy project is the reference implementation; adapt its event loop logging and cache patterns.