alktls 0.1.0

Shared TLS setup types: server and client rustls configs, cert resolvers, verifiers, and ACME state-machine wiring, transport-agnostic and shareable across transports.
Documentation

alktls

Shared TLS setup types for rustls: server and client config construction, cert resolvers, verifiers, and ACME state-machine wiring — transport-agnostic and shareable across transports.

The crate owns config construction: given an identity and an ALPN list, produce a rustls::ServerConfig or rustls::ClientConfig and hand it to whichever transport the deployment runs (QUIC, TCP+TLS, or your own wrapper). It does not dial, accept, dispatch, or resolve peer identities — those stay in the transport and auth layers.

Not tied to any particular stack: compose it with any transport that consumes a rustls config.

Quick start

Server — one identity, N transports

use std::sync::Arc;
use alktls::{TlsServerConfig, TlsIdentity};

// Any of: X509 { cert, key }, RawKey(Ed25519SecretKey),
// SelfSigned, or Acme { .. } (behind the `acme` feature).
let identity = TlsIdentity::RawKey(Ed25519SecretKey::generate());
let alpn = vec![b"my/protocol".to_vec()];

let tls = Arc::new(TlsServerConfig::new(&identity, &alpn).await?);
// The same config feeds every transport — share it via Arc, never
// re-build per transport (and never spawn a second ACME machine):
let _rustls = tls.rustls_config();          // any custom wrapper
#[cfg(feature = "tcp")]
let _acceptor = tls.for_tcp_tls();          // tokio-rustls TlsAcceptor
#[cfg(feature = "noq")]
let _quic = tls.for_noq()?;                 // noq (QUIC) ServerConfig

On the ACME path the config spawns a background renewal task and appends acme-tls/1 to the ALPN list for you (idempotently); the handle lives in the config, which is why TlsServerConfig is not Clone — share via Arc.

Client — pin, CA, or fail closed

use alktls::{ConnectionCredentials, RemoteIdentity, TlsClientConfig};

// Known peer: pin its fingerprint (the fingerprint IS the trust
// anchor). Pins are format-exact: `ed25519:<hex>` for raw-key
// remotes, `SHA256:<hex>` for X.509 remotes.
let creds = ConnectionCredentials::new()
    .with_remote_identity(RemoteIdentity {
        fingerprint: "ed25519:64-digit-lowercase-hex".into(),
    });
let config = TlsClientConfig::new(&creds, b"my/protocol")?;
let _rustls = config.into_rustls_config();  // for a TCP+TLS dial

// Unknown X.509 endpoint: `remote_identity: None` → CA verification
// against the platform root store (falls back to webpki-roots when
// the platform bundle is missing, e.g. containers).
let public = ConnectionCredentials::new();
let _config = TlsClientConfig::new(&public, b"my/protocol")?;

Verifier selection follows the identity: known peer + fingerprint → pin; unknown + X.509 → CA; unknown + raw key → fail closed at the handshake. Unknown raw-key remotes are never downgraded to CA verification — a raw-key remote has no CA, so it is always a known peer.

Identity model

TlsIdentity is the one identity type on both paths:

Identity Server presents Client auth presents
X509 { cert, key } cert chain from PEM files cert chain from PEM files
RawKey(Ed25519SecretKey) RFC 7250 raw public key (SPKI) RFC 7250 raw public key
SelfSigned generated in-memory dev cert (no SANs, never expires; pair with fingerprint pinning) nothing (NoClientCertResolver)
Acme { .. } (acme feature) ACME-managed cert, auto-renewed config error — server-only

Client-auth presentation follows the local identity: raw key and X.509 present their cert, SelfSigned/None present nothing.

Behavior-preservation invariants

Some settings look optional but are load-bearing — the crate sets them on every path so callers cannot forget them:

  • max_early_data_size = u32::MAX on server configs (0-RTT works out of the box), enable_early_data = true on the client.
  • aws-lc-rs as the crypto provider everywhere.
  • acme-tls/1 ALPN append for the ACME path only, done by the crate.
  • Non-empty root store — the client CA path merges webpki-roots when the platform store is empty.
  • Fail closed — verifier selection never silently downgrades; a wrong pin fails the handshake, not the verification posture.
  • Proof-of-possession by default — the default server verifier verifies the client's CertificateVerify signature against the presented cert (raw-key and X.509 paths), so a copied cert is not usable by a party that lacks the matching private key. The no-possession escape hatch (AcceptAnyCertVerifier) exists but is never the default.

Features

Feature Contents
(default) config construction, identity/credential/fingerprint types, PEM loading, verifiers, resolvers
tcp for_tcp_tls() — the tokio-rustls acceptor wrapper
noq for_noq() — QUIC config wrapping via noq
acme the ACME path — rustls-acme state machine, background renewal, acme-tls/1

The default crate is deliberately lean (rustls + cert material + tokio spawn); the transport-specific dependencies are opt-in.

Scope boundary

This crate is the cert/config provider, not the accept loop. It does not bind sockets, dial, or dispatch — accept loops and dial seams live in the consumers. Handshake-time outcomes (a rejected cert, a mismatched pin) surface through the transport's connector, not through TlsError, which covers config construction only. ACME state-machine runtime events are logged in the spawned renewal task.

Documentation

  • Architecture docs — the authoritative spec: overview, the server and client API surfaces, and ADRs 001–008 (extraction baseline, TlsError shape, noq, accessors, config-type ownership, module layout, RFC 7250 negotiation, possession verification).
  • API docs — full crate documentation on docs.rs.

Verification

cargo test                  # default crate
cargo test --all-features   # + tcp, noq, acme suites
cargo clippy --all-targets -- -D warnings
cargo fmt --check

License

MIT OR Apache-2.0