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()))),
}
}