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:
217
Cargo.lock
generated
217
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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)]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()`)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user