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 — server and client `rustls` configs,
//! cert resolvers, verifiers, ACME state-machine wiring, and the identity /
//! credential / fingerprint types that drive config construction.
//!
//! 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 wrapper the deployment runs (`noq` for QUIC,
//! `tokio-rustls` for TCP+TLS). It does not dial, accept, dispatch, or
//! resolve peer identities — those belong to the dial seam, the accept
//! loop, and the auth layer.
//!
//! Core types:
//!
//! - [`TlsServerConfig`] — built once per identity + ALPN list, shared
//!   across transports via [`Arc`](std::sync::Arc) (not `Clone`; it holds
//!   the ACME task's `JoinHandle`).
//! - [`TlsClientConfig`] — built per dial from a [`ConnectionCredentials`] +
//!   ALPN; consumed by its accessors.
//! - [`TlsError`] — the config-construction error type. Handshake outcomes
//!   and ACME runtime errors are not `TlsError`s.
//!
//! Transport-specific accessors are feature-gated: `noq` (`for_noq`),
//! `tcp` (`for_tcp_tls`), `acme` (the ACME path). `default = []`.

pub mod client;
pub mod credentials;
pub mod fingerprint;
pub mod identity;
pub mod pem;
pub mod server;
pub mod signing;

pub use fingerprint::{extract_ed25519_raw_key_from_spki, fingerprint_from_cert_der};

pub use pem::{load_cert_chain, load_private_key};
pub use signing::Ed25519SigningKey;

pub use identity::{AcmeDirectory, Ed25519SecretKey, TlsIdentity};

pub use client::{
    build_client_auth, load_platform_root_cert_store, select_server_verifier,
    FingerprintPinVerifier, NoClientCertResolver, RawKeyClientCertResolver, TlsClientConfig,
};
pub use credentials::{ConnectionCredentials, RemoteIdentity};

pub use server::{
    build_rustls_server_config, generate_self_signed_cert, AcceptAnyCertVerifier,
    RawKeyCertResolver, SelfSignedCert, TlsServerConfig, VerifyPresentedCertVerifier,
};

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TlsError {
    /// Cert or key file read / PEM parse. `rustls_pemfile` funnels its
    /// own error type into `io::Error`, so one `io::Error` source covers
    /// the whole loading path.
    #[error("loading cert/key material: {0}")]
    CertLoad(#[from] std::io::Error),

    /// Self-signed cert generation (rcgen). Server `SelfSigned` path.
    #[error("generating self-signed cert: {0}")]
    SelfSigned(#[from] rcgen::Error),

    /// rustls server or client config construction
    /// (`with_safe_default_protocol_versions`, `with_single_cert`,
    /// `CertifiedKey::from_der`, `RootCertStore::add`).
    #[error("building rustls config: {0}")]
    Rustls(#[from] rustls::Error),

    /// `WebPkiServerVerifier::builder(_with_provider)..build()` — the
    /// unknown-X.509-remote client path. Re-exported by rustls at
    /// `rustls::client` (the `rustls::webpki` module is private at the
    /// pinned 0.23.44; same type, public path).
    #[error("building webpki verifier: {0}")]
    VerifierBuild(#[from] rustls::client::VerifierBuilderError),

    /// QUIC config wrapping — the one path where `for_noq()` fails
    /// (`NoInitialCipherSuite`, not a `rustls::Error`). noq-gated.
    #[cfg(feature = "noq")]
    #[error("wrapping rustls config for noq: {0}")]
    NoqWrap(#[from] noq_proto::crypto::rustls::NoInitialCipherSuite),

    /// Config-mismatch errors that are not wrapped third-party
    /// errors: ACME feature not enabled but `Acme` configured
    /// (server), or `Acme` identity used for client auth. A config
    /// error, not a wrapped third-party error.
    #[error("TLS config error: {0}")]
    AcmeConfig(String),
}

#[cfg(test)]
mod tests {
    use super::TlsError;

    #[test]
    fn tls_error_matches_the_adr_002_variant_set() {
        let io: TlsError = std::io::Error::other("x").into();
        assert!(matches!(io, TlsError::CertLoad(_)));
        let rcgen: TlsError = rcgen::Error::CouldNotParseCertificate.into();
        assert!(matches!(rcgen, TlsError::SelfSigned(_)));
        let rustls: TlsError = rustls::Error::General("x".into()).into();
        assert!(matches!(rustls, TlsError::Rustls(_)));
        let builder: TlsError = rustls::client::VerifierBuilderError::NoRootAnchors.into();
        assert!(matches!(builder, TlsError::VerifierBuild(_)));
        let acme = TlsError::AcmeConfig("acme feature not enabled".into());
        assert!(acme.to_string().contains("acme feature not enabled"));
    }

    #[cfg(feature = "noq")]
    #[test]
    fn tls_error_noq_wrap_variant_exists() {
        let _ = matches!(
            Option::<TlsError>::None,
            Some(TlsError::NoqWrap(_)) | Some(_)
        );
    }
}