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
//! Transport-level credential bundle for outbound connections:
//! [`ConnectionCredentials`], [`RemoteIdentity`] (ADR-005, moved from
//! alknet-core `credentials.rs`; alknet ADR-091's semantics).
//!
//! [`ConnectionCredentials`] carries the two dimensions the client config
//! consumes: the local node's identity and the expected remote identity.
//! It is transport-agnostic — consumed by this crate's
//! [`TlsClientConfig`](crate::TlsClientConfig) (TLS setup) and by the dial
//! seam in the consumers.

use crate::identity::TlsIdentity;

/// Expected identity of the remote node (alknet ADR-017 §7, extended by
/// alknet ADR-034 §2; ADR-005 moved the type here).
///
/// Carries a fingerprint string the assembly layer derives when the local
/// node knows the remote (the known-peer case → fingerprint pin).
///
/// `remote_identity: None` is the **public X.509 endpoint** case: the local
/// node has no prior knowledge of the remote, so there is no fingerprint to
/// pin. Combined with an X.509 remote, `None` selects CA verification
/// ([`WebPkiServerVerifier`] per the verifier-selection rule in alknet
/// ADR-034 §3). Combined with an Ed25519 raw-key remote, `None` fails closed
/// at handshake (raw-key remotes are always known peers — no CA to fall
/// back to).
///
/// The `Option` is therefore load-bearing, not cosmetic:
/// `Some(fingerprint)` means "pin this" (known peer), `None` means "trust
/// the CA or fail" (unknown remote). An implementer must not default
/// `remote_identity` to a placeholder value to "satisfy" the field — `None`
/// is a real state that drives verifier selection.
///
/// [`WebPkiServerVerifier`]: rustls::client::WebPkiServerVerifier
#[derive(Debug, Clone)]
pub struct RemoteIdentity {
    /// The pinned fingerprint: `ed25519:<hex>` for raw-key remotes,
    /// `SHA256:<hex>` for X.509 cert remotes (alknet ADR-030 §6).
    ///
    /// **Case- and format-exact** (review 001 §C-4): the pin must be
    /// produced by [`fingerprint_from_cert_der`] — lowercase hex, the
    /// exact `ed25519:` / `SHA256:` prefixes — because the verifier's
    /// comparison is exact-string against the fingerprint it computes
    /// from the presented cert. A config author cannot mis-case a pin
    /// and get away with it:
    ///
    /// - Same-format-but-wrong pins (uppercase hex, lowercase `sha256:`
    ///   prefix, garbage) construct fine and fail closed at the pin
    ///   compare (handshake rejection — never a downgrade).
    /// - Mismatched *format* fails earlier, at cert-type negotiation
    ///   ([ADR-007](../docs/architecture/decisions/007-cert-type-negotiation.md)):
    ///   an `ed25519:` pin makes the client offer raw-key-only server
    ///   cert types so it aborts against an X.509 server before the pin
    ///   compare is reached, and a `SHA256:` pin against a raw-key
    ///   server likewise fails at negotiation. Same fail-closed verdict,
    ///   earlier failure point — match the prefix to the peer's actual
    ///   cert kind.
    ///
    /// [`fingerprint_from_cert_der`]: crate::fingerprint_from_cert_der
    pub fingerprint: String,
}

/// Credentials for an outbound connection (alknet ADR-091's semantics,
/// ADR-005 moved the type here). All dimensions come from the assembly
/// layer's configuration — never from environment variables.
///
/// The two `Option`s are the two credential dimensions the client config
/// consumes (see `docs/architecture/client.md`): the client-auth
/// presentation (`local_identity`) and the verifier selection
/// (`remote_identity`). Both are load-bearing, not cosmetic — `Some` means
/// "pin this", `None` means "trust the CA or fail", never a placeholder
/// default.
#[derive(Debug, Clone, Default)]
pub struct ConnectionCredentials {
    /// The local node's identity (RFC 7250 raw key or X.509), derived from
    /// the vault at startup.
    pub local_identity: Option<TlsIdentity>,
    /// Expected fingerprint/cert of the remote node. `Some` → fingerprint
    /// pin (known peer); `None` → CA verification for X.509 remotes,
    /// fail-closed for Ed25519 raw-key remotes (alknet ADR-034 §2/§3).
    /// `None` is the public-X.509-endpoint state, not a missing field —
    /// must not be defaulted to a placeholder.
    pub remote_identity: Option<RemoteIdentity>,
}

impl ConnectionCredentials {
    /// Credentials with both dimensions unset — the public-X.509-endpoint
    /// baseline.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the local identity (the client-auth presentation).
    pub fn with_local_identity(mut self, local_identity: TlsIdentity) -> Self {
        self.local_identity = Some(local_identity);
        self
    }

    /// Set the remote identity (the fingerprint pin).
    pub fn with_remote_identity(mut self, remote: RemoteIdentity) -> Self {
        self.remote_identity = Some(remote);
        self
    }
}

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

    #[test]
    fn connection_credentials_builder_methods() {
        let creds = ConnectionCredentials::new().with_remote_identity(RemoteIdentity {
            fingerprint: "SHA256:abc".to_string(),
        });
        assert_eq!(
            creds.remote_identity.as_ref().unwrap().fingerprint,
            "SHA256:abc"
        );
        assert!(creds.local_identity.is_none());
    }

    #[test]
    fn connection_credentials_none_is_load_bearing_not_defaulted() {
        let creds = ConnectionCredentials::new();
        assert!(
            creds.remote_identity.is_none(),
            "ConnectionCredentials::new() must keep remote_identity as None (the load-bearing \
             public-X.509-endpoint state), not default it to a placeholder"
        );
    }
}