yog 0.0.73

yog: the standalone server for litany loops — the world, the balls and the conversations, behind one wire
Documentation
//! **Enrollment** (REMOTE §1.4 as amended, §4.2, §8.2; bl-f4e3): what an
//! operator asks for when a new device joins, and the whole of what the engine
//! answers with.
//!
//! **§1.4 is not lifted and this is not a pairing flow.** The device performs
//! no channel act — it has no certificate, so it cannot open a connection at
//! all. An **operator-grade seat** performs the act, over its own already
//! authenticated channel, and the material the answer carries then travels to
//! the device **out of channel**: a QR on a screen is a scp shaped like a
//! photograph, and the operator is standing in front of both machines. That is
//! the same class as the engine-boot mint (`wire::provision`, bl-ae05) — the
//! operator's own tooling, reached through the boundary because REMOTE §3
//! forbids a capability that exists on the wire and nowhere else.
//!
//! **The two values here are the vocabulary, not the act.** The executor is
//! [`boundary::dispatch::enroll`](crate::boundary::dispatch), beside
//! `advertise` and for its reason: everything else in the chokepoint routes,
//! and these gate. They live in the registry because the identity they mint
//! and the registration they seat are the registry's own two facts, and a
//! payload type with its own module is the fold [`mailbox::Verb`](super::mailbox::Verb)
//! and [`monitor::Verb`](crate::monitor::Verb) already take — one variant at
//! the boundary, one home for the doc.

use super::Grade;
use crate::boundary::codec::fields::opt_str_of;
pub use crate::wire::rendezvous::material::Handoff;
use serde_json::{Map, Value};

/// What an enrollment asks for: a workspace to seat the new client in, the
/// common name its certificate will carry, and what that certificate may say.
///
/// **It addresses a workspace like any other gesture** (REMOTE §8). The act
/// creates the registration, and a registration is the pair
/// `(client, workspace)` — so an enrollment that named no workspace would mint
/// a certificate that authenticates and sees nothing, and the operator would
/// have to finish the job with a `touch`. One act, one pair.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
    /// The workspace the new client is registered in (§4.1) —
    /// `clients/<name>/workspaces/<workspace>`.
    pub workspace: String,
    /// The certificate's subject common name, which **is** the client identity
    /// (§2). Refused by [`Client::parse`](super::Client::parse) exactly as
    /// every other identity is: one path component, never [`LOCAL`](super::LOCAL).
    pub name: String,
    /// What that certificate may say (§4.2). Minted into the subject by the
    /// operator's own CA, which is the only thing entitled to write it.
    pub grade: Grade,
    /// **The address the DEVICE will dial** (bl-fec6), or `None` for the one
    /// this engine wrote for itself.
    ///
    /// The device being enrolled is by definition not this box, so the route it
    /// reaches this engine by is not necessarily the one this box wrote in
    /// `wire/address`: an emulator reaches its host only through the emulator's
    /// own alias, a phone reaches it on the LAN, an overlay peer by a name. The
    /// envelope carried `wire/address` verbatim, so a scan was correct only for
    /// a device sharing this box's own loopback view — and the material had to
    /// be hand-edited afterwards on any device you could reach a shell on.
    ///
    /// It is a field of the REQUEST rather than a setting, because it is a fact
    /// about the device being enrolled and not about this engine: two devices
    /// enrolled a minute apart can rightly need two different routes. The mint
    /// already understands this — `WIRE_HOST` is a list, and every entry rides
    /// the server leaf — so what an operator states here should be one of the
    /// spellings they minted; a route the certificate does not cover fails
    /// verification at the device rather than here, which is the residual §8.4
    /// records.
    pub address: Option<String>,
}

/// What one enrollment answers with — the whole of what a new device needs and
/// nothing else.
///
/// **The private key is here and nowhere else.** The engine mints the pair,
/// reads it, hands it over and **shreds the key** before the answer leaves;
/// what stays on disk is the certificate, which is public material and whose
/// presence is what refuses a second enrollment under the same name
/// ([`provision::issue`](crate::wire::provision)). Custody after that is the
/// transport's: over the wire the answer is TLS bytes and a seat's RAM (§6),
/// while a deposit through the `gestures/` inbox lands it in a reply file
/// inside the world — on the operator's own box, beside the CA that can mint
/// the same leaf again at will, so it discloses nothing to anyone who could
/// not already mint. It does *persist*, and the remedy is `rm`.
///
/// **The payload contract is REMOTE §8.4's**: the QR envelope is these six
/// fields under a `"yog-enroll": 1` marker, compact JSON, PEM verbatim — and
/// the rendezvous pair beside them when this box holds one.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Enrolled {
    /// The grade minted into the subject (§4.2).
    pub grade: Grade,
    /// The common name the certificate carries — the device's identity.
    pub name: String,
    /// **The engine's own wire address, as clients dial it** (§8): the one the
    /// boot recorded in `wire/address`, never the port a `:0` request became.
    /// A runtime port changes on the next boot, so a QR carrying one would be
    /// stale before it was scanned.
    pub address: String,
    /// The operator CA both ends verify against, PEM.
    pub ca: String,
    /// This device's own certificate, PEM.
    pub cert: String,
    /// Its private key, PEM — held by nothing on this box once this value is
    /// built.
    pub key: String,
    /// **The rendezvous hand-off** (REMOTE §8.4, §13.2; bl-9043): the engine's
    /// public rendezvous key and the pairing salt, so a device can find this
    /// engine off its stated address. `None` on a box that minted none — a
    /// loopback-only engine has nothing to rendezvous for — and then the
    /// envelope omits both keys.
    pub rendezvous: Option<Handoff>,
}

/// The envelope keys the hand-off rides under — the files' own names
/// (`rendezvous.pub`, `pairing.salt`) with the dot a JSON key would not want.
const PUBLIC: &str = "rendezvous_pub";
const SALT: &str = "pairing_salt";

/// Write the hand-off into a reply object, or nothing when there is none.
pub(crate) fn handoff_into(map: &mut Map<String, Value>, handoff: Option<&Handoff>) {
    if let Some(handoff) = handoff {
        map.insert(PUBLIC.to_owned(), Value::from(handoff.public.clone()));
        map.insert(SALT.to_owned(), Value::from(handoff.salt.clone()));
    }
}

/// Read the hand-off back: both keys or neither, because half a pairing
/// derives nothing a device could use.
pub(crate) fn handoff_of(o: &Map<String, Value>) -> Result<Option<Handoff>, String> {
    match (opt_str_of(o, PUBLIC)?, opt_str_of(o, SALT)?) {
        (None, None) => Ok(None),
        (Some(public), Some(salt)) => Ok(Some(Handoff { public, salt })),
        _ => Err(format!("enrolled: {PUBLIC} and {SALT} travel together")),
    }
}