Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/Skidbladnir

crates/installer/nordisk-twin/src/lib.rs

//! **A box's twin over mutual TLS 1.3** — certified once, pinned, dialled.
//!
//! A twin is installed from the SAME medium as its primary and carries nothing
//! per box (the one-medium ruling), so the credential it follows with cannot be
//! on the medium: it is minted when the pair is formed. And it must not be the
//! installer CA's — every box certificate and the medium's own `bootstrap`
//! certificate chain to that CA, so trusting it trusts anyone holding the medium
//! (holger stinky 14 §2 correction 2). The primary's OWN issuer signs it, and the
//! twin pins that issuer and nothing else.
//!
//! ```text
//!   front     BecomeTwin(primary)           on the twin: the role, live
//!   twin      TwinCsr                       mint the key HERE, answer the request
//!   primary   IssueTwinCertificate          issuer signs it: CN forced, clientAuth, 365 d
//!   twin      InstallTwinCertificate        our key? the CA signed it? pin the CA
//!   twin      dial the replication door     TLS 1.3 only, the pinned CA, its own cert
//! ```
//!
//! The pieces, none of which names a product:
//!
//! * [`pki`] — the persistent issuer (P-256, key `0600` under the caller's
//!   directory), a leaf from a request, the door's server identity, a request
//!   for a key minted here;
//! * [`credential`] — the twin's side: [`credential::request`],
//!   [`credential::install`] (refused by name: no key, another key, a CA that
//!   did not sign it, a CA other than the pinned one — renewal under the SAME
//!   CA is taken), [`credential::load`];
//! * [`tls`] — `ring` with every non-1.3 suite removed, the door's config
//!   (client certificate REQUIRED, chained to the issuer) and the twin's
//!   (the pinned CA as the only root);
//! * `net` (feature `tokio`) — the door's accept loop, which names every
//!   refused handshake, and the twin's dialler: the first recorded host that
//!   answers a short TCP probe;
//! * [`certify`] — the front's state machine over a [`certify::Boxes`] trait,
//!   gRPC in a product, a fake in the tests; [`certify::run_while`] stops a
//!   loop whose pair the front no longer wants;
//! * [`fence`] — fence before recharge: [`fence::fence_gate`] refuses
//!   `not_fenced` while the twin's old primary may still write;
//! * one holder per principal: [`pki::Issuer::issue_superseding`] pins the
//!   newest key, and [`tls::server_config_current`] refuses every other key's
//!   leaf at the door (`superseded`).
//!
//! What a product keeps: its RPCs, its principal names, its auth mapping of the
//! peer's CN, where its directories are, and its error codes (every refusal
//! here is a [`TwinError`] with a stable [`TwinError::kind`] to map from).

pub mod certify;
pub mod credential;
pub mod fence;
pub mod pki;
/// The recharge lane: a fresh main filled from its twin (moved from holger, lane G11).
pub mod recharge;
pub mod tls;
#[cfg(feature = "tokio")]
pub mod net;

/// Why something was refused. [`TwinError::kind`] is stable — a product maps it
/// to its own frozen error code; the text is for the operator.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TwinError {
    /// A certificate, key or request that does not parse or sign.
    Pki(String),
    /// The filesystem said no.
    Io(String),
    /// Install before request: this box never minted the key a certificate
    /// could be for.
    NoKey(String),
    /// The certificate is for a DIFFERENT key than the one this box minted.
    KeyMismatch,
    /// The CA handed with the certificate did not sign it (or the chain does not
    /// verify for client authentication now).
    NotIssuedBy(String),
    /// A different CA than the one already pinned — a twin is never re-pointed.
    Repointed,
    /// The certificate is outside its validity window.
    Expired(String),
    /// A host the source reported is not an IP address, or none was reported.
    BadHost(String),
    /// The source reported no replication port.
    NoPort,
    /// A request whose claimed CN is not the principal it is being issued for.
    CsrClaim { claimed: String, principal: String },
}

