feat(core): ADR-027 — RawKey decoupling, client cert request, ACME integration

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.
This commit is contained in:
2026-06-24 20:29:43 +00:00
parent d94d7a132a
commit 00edfc0889
8 changed files with 607 additions and 37 deletions

217
Cargo.lock generated
View File

@@ -71,6 +71,7 @@ dependencies = [
"arc-swap",
"async-trait",
"bytes",
"ed25519-dalek",
"futures",
"hex",
"iroh",
@@ -78,11 +79,13 @@ dependencies = [
"rand 0.8.6",
"rcgen 0.13.2",
"rustls",
"rustls-acme",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"sha2",
"tempfile",
"thiserror 2.0.18",
"tokio",
"toml",
@@ -212,6 +215,18 @@ dependencies = [
"syn",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-compat"
version = "0.2.5"
@@ -225,6 +240,54 @@ dependencies = [
"tokio",
]
[[package]]
name = "async-http-codec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "096146020b08dbc4587685b0730a7ba905625af13c65f8028035cdfd69573c91"
dependencies = [
"anyhow",
"futures",
"http",
"httparse",
"log",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-net"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
dependencies = [
"async-io",
"blocking",
"futures-lite",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -236,6 +299,25 @@ dependencies = [
"syn",
]
[[package]]
name = "async-web-client"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8caf502b44d6d4be6154ac33af012cbb5fef11e6066edcfb42834217fbaf501b"
dependencies = [
"async-http-codec",
"async-net",
"futures",
"futures-rustls",
"http",
"lazy_static",
"log",
"rustls-pki-types",
"serde",
"thiserror 1.0.69",
"webpki-roots 0.26.11",
]
[[package]]
name = "async_io_stream"
version = "0.3.3"
@@ -381,6 +463,19 @@ dependencies = [
"generic-array",
]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "bounded-integer"
version = "0.5.8"
@@ -991,6 +1086,27 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@@ -1139,6 +1255,17 @@ dependencies = [
"syn",
]
[[package]]
name = "futures-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb"
dependencies = [
"futures-io",
"rustls",
"rustls-pki-types",
]
[[package]]
name = "futures-sink"
version = "0.3.32"
@@ -1305,6 +1432,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
@@ -2033,6 +2166,12 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
@@ -2690,6 +2829,17 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkarr"
version = "3.10.0"
@@ -2773,6 +2923,20 @@ dependencies = [
"pnet_macros_support",
]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "poly1305"
version = "0.8.0"
@@ -3112,6 +3276,7 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
dependencies = [
"aws-lc-rs",
"pem",
"ring",
"rustls-pki-types",
@@ -3262,6 +3427,19 @@ dependencies = [
"nom",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.13.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.41"
@@ -3278,6 +3456,32 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-acme"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54f05935c0b1d7c5981c40b768c5d5ed96a43f5cb5166f8f5be09779c5825697"
dependencies = [
"async-io",
"async-trait",
"async-web-client",
"aws-lc-rs",
"base64",
"blocking",
"chrono",
"futures",
"futures-rustls",
"http",
"log",
"pem",
"rcgen 0.13.2",
"serde",
"serde_json",
"thiserror 2.0.18",
"webpki-roots 0.26.11",
"x509-parser 0.16.0",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
@@ -3844,6 +4048,19 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"

View File

@@ -13,6 +13,7 @@ name = "alknet_core"
default = ["quinn"]
quinn = ["dep:quinn"]
iroh = ["dep:iroh"]
acme = ["dep:rustls-acme"]
[dependencies]
tokio = { version = "1", features = ["full"] }
@@ -34,4 +35,9 @@ futures = "0.3"
sha2 = "0.10"
hex = "0.4"
rand = "0.8"
rcgen = "0.13"
rcgen = "0.13"
ed25519-dalek = { version = "2", features = ["rand_core"] }
rustls-acme = { version = "0.12", optional = true, features = ["aws-lc-rs"] }
[dev-dependencies]
tempfile = "3"

View File

@@ -29,15 +29,72 @@ pub struct StaticConfig {
pub drain_timeout: Duration,
}
#[derive(Clone)]
pub struct Ed25519SecretKey(ed25519_dalek::SigningKey);
impl Ed25519SecretKey {
pub fn generate() -> Self {
let mut csprng = rand::rngs::OsRng;
Self(ed25519_dalek::SigningKey::generate(&mut csprng))
}
pub fn from_bytes(bytes: &[u8; 32]) -> Self {
Self(ed25519_dalek::SigningKey::from_bytes(bytes))
}
pub fn as_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
pub fn public(&self) -> ed25519_dalek::VerifyingKey {
self.0.verifying_key()
}
pub fn sign(&self, message: &[u8]) -> ed25519_dalek::Signature {
use ed25519_dalek::Signer;
self.0.sign(message)
}
}
impl std::fmt::Debug for Ed25519SecretKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ed25519SecretKey").finish_non_exhaustive()
}
}
impl zeroize::ZeroizeOnDrop for Ed25519SecretKey {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AcmeDirectory {
Production,
Staging,
Custom(String),
}
impl AcmeDirectory {
pub fn url(&self) -> &str {
match self {
AcmeDirectory::Production => "https://acme-v02.api.letsencrypt.org/directory",
AcmeDirectory::Staging => "https://acme-staging-v02.api.letsencrypt.org/directory",
AcmeDirectory::Custom(url) => url,
}
}
}
#[derive(Debug, Clone)]
pub enum TlsIdentity {
X509 {
cert: PathBuf,
key: PathBuf,
},
#[cfg(feature = "iroh")]
RawKey(iroh::SecretKey),
RawKey(Ed25519SecretKey),
SelfSigned,
Acme {
domains: Vec<String>,
cache_dir: PathBuf,
directory: AcmeDirectory,
contact: Vec<String>,
},
}
#[derive(Debug, Clone, Default)]

View File

@@ -103,6 +103,8 @@ pub struct AlknetEndpoint {
shutdown_tx: watch::Sender<bool>,
shutdown_rx: watch::Receiver<bool>,
drain_timeout: Duration,
#[cfg(feature = "acme")]
acme_state_handle: Option<tokio::task::JoinHandle<()>>,
}
#[cfg(any(feature = "quinn", feature = "iroh"))]
@@ -129,19 +131,29 @@ impl AlknetEndpoint {
let drain_timeout = static_config.drain_timeout;
#[cfg(feature = "quinn")]
let quinn = if let Some(listen_addr) = static_config.listen_addr {
#[cfg_attr(not(feature = "acme"), allow(unused_variables))]
let (quinn, acme_state_handle) = if let Some(listen_addr) = static_config.listen_addr {
let tls_identity = static_config.tls_identity.as_ref().ok_or_else(|| {
EndpointError::TlsConfig(io::Error::new(
io::ErrorKind::InvalidInput,
"quinn endpoint requires tls_identity in static config",
))
})?;
let server_config = build_quinn_server_config(tls_identity, &alpns)?;
let tls_setup = TlsSetup::new(tls_identity, &alpns).await?;
let server_config =
build_quinn_server_config_from_rustls(tls_setup.server_config)?;
let endpoint = quinn::Endpoint::server(server_config, listen_addr)
.map_err(EndpointError::BindFailed)?;
Some(endpoint)
#[cfg(feature = "acme")]
{
(Some(endpoint), tls_setup.acme_state_handle)
}
#[cfg(not(feature = "acme"))]
{
(Some(endpoint), None::<tokio::task::JoinHandle<()>>)
}
} else {
None
(None, None)
};
#[cfg(not(feature = "quinn"))]
@@ -170,6 +182,8 @@ impl AlknetEndpoint {
shutdown_tx,
shutdown_rx,
drain_timeout,
#[cfg(feature = "acme")]
acme_state_handle,
})
}
@@ -190,6 +204,11 @@ impl AlknetEndpoint {
iroh.close().await;
}
#[cfg(feature = "acme")]
if let Some(handle) = &self.acme_state_handle {
handle.abort();
}
tokio::time::sleep(self.drain_timeout).await;
#[cfg(feature = "quinn")]
@@ -290,6 +309,14 @@ fn dispatch_quinn(
identity_provider: &Arc<dyn IdentityProvider>,
) {
let alpn = extract_quinn_alpn(&connection);
#[cfg(feature = "acme")]
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;
}
let handler = match handlers.get(&alpn) {
Some(h) => h.clone(),
None => {
@@ -448,12 +475,135 @@ fn build_auth_context(
}
#[cfg(feature = "quinn")]
fn build_quinn_server_config(
tls_identity: &TlsIdentity,
alpns: &[Vec<u8>],
struct TlsSetup {
server_config: rustls::ServerConfig,
#[cfg(feature = "acme")]
acme_state_handle: Option<tokio::task::JoinHandle<()>>,
}
#[cfg(feature = "quinn")]
impl TlsSetup {
async fn new(
tls_identity: &TlsIdentity,
alpns: &[Vec<u8>],
) -> Result<Self, EndpointError> {
match tls_identity {
TlsIdentity::Acme {
domains,
cache_dir,
directory,
contact,
} => {
#[cfg(feature = "acme")]
{
Self::new_acme(domains, cache_dir, directory, contact, alpns).await
}
#[cfg(not(feature = "acme"))]
{
let _ = (domains, cache_dir, directory, contact, alpns);
Err(EndpointError::TlsConfig(io::Error::new(
io::ErrorKind::Unsupported,
"ACME feature not enabled but TlsIdentity::Acme configured",
)))
}
}
_ => {
let server_config = build_rustls_server_config(tls_identity, alpns)?;
Ok(Self {
server_config,
#[cfg(feature = "acme")]
acme_state_handle: None,
})
}
}
}
#[cfg(feature = "acme")]
async fn new_acme(
domains: &[String],
cache_dir: &std::path::Path,
directory: &crate::config::AcmeDirectory,
contact: &[String],
alpns: &[Vec<u8>],
) -> Result<Self, EndpointError> {
use rustls_acme::caches::DirCache;
use rustls_acme::{AcmeConfig, EventError, EventOk};
let acme_config = AcmeConfig::new(domains.to_vec())
.cache(DirCache::new(cache_dir.to_path_buf()))
.directory(directory.url())
.contact(contact.iter().map(|c| c.as_str()));
let state = acme_config.state();
let resolver = state.resolver();
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let mut config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|e| EndpointError::TlsConfig(io::Error::other(e)))?
.with_client_cert_verifier(Arc::new(AcceptAnyCertVerifier))
.with_cert_resolver(resolver);
config.max_early_data_size = u32::MAX;
let mut alpn = alpns.to_vec();
alpn.push(b"acme-tls/1".to_vec());
config.alpn_protocols = alpn;
let domains_owned: Vec<String> = domains.to_vec();
let handle = tokio::spawn(async move {
use futures::StreamExt;
let mut state = state;
while let Some(event) = state.next().await {
match event {
Ok(EventOk::DeployedCachedCert) => {
debug!(domains = ?domains_owned, "ACME: deployed cached certificate");
}
Ok(EventOk::DeployedNewCert) => {
debug!(domains = ?domains_owned, "ACME: deployed new certificate");
}
Ok(EventOk::CertCacheStore) => {
debug!(domains = ?domains_owned, "ACME: certificate stored to cache");
}
Ok(EventOk::AccountCacheStore) => {
debug!(domains = ?domains_owned, "ACME: account stored to cache");
}
Err(EventError::CertCacheLoad(e)) => {
error!(domains = ?domains_owned, error = ?e, "ACME: certificate cache load failed");
}
Err(EventError::AccountCacheLoad(e)) => {
error!(domains = ?domains_owned, error = ?e, "ACME: account cache load failed");
}
Err(EventError::CertCacheStore(e)) => {
warn!(domains = ?domains_owned, error = ?e, "ACME: certificate cache store failed");
}
Err(EventError::AccountCacheStore(e)) => {
warn!(domains = ?domains_owned, error = ?e, "ACME: account cache store failed");
}
Err(EventError::CachedCertParse(e)) => {
error!(domains = ?domains_owned, error = ?e, "ACME: cached certificate parse failed");
}
Err(EventError::Order(e)) => {
warn!(domains = ?domains_owned, error = ?e, "ACME: certificate order failed, will retry");
}
Err(EventError::NewCertParse(e)) => {
error!(domains = ?domains_owned, error = ?e, "ACME: new certificate parse failed");
}
}
}
debug!(domains = ?domains_owned, "ACME: state machine ended");
});
Ok(Self {
server_config: config,
acme_state_handle: Some(handle),
})
}
}
#[cfg(feature = "quinn")]
fn build_quinn_server_config_from_rustls(
rustls_config: rustls::ServerConfig,
) -> Result<quinn::ServerConfig, EndpointError> {
use quinn::crypto::rustls::QuicServerConfig;
let rustls_config = build_rustls_server_config(tls_identity, alpns)?;
let quic_server_config = QuicServerConfig::try_from(rustls_config)
.map_err(|e| EndpointError::TlsConfig(io::Error::other(e)))?;
Ok(quinn::ServerConfig::with_crypto(Arc::new(
@@ -467,6 +617,7 @@ fn build_rustls_server_config(
alpns: &[Vec<u8>],
) -> Result<rustls::ServerConfig, EndpointError> {
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let client_verifier = Arc::new(AcceptAnyCertVerifier);
match tls_identity {
TlsIdentity::X509 { cert, key } => {
let cert_chain = load_cert_chain(cert)?;
@@ -474,20 +625,19 @@ fn build_rustls_server_config(
let mut config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|e| EndpointError::TlsConfig(io::Error::other(e)))?
.with_no_client_auth()
.with_client_cert_verifier(client_verifier)
.with_single_cert(cert_chain, private_key)
.map_err(|e| EndpointError::TlsConfig(io::Error::other(e)))?;
config.alpn_protocols = alpns.to_vec();
config.max_early_data_size = u32::MAX;
Ok(config)
}
#[cfg(feature = "iroh")]
TlsIdentity::RawKey(secret_key) => {
let resolver = Arc::new(RawKeyCertResolver::new(secret_key));
let mut config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|e| EndpointError::TlsConfig(io::Error::other(e)))?
.with_no_client_auth()
.with_client_cert_verifier(client_verifier)
.with_cert_resolver(resolver);
config.alpn_protocols = alpns.to_vec();
config.max_early_data_size = u32::MAX;
@@ -498,13 +648,16 @@ fn build_rustls_server_config(
let mut config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|e| EndpointError::TlsConfig(io::Error::other(e)))?
.with_no_client_auth()
.with_client_cert_verifier(client_verifier)
.with_single_cert(cert.cert_chain, cert.private_key)
.map_err(|e| EndpointError::TlsConfig(io::Error::other(e)))?;
config.alpn_protocols = alpns.to_vec();
config.max_early_data_size = u32::MAX;
Ok(config)
}
TlsIdentity::Acme { .. } => {
unreachable!("TlsIdentity::Acme is handled by TlsSetup::new_acme, not build_rustls_server_config")
}
}
}
@@ -516,7 +669,8 @@ async fn build_iroh_endpoint(
let mut builder = iroh::Endpoint::builder();
if let Some(TlsIdentity::RawKey(secret_key)) = static_config.tls_identity.as_ref() {
builder = builder.secret_key(secret_key.clone());
let iroh_key = iroh::SecretKey::from_bytes(&secret_key.as_bytes());
builder = builder.secret_key(iroh_key);
} else {
let mut csprng = rand::rngs::OsRng;
builder = builder.secret_key(iroh::SecretKey::generate(&mut csprng));
@@ -589,14 +743,80 @@ fn generate_self_signed_cert() -> Result<SelfSignedCert, EndpointError> {
})
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
struct AcceptAnyCertVerifier;
#[cfg(feature = "quinn")]
impl std::fmt::Debug for AcceptAnyCertVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcceptAnyCertVerifier").finish()
}
}
#[cfg(feature = "quinn")]
impl rustls::server::danger::ClientCertVerifier for AcceptAnyCertVerifier {
fn offer_client_auth(&self) -> bool {
true
}
fn client_auth_mandatory(&self) -> bool {
false
}
fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {
&[]
}
fn verify_client_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::server::danger::ClientCertVerified, rustls::Error> {
Ok(rustls::server::danger::ClientCertVerified::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
]
}
}
#[cfg(feature = "quinn")]
struct RawKeyCertResolver {
key: Arc<rustls::sign::CertifiedKey>,
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
impl RawKeyCertResolver {
fn new(secret_key: &iroh::SecretKey) -> Self {
fn new(secret_key: &crate::config::Ed25519SecretKey) -> Self {
let signing_key = Arc::new(Ed25519SigningKey::new(secret_key.clone()));
let public_key = signing_key.spki_public_key();
let cert = rustls::pki_types::CertificateDer::from(public_key.to_vec());
@@ -607,7 +827,7 @@ impl RawKeyCertResolver {
}
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
impl rustls::server::ResolvesServerCert for RawKeyCertResolver {
fn resolve(
&self,
@@ -621,29 +841,29 @@ impl rustls::server::ResolvesServerCert for RawKeyCertResolver {
}
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
impl std::fmt::Debug for RawKeyCertResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawKeyCertResolver").finish()
}
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
#[derive(Clone)]
struct Ed25519SigningKey {
key: iroh::SecretKey,
key: crate::config::Ed25519SecretKey,
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
impl std::fmt::Debug for Ed25519SigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ed25519SigningKey").finish()
}
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
impl Ed25519SigningKey {
fn new(key: iroh::SecretKey) -> Self {
fn new(key: crate::config::Ed25519SecretKey) -> Self {
Self { key }
}
@@ -655,7 +875,7 @@ impl Ed25519SigningKey {
}
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
impl rustls::sign::SigningKey for Ed25519SigningKey {
fn choose_scheme(
&self,
@@ -677,7 +897,7 @@ impl rustls::sign::SigningKey for Ed25519SigningKey {
}
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
impl rustls::sign::Signer for Ed25519SigningKey {
fn sign(&self, message: &[u8]) -> Result<Vec<u8>, rustls::Error> {
Ok(self.key.sign(message).to_bytes().to_vec())
@@ -850,12 +1070,11 @@ mod tests {
assert!(auth.tls_client_fingerprint.is_some());
}
#[cfg(all(feature = "quinn", feature = "iroh"))]
#[cfg(feature = "quinn")]
#[test]
fn raw_key_cert_resolver_only_raw_public_keys() {
use rustls::server::ResolvesServerCert;
let mut csprng = rand::rngs::OsRng;
let sk = iroh::SecretKey::generate(&mut csprng);
let sk = crate::config::Ed25519SecretKey::generate();
let resolver = RawKeyCertResolver::new(&sk);
assert!(resolver.only_raw_public_keys());
}
@@ -863,10 +1082,9 @@ mod tests {
#[cfg(feature = "iroh")]
#[tokio::test]
async fn endpoint_constructs_with_iroh_raw_key_identity() {
let mut csprng = rand::rngs::OsRng;
let static_config = StaticConfig {
listen_addr: None,
tls_identity: Some(TlsIdentity::RawKey(iroh::SecretKey::generate(&mut csprng))),
tls_identity: Some(TlsIdentity::RawKey(crate::config::Ed25519SecretKey::generate())),
iroh_relay: None,
drain_timeout: Duration::from_millis(10),
};
@@ -894,7 +1112,7 @@ mod tests {
#[tokio::test]
async fn iroh_endpoint_runs_accept_loop_and_shutdown() {
use std::sync::Mutex;
let server_sk = iroh::SecretKey::generate(&mut rand::rngs::OsRng);
let server_sk = crate::config::Ed25519SecretKey::generate();
let static_config = StaticConfig {
listen_addr: None,
tls_identity: Some(TlsIdentity::RawKey(server_sk)),
@@ -1042,4 +1260,60 @@ mod tests {
let b = fingerprint_from_cert_der(cert).unwrap();
assert_eq!(a, b, "same cert DER must produce same fingerprint");
}
#[test]
fn acme_directory_production_url() {
use crate::config::AcmeDirectory;
let dir = AcmeDirectory::Production;
assert_eq!(
dir.url(),
"https://acme-v02.api.letsencrypt.org/directory"
);
}
#[test]
fn acme_directory_staging_url() {
use crate::config::AcmeDirectory;
let dir = AcmeDirectory::Staging;
assert_eq!(
dir.url(),
"https://acme-staging-v02.api.letsencrypt.org/directory"
);
}
#[test]
fn acme_directory_custom_url() {
use crate::config::AcmeDirectory;
let url = "https://custom-acme.example.com/directory";
let dir = AcmeDirectory::Custom(url.to_string());
assert_eq!(dir.url(), url);
}
#[cfg(feature = "quinn")]
#[tokio::test]
async fn tls_setup_x509_returns_no_acme_state() {
use rcgen::{CertificateParams, KeyPair};
let key_pair = KeyPair::generate().unwrap();
let params = CertificateParams::default();
let cert = params.self_signed(&key_pair).unwrap();
let cert_pem = cert.pem();
let key_pem = key_pair.serialize_pem();
let dir = tempfile::tempdir().unwrap();
let cert_path = dir.path().join("cert.pem");
let key_path = dir.path().join("key.pem");
std::fs::write(&cert_path, cert_pem).unwrap();
std::fs::write(&key_path, key_pem).unwrap();
let tls_identity = TlsIdentity::X509 {
cert: cert_path,
key: key_path,
};
let setup = TlsSetup::new(&tls_identity, &[b"alknet/test".to_vec()])
.await
.expect("X509 tls setup should succeed");
let _ = setup.server_config;
#[cfg(feature = "acme")]
assert!(setup.acme_state_handle.is_none());
}
}

View File

@@ -185,6 +185,22 @@ unresolved at the endpoint layer. A follow-up task will switch the server
config to request-but-not-require client certs so fingerprints flow for
peers that present them.
### Server-side client cert request
The quinn `rustls::ServerConfig` uses a custom `AcceptAnyCertVerifier`
that requests client certs but does not require them and does not verify
them against a CA. This is the "request-but-don't-require" mode: peers
that present a cert (X.509 or RFC 7250 raw key) have their fingerprint
extracted via `peer_identity()`; peers that don't present a cert connect
normally with `tls_client_fingerprint: None`.
The verifier accepts any presented cert without CA verification because
alknet's identity model is fingerprint-based, not PKI-based — the
`AuthPolicy::authorized_fingerprints` set is the trust anchor, not a
root CA store. The cert bytes are extracted at the TLS layer and hashed
to a fingerprint string; the fingerprint is then matched against the
configured set by `IdentityProvider::resolve_from_fingerprint()`.
## Resolution Flow
### Endpoint-level (before `handle()`)

View File

@@ -1,7 +1,7 @@
---
id: core/acme-integration
name: Add ACME auto-provisioning via rustls-acme (ADR-027)
status: pending
status: completed
depends_on: [core/rawkey-decouple-from-iroh]
scope: moderate
risk: medium

View File

@@ -1,7 +1,7 @@
---
id: core/endpoint-request-client-cert
name: Switch rustls ServerConfig from with_no_client_auth to request-but-don't-require client certs
status: pending
status: completed
depends_on: [core/endpoint-client-fingerprint]
scope: narrow
risk: medium

View File

@@ -1,7 +1,7 @@
---
id: core/rawkey-decouple-from-iroh
name: Decouple TlsIdentity::RawKey from the iroh feature (ADR-027)
status: pending
status: completed
depends_on: []
scope: narrow
risk: medium