impl TwinError {
    /// The stable name of this refusal, `twin-<what>`.
    pub fn kind(&self) -> &'static str {
        match self {
            TwinError::Pki(_) => "twin-pki",
            TwinError::Io(_) => "twin-io",
            TwinError::NoKey(_) => "twin-no-key",
            TwinError::KeyMismatch => "twin-key-mismatch",
            TwinError::NotIssuedBy(_) => "twin-not-issued-by",
            TwinError::Repointed => "twin-repointed",
            TwinError::Expired(_) => "twin-expired",
            TwinError::BadHost(_) => "twin-bad-host",
            TwinError::NoPort => "twin-no-port",
            TwinError::CsrClaim { .. } => "twin-csr-claim",
        }
    }
}

impl std::fmt::Display for TwinError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let why = match self {
            TwinError::Pki(s) | TwinError::Io(s) => s.clone(),
            TwinError::NoKey(s) => format!("this box has no replication key ({s}); the request step mints it — a certificate cannot be installed for a key that was never made"),
            TwinError::KeyMismatch => "this certificate was issued for a DIFFERENT key than the one this box minted, so it could never complete a handshake here. Nothing was installed; certify again against this box — the request it signs is this box's own".into(),
            TwinError::NotIssuedBy(s) => format!("the CA handed with the certificate did not sign it: {s}. This box would pin a CA that did not sign its own credential; nothing was installed"),
            TwinError::Repointed => "this box already pins a DIFFERENT issuer. A twin is never re-pointed, and a door that could swap the CA it trusts is a door for redirecting the replication stream. A renewal comes from the SAME issuer; a new primary needs a fresh twin".into(),
            TwinError::Expired(s) => format!("the certificate is outside its validity window: {s}"),
            TwinError::BadHost(s) => format!("the source reported replication host(s) {s:?}, which is not a list of IP addresses; nothing was installed"),
            TwinError::NoPort => "the source reported no replication port; there is no mutual door for this box to dial".into(),
            TwinError::CsrClaim { claimed, principal } => format!("the request claims CN `{claimed}` and is being issued for `{principal}`: a request is issued as what it asked to be or not at all"),
        };
        write!(f, "REFUSED [{}] {why}", self.kind())
    }
}

impl std::error::Error for TwinError {}

pub type Result<T> = std::result::Result<T, TwinError>;

/// Seconds since the epoch, now.
pub fn now_unix() -> i64 {
    time::OffsetDateTime::now_utc().unix_timestamp()
}

/// Whole, then renamed — and `0600` when `private`. The one way this crate
/// writes a file.
pub(crate) fn write_atomic(dir: &std::path::Path, name: &str, bytes: &[u8], private: bool) -> Result<()> {
    use std::io::Write as _;
    std::fs::create_dir_all(dir).map_err(|e| TwinError::Io(format!("{}: {e}", dir.display())))?;
    let at = dir.join(name);
    let tmp = dir.join(format!(".{name}.part"));
    let mut o = std::fs::OpenOptions::new();
    o.write(true).create(true).truncate(true);
    #[cfg(unix)]
    if private {
        use std::os::unix::fs::OpenOptionsExt as _;
        o.mode(0o600);
    }
    #[cfg(not(unix))]
    let _ = private;
    let mut f = o.open(&tmp).map_err(|e| TwinError::Io(format!("{}: {e}", tmp.display())))?;
    f.write_all(bytes).and_then(|_| f.sync_all()).map_err(|e| TwinError::Io(format!("{}: {e}", tmp.display())))?;
    std::fs::rename(&tmp, &at).map_err(|e| TwinError::Io(format!("{} → {}: {e}", tmp.display(), at.display())))
}

/// A file's text, `None` when it does not exist.
pub(crate) fn read_opt(dir: &std::path::Path, name: &str) -> Result<Option<String>> {
    match std::fs::read_to_string(dir.join(name)) {
        Ok(s) => Ok(Some(s)),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(e) => Err(TwinError::Io(format!("{}: {e}", dir.join(name).display()))),
    }
}