crates/monetize-server/src/custody.rs
//! **The custody of a private estate's secrets** — monetize keeps them sealed.
//!
//! Owner, 2026-10-04: a private gunnar's estate-level secrets are made and kept
//! by MONETIZE (gunnar.rs's own, on the front) and never on an operator's
//! laptop. `private-gunnar-ops/PRIVATE-GUNNAR.md` §3 has the decision and the
//! blast radius; this file is the mechanism.
//!
//! # What is kept here, and what is not
//!
//! Kept, per estate, sealed in `custody/<company>.sealed`:
//!
//! * the **media signing seed** — the root every medium of that estate is
//! signed with (the kit's `root_seed`), 32 bytes;
//! * the **pairing seed**, 32 bytes, and the **pairing material** the kit
//! minted for the estate's boxes (the installer CA, its keys and the link
//! keys), handed back after the install and sealed here;
//! * the **recovery (break-glass) bearer** for the estate's console;
//! * the **backup key** that seals the estate's monetize state on its way to
//! the estate's own twin;
//! * the **buyer's first-login invite** (private ownership, 2026-10-07): the
//! kit feeds its hash to gunnar as the operator's first invite, single use
//! and short lived, and the "ready" mail carries it as the buyer's link.
//!
//! * the estate's **off-site key pair** (lane O1, an estate whose main ships an
//! off-site copy: Holger's) — minted with the pairing material, its public
//! half fed as the main's `offsite_recipient`; the hand-off keeps it in a
//! seal of its own (`<company>.offsite.sealed`), so the operator can still
//! open the estate's copies; a full forget shreds it.
//!
//! NOT kept here: a box's own host, TLS and forge SSH keys. Those are made on
//! the box and only their public halves leave it, exactly as on gunnar.rs.
//!
//! # How the seal is protected
//!
//! * [`crate::seal`] — AES-256-GCM in monetize-front's ledger-backup format
//! (feature `custody-ring`) — under the custody key `custody/kek` (32
//! random bytes, mode 600, made on first use). The company name is in the associated data, so
//! a sealed file copied onto another estate's name does not open.
//! * A secret is unsealed only for the step that needs it, into a HAND-OFF
//! directory (mode 700, files mode 600) that [`HandOff`] deletes when it is
//! dropped.
//! * No secret is ever written to the job's run file, its journal, a log line
//! or an RPC answer — `estate_order::tests` searches all four for every one.
//! * The custody key is not in monetize's text backup (`crate::backup` keeps no
//! secret, by design).
use std::io::Write as _;
use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
use std::path::{Path, PathBuf};
use crate::seal;
/// What a custody file starts with.
const MAGIC: &[u8] = b"MCUS1\0";
fn aad(company: &str) -> Vec<u8> {
format!("monetize-custody-1:{company}").into_bytes()
}
/// A credential's associated data: its name, and that it is not an estate's.
fn credential_aad(name: &str) -> Vec<u8> {
format!("monetize-custody-1:credential:{name}").into_bytes()
}
/// **A health reader sealed apart by a 5.3.1 custody** (`<company>.reader.sealed`): LEGACY (ONE
/// monetize, G12/F4 2026-10-10) — monetize dials no estate, so nothing reads the twin through
/// the estate's door and no reader is kept; [`Custody::forget`] removes one left behind.
const LEGACY_READER_SUFFIX: &str = ".reader.sealed";
/// **A narrated site's installer link file, kept past the hand-off** (`<company>.link.sealed`,
/// lane N2): the ONE file of the estate's pairing material its DEV-NARRATE receiver reads —
/// moved out of the estate's seal when the hand-off forgets the rest, sealed under its own
/// associated data, removed by [`Custody::forget`].
const LINK_SUFFIX: &str = ".link.sealed";
/// The link seal's associated data: its company, and that it is the link file alone.
fn link_aad(company: &str) -> Vec<u8> {
format!("monetize-custody-1:link:{company}").into_bytes()
}
/// **An estate's OFF-SITE key pair, kept past the hand-off** (`<company>.offsite.sealed`, lane
/// O1): the X25519 pair whose public half the estate's main (and the twin that may become main)
/// seals its off-site copy for (Holger's `offsite_recipient`). The shop's pairing mints it with
/// the order's material ([`OFFSITE_KEY_FILE`], [`OFFSITE_PUB_FILE`]); the hand-off moves it out
/// of the estate's seal into this one, so the ONE box — the operator — can still open the
/// estate's copies when the estate is gone (the ONE box's own off-box copy carries
/// `custody/*.sealed`). The estate itself never holds the private half. Removed by
/// [`Custody::forget`] (a refund, a deletion: the copies are then shredded with it).
const OFFSITE_SUFFIX: &str = ".offsite.sealed";
/// The off-site seal's associated data: its company, and that it is the off-site pair alone.
fn offsite_aad(company: &str) -> Vec<u8> {
format!("monetize-custody-1:offsite:{company}").into_bytes()
}
/// The pairing material's names of the estate's off-site key pair (X25519, base64 of 32 bytes
/// each: Holger's `offsite::key32`): the private half, kept by the ONE box alone, and the public
/// half the estate is fed.
pub const OFFSITE_KEY_FILE: &str = "offsite.key";
pub const OFFSITE_PUB_FILE: &str = "offsite.pub";
/// The pairing material's name of the installer's link file (nordisk-pairing
/// `installer::LINK_KEYS`, the same literal: monetize-server links no installer).
pub const LINK_KEYS_FILE: &str = "link.toml";
/// The custody key's file name in the custody directory.
pub const KEK_FILE: &str = "kek";
/// The hand-off file names the kit reads (`cargo xtask … estate install --laid`).
pub mod handoff {
/// The media signing seed, 32 raw bytes — what the kit takes as the
/// instance's `root_seed` (the format `dwarves root-key generate` writes).
pub const ROOT_SEED: &str = "root.seed";
/// The console's break-glass bearer, 64 hex (the kit's `console-admin-token`).
pub const CONSOLE_TOKEN: &str = "console-admin-token";
/// The pairing seed, 64 hex.
pub const PAIRING_SEED: &str = "pairing.seed";
/// **The buyer's first-login invite**, 64 hex (private ownership,
/// 2026-10-07): the kit feeds its HASH as the operator's first invite
/// (`operator_invite_sha256`), and the "ready" mail carries it as
/// `https://<company>.<apex>/login?invite=…&user=<account>`.
pub const FIRST_LOGIN: &str = "first-login-invite";
/// **The estate's client key** (one shop, 2026-10-07), 64 hex: the seed of
/// the Ed25519 key a SOLD estate signs its `SoldEstate` calls to the ONE
/// monetize with (it runs no monetize of its own, G16); the kit feeds it to
/// the estate. Its public half is on the order.
pub const ESTATE_CLIENT: &str = "estate-client.key";
/// Where the kit hands the pairing material it minted BACK to the custodian.
pub const RETURN_DIR: &str = "return";
/// **The installer material the estate keeps** (F4): the bundle the shop's pairing hands
/// back after a PASSed pairing (material, ledger, plan); the sold estate's front TAKES it
/// (`SoldEstate.TakeMaterial`, the pull: monetize never dials an estate) before the
/// hand-off forgets it with the rest.
pub const INSTALLER_MATERIAL: &str = "installer-material.json";
/// Where a later step finds the pairing material handed back at the
/// install (the kit's installer material directory).
pub const MATERIAL_DIR: &str = "material";
}
/// **One estate's secrets, in memory only.** Overwritten when dropped.
pub struct EstateSecrets {
pub signing_seed: [u8; 32],
pub pairing_seed: [u8; 32],
pub break_glass: [u8; 32],
/// **The buyer's first-login invite** ([`handoff::FIRST_LOGIN`]); `None`
/// in a seal made before it existed, until [`Custody::mint`] adds one.
pub first_login: Option<[u8; 32]>,
/// **The estate's client key's seed** ([`handoff::ESTATE_CLIENT`]); `None`
/// in a seal made before it existed, until [`Custody::mint`] adds one.
pub estate_client: Option<[u8; 32]>,
/// The pairing material the kit handed back: file name → contents.
pub pairing_material: Vec<(String, String)>,
}
impl Drop for EstateSecrets {
fn drop(&mut self) {
for b in [&mut self.signing_seed, &mut self.pairing_seed, &mut self.break_glass] {
b.iter_mut().for_each(|x| *x = 0);
}
for b in [self.first_login.as_mut(), self.estate_client.as_mut()].into_iter().flatten() {
b.iter_mut().for_each(|x| *x = 0);
}
for (_, v) in &mut self.pairing_material {
v.clear();
}
}
}
impl std::fmt::Debug for EstateSecrets {
/// Names, never values: a `{:?}` in a log line must not be a leak.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EstateSecrets {{ signing_seed, pairing_seed, break_glass, first_login, estate_client, pairing_material: {} file(s) }}", self.pairing_material.len())
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Plain {
signing_seed: String,
pairing_seed: String,
break_glass: String,
#[serde(default)]
first_login: String,
#[serde(default)]
estate_client: String,
#[serde(default)]
pairing_material: Vec<(String, String)>,
}
/// **The custodian**: a directory of sealed files and the key they are sealed under.
#[derive(Clone, Debug)]
pub struct Custody {
dir: PathBuf,
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn unhex(s: &str) -> Result<Vec<u8>, String> {
if s.len() % 2 != 0 {
return Err("odd hex".into());
}
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string())).collect()
}
fn random32() -> Result<[u8; 32], String> {
seal::random32().map_err(|e| format!("custody: {e}"))
}
fn arr(v: &[u8], what: &str) -> Result<[u8; 32], String> {
v.try_into().map_err(|_| format!("custody: {what} is {} bytes, not 32", v.len()))
}
/// Write `bytes` to `path`, mode 600, created fresh (a stale file is replaced).
pub fn write_private(path: &Path, bytes: &[u8]) -> Result<(), String> {
let _ = std::fs::remove_file(path);
let mut f = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path).map_err(|e| format!("{}: {e}", path.display()))?;
f.write_all(bytes).and_then(|_| f.sync_all()).map_err(|e| format!("{}: {e}", path.display()))
}
impl Custody {
/// The custodian at `dir` (made, mode 700, when absent).
pub fn open(dir: impl Into<PathBuf>) -> Result<Custody, String> {
let dir = dir.into();
std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).map_err(|e| format!("{}: {e}", dir.display()))?;
Ok(Custody { dir })
}
pub fn dir(&self) -> &Path {
&self.dir
}
fn sealed_path(&self, company: &str) -> PathBuf {
self.dir.join(format!("{company}.sealed"))
}
/// The custody key: the one in `kek`, else 32 random bytes made here, mode 600.
///
/// **Read or made under one lock** (found by the UpCloud mockup, 2026-10-08:
/// two orders paid in the same second — capture at the order — both found
/// no key, both made one, the second write replaced the first, and the
/// first estate's seal no longer opened: "WrongKey").
fn kek(&self) -> Result<[u8; 32], String> {
static MINTING: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _one = MINTING.lock().unwrap_or_else(|p| p.into_inner());
let path = self.dir.join(KEK_FILE);
match std::fs::read(&path) {
Ok(b) => arr(&b, "the custody key"),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let k = random32()?;
write_private(&path, &k)?;
Ok(k)
}
Err(e) => Err(format!("{}: {e}", path.display())),
}
}
/// Whether the estate's secrets are kept here.
pub fn holds(&self, company: &str) -> bool {
self.sealed_path(company).is_file()
}
/// **Make the estate's secrets and seal them** — once: a second call for
/// the same company keeps what is there (a resumed job must not re-key an
/// estate whose boxes already trust the first seed).
///
/// A seal made before the first-login invite or the estate's client key
/// existed is given them here, every other secret kept (a job resumed past
/// the hold calls this again before its install).
pub fn mint(&self, company: &str) -> Result<(), String> {
if self.holds(company) {
let mut s = self.unseal(company)?;
if s.first_login.is_none() || s.estate_client.is_none() {
if s.first_login.is_none() {
s.first_login = Some(random32()?);
}
if s.estate_client.is_none() {
s.estate_client = Some(random32()?);
}
self.seal(company, &s)?;
}
return Ok(());
}
let s = EstateSecrets {
signing_seed: random32()?,
pairing_seed: random32()?,
break_glass: random32()?,
first_login: Some(random32()?),
estate_client: Some(random32()?),
pairing_material: Vec::new(),
};
self.seal(company, &s)
}
fn seal(&self, company: &str, s: &EstateSecrets) -> Result<(), String> {
let plain = serde_json::to_vec(&Plain {
signing_seed: hex(&s.signing_seed),
pairing_seed: hex(&s.pairing_seed),
break_glass: hex(&s.break_glass),
first_login: s.first_login.as_ref().map(|b| hex(b)).unwrap_or_default(),
estate_client: s.estate_client.as_ref().map(|b| hex(b)).unwrap_or_default(),
pairing_material: s.pairing_material.clone(),
})
.map_err(|e| e.to_string())?;
let sealed = seal::seal(&self.kek()?, MAGIC, &aad(company), &plain).map_err(|e| format!("custody: {e}"))?;
let tmp = self.dir.join(format!(".{company}.sealed.tmp"));
write_private(&tmp, &sealed)?;
std::fs::rename(&tmp, self.sealed_path(company)).map_err(|e| format!("custody: {e}"))
}
/// **Open the estate's seal**, in memory.
pub fn unseal(&self, company: &str) -> Result<EstateSecrets, String> {
let path = self.sealed_path(company);
let sealed = std::fs::read(&path).map_err(|e| format!("custody: {company} is not held here ({e})"))?;
let plain = seal::open(&self.kek()?, MAGIC, &aad(company), &sealed).map_err(|e| format!("custody: {company}'s seal does not open under this custody key ({e:?})"))?;
let p: Plain = serde_json::from_slice(&plain).map_err(|e| format!("custody: {e}"))?;
Ok(EstateSecrets {
signing_seed: arr(&unhex(&p.signing_seed)?, "the signing seed")?,
pairing_seed: arr(&unhex(&p.pairing_seed)?, "the pairing seed")?,
break_glass: arr(&unhex(&p.break_glass)?, "the break-glass bearer")?,
first_login: if p.first_login.is_empty() { None } else { Some(arr(&unhex(&p.first_login)?, "the first-login invite")?) },
estate_client: if p.estate_client.is_empty() { None } else { Some(arr(&unhex(&p.estate_client)?, "the estate's client key")?) },
pairing_material: p.pairing_material,
})
}
/// **Unseal for one step**: the files the kit reads, in `dir` (mode 700,
/// each file 600). Deleted when the returned [`HandOff`] is dropped.
pub fn hand_off(&self, company: &str, dir: &Path) -> Result<HandOff, String> {
let s = self.unseal(company)?;
let _ = std::fs::remove_dir_all(dir);
std::fs::create_dir_all(dir.join(handoff::RETURN_DIR)).map_err(|e| format!("{}: {e}", dir.display()))?;
for d in [dir.to_path_buf(), dir.join(handoff::RETURN_DIR)] {
std::fs::set_permissions(&d, std::fs::Permissions::from_mode(0o700)).map_err(|e| format!("{}: {e}", d.display()))?;
}
let guard = HandOff { dir: dir.to_path_buf() };
write_private(&dir.join(handoff::ROOT_SEED), &s.signing_seed)?;
write_private(&dir.join(handoff::CONSOLE_TOKEN), hex(&s.break_glass).as_bytes())?;
write_private(&dir.join(handoff::PAIRING_SEED), hex(&s.pairing_seed).as_bytes())?;
if let Some(invite) = &s.first_login {
write_private(&dir.join(handoff::FIRST_LOGIN), hex(invite).as_bytes())?;
}
if let Some(seed) = &s.estate_client {
write_private(&dir.join(handoff::ESTATE_CLIENT), hex(seed).as_bytes())?;
}
// A later step reaches the estate with the pairing material the install
// handed back (the installer CA and its keys, as the kit's material
// directory holds them) — until the hand-off forgets it all. An upgrade or
// a recharge of a sold estate therefore has NO material here: the estate
// took its own (`handoff::INSTALLER_MATERIAL`, F4).
if !s.pairing_material.is_empty() {
let m = dir.join(handoff::MATERIAL_DIR);
std::fs::create_dir_all(&m).map_err(|e| format!("{}: {e}", m.display()))?;
std::fs::set_permissions(&m, std::fs::Permissions::from_mode(0o700)).map_err(|e| format!("{}: {e}", m.display()))?;
for (name, text) in &s.pairing_material {
if name.contains('/') || name.starts_with('.') {
return Err(format!("custody: a handed-back file is named {name:?}"));
}
write_private(&m.join(name), text.as_bytes())?;
}
}
Ok(guard)
}
/// **The buyer's first-login invite**, 64 hex, for the "ready" mail's
/// link — `None` when the seal holds none. Never logged by a caller.
pub fn first_login(&self, company: &str) -> Result<Option<String>, String> {
Ok(self.unseal(company)?.first_login.map(|b| hex(&b)))
}
/// **The public half of the estate's client key**, 64 hex — what the order
/// records and the shop verifies the estate's calls under. `None` when the
/// seal holds none yet ([`Self::mint`] adds one).
pub fn estate_client_public(&self, company: &str) -> Result<Option<String>, String> {
Ok(self.unseal(company)?.estate_client.map(|seed| hex(&monetize::Signer::from_bytes(&seed).public_key())))
}
fn credential_path(&self, name: &str) -> PathBuf {
self.dir.join(format!("credential.{name}.sealed"))
}
fn credential_name_ok(name: &str) -> Result<(), String> {
if name.is_empty() || !name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '.' || c == '-') || name.starts_with('.') {
return Err(format!("custody: {name:?} is not a credential's name (lowercase letters, digits, `.` and `-`)"));
}
Ok(())
}
/// **Seal one of THIS server's own credentials** (a DNS writer's zone and
/// token: `dns.cloudflare`) under the custody key, the name bound into the
/// seal. Replaces what was sealed under the name.
pub fn seal_credential(&self, name: &str, plain: &[u8]) -> Result<(), String> {
Self::credential_name_ok(name)?;
let sealed = seal::seal(&self.kek()?, MAGIC, &credential_aad(name), plain).map_err(|e| format!("custody: {e}"))?;
let tmp = self.dir.join(format!(".credential.{name}.sealed.tmp"));
write_private(&tmp, &sealed)?;
std::fs::rename(&tmp, self.credential_path(name)).map_err(|e| format!("custody: {e}"))
}
/// **Open one credential**, in memory, for the call that needs it.
pub fn unseal_credential(&self, name: &str) -> Result<Vec<u8>, String> {
Self::credential_name_ok(name)?;
let sealed = std::fs::read(self.credential_path(name)).map_err(|_| format!("custody: no credential {name} is sealed here (`monetize-server --custody-credential {name}` seals one from stdin)"))?;
seal::open(&self.kek()?, MAGIC, &credential_aad(name), &sealed).map_err(|e| format!("custody: the credential {name} does not open under this custody key ({e:?})"))
}
/// **The pairing material sealed for `company`** (the kit's hand-back, or the shop's own
/// pairing's mint), by file name. Never logged by a caller.
pub fn pairing_files(&self, company: &str) -> Result<Vec<(String, String)>, String> {
Ok(self.unseal(company)?.pairing_material.clone())
}
/// **Seal pairing material minted in this process** (the shop's own pairing, before the
/// lay): added to what is sealed, a file of the same name replaced.
pub fn seal_pairing(&self, company: &str, files: Vec<(String, String)>) -> Result<(), String> {
for (name, _) in &files {
if name.is_empty() || name.contains('/') || name.starts_with('.') {
return Err(format!("custody: a pairing file is named {name:?}"));
}
}
let mut s = self.unseal(company)?;
s.pairing_material.retain(|(n, _)| !files.iter().any(|(m, _)| m == n));
s.pairing_material.extend(files);
s.pairing_material.sort();
self.seal(company, &s)
}
/// **Seal what the kit handed back** (its `return/` directory: the pairing
/// material it minted) into the estate's custody, beside the seeds.
/// Answers how many files were taken. The plain copies go with the hand-off.
pub fn take_back(&self, company: &str, handoff: &HandOff) -> Result<usize, String> {
let back = handoff.dir.join(handoff::RETURN_DIR);
let mut files: Vec<(String, String)> = Vec::new();
if let Ok(rd) = std::fs::read_dir(&back) {
for e in rd.flatten() {
if e.path().is_file() {
let text = std::fs::read_to_string(e.path()).map_err(|x| format!("{}: {x}", e.path().display()))?;
files.push((e.file_name().to_string_lossy().into_owned(), text));
}
}
}
if files.is_empty() {
return Ok(0);
}
let mut s = self.unseal(company)?;
let n = files.len();
// A file handed back replaces the one of its name; the others stand (F4: the pairing
// hands back only its bundle beside the material minted at the payment).
let mut all: std::collections::BTreeMap<String, String> = std::mem::take(&mut s.pairing_material).into_iter().collect();
all.extend(files);
s.pairing_material = all.into_iter().collect();
self.seal(company, &s)?;
Ok(n)
}
/// **The installer material its estate keeps** ([`handoff::INSTALLER_MATERIAL`]), while the
/// shop still holds the estate's seal: `None` when the install handed none back.
pub fn installer_material(&self, company: &str) -> Result<Option<String>, String> {
Ok(self.unseal(company)?.pairing_material.iter().find(|(n, _)| n == handoff::INSTALLER_MATERIAL).map(|(_, t)| t.clone()))
}
/// **The estate's installer link file** ([`LINK_KEYS_FILE`]: the installer's WireGuard/X25519
/// private key and the VM's public key), read IN PLACE for the step that needs it (the
/// DEV-NARRATE receiver, lane N2): from the estate's seal while it is held, else from the
/// link seal a narrated site keeps past its hand-off ([`Self::forget_keeping_link`]).
/// `None`: neither holds one. Never written anywhere, never logged by a caller.
pub fn installer_link(&self, company: &str) -> Result<Option<String>, String> {
if self.holds(company) {
return Ok(self.unseal(company)?.pairing_material.iter().find(|(n, _)| n == LINK_KEYS_FILE).map(|(_, t)| t.clone()));
}
let path = self.dir.join(format!("{company}{LINK_SUFFIX}"));
let sealed = match std::fs::read(&path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(format!("custody: {e}")),
};
let plain = seal::open(&self.kek()?, MAGIC, &link_aad(company), &sealed).map_err(|e| format!("custody: {company}'s link seal does not open under this custody key ({e:?})"))?;
String::from_utf8(plain).map(Some).map_err(|e| format!("custody: {company}'s link file: {e}"))
}
/// **The hand-off of a NARRATED site** (lane N2): every secret forgotten as [`Self::forget`]
/// forgets it, except the installer's link file, which moves out of the estate's seal into
/// a seal of its own (nothing else is in it), so the site's DEV-NARRATE receiver keeps
/// hearing its boxes. Never a plain copy: the file goes from one seal to the other.
pub fn forget_keeping_link(&self, company: &str) -> Result<(), String> {
if let Some(link) = self.installer_link(company)? {
let sealed = seal::seal(&self.kek()?, MAGIC, &link_aad(company), link.as_bytes()).map_err(|e| format!("custody: {e}"))?;
let tmp = self.dir.join(format!(".{company}{LINK_SUFFIX}.tmp"));
write_private(&tmp, &sealed)?;
std::fs::rename(&tmp, self.dir.join(format!("{company}{LINK_SUFFIX}"))).map_err(|e| format!("custody: {e}"))?;
}
for p in [self.sealed_path(company), self.dir.join(format!("{company}{LEGACY_READER_SUFFIX}"))] {
match std::fs::remove_file(&p) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(format!("custody: {e}")),
}
}
Ok(())
}
fn offsite_path(&self, company: &str) -> PathBuf {
self.dir.join(format!("{company}{OFFSITE_SUFFIX}"))
}
/// The off-site seal's files, `None` when there is none.
fn offsite_files(&self, company: &str) -> Result<Option<Vec<(String, String)>>, String> {
let sealed = match std::fs::read(self.offsite_path(company)) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(format!("custody: {e}")),
};
let plain = seal::open(&self.kek()?, MAGIC, &offsite_aad(company), &sealed).map_err(|e| format!("custody: {company}'s off-site seal does not open under this custody key ({e:?})"))?;
serde_json::from_slice(&plain).map(Some).map_err(|e| format!("custody: {company}'s off-site seal: {e}"))
}
/// One file of the estate's off-site pair: from the estate's seal while it is held, else
/// from the off-site seal the hand-off kept.
fn offsite_file(&self, company: &str, name: &str) -> Result<Option<String>, String> {
let find = |files: &[(String, String)]| files.iter().find(|(n, _)| n == name).map(|(_, t)| t.trim().to_string()).filter(|t| !t.is_empty());
if self.holds(company) {
if let Some(t) = find(&self.unseal(company)?.pairing_material) {
return Ok(Some(t));
}
}
Ok(self.offsite_files(company)?.as_deref().and_then(find))
}
/// **The estate's off-site recipient** (lane O1): the PUBLIC half of its off-site pair,
/// base64 — what its main is fed as `offsite_recipient`. `None`: no pair was minted.
pub fn offsite_recipient(&self, company: &str) -> Result<Option<String>, String> {
self.offsite_file(company, OFFSITE_PUB_FILE)
}
/// **The private half of the estate's off-site pair**, base64, for the operator's restore
/// of one of its copies. Never logged, never written by a caller.
pub fn offsite_identity(&self, company: &str) -> Result<Option<String>, String> {
self.offsite_file(company, OFFSITE_KEY_FILE)
}
/// The estate's off-site pair moved from its seal into the off-site seal (nothing else in
/// it); a pair already kept is kept. Nothing to keep: nothing is written.
fn keep_offsite(&self, company: &str) -> Result<(), String> {
if !self.holds(company) {
return Ok(());
}
let pair: Vec<(String, String)> = self.unseal(company)?.pairing_material.iter().filter(|(n, _)| n == OFFSITE_KEY_FILE || n == OFFSITE_PUB_FILE).cloned().collect();
if pair.len() != 2 {
return Ok(());
}
let plain = serde_json::to_vec(&pair).map_err(|e| e.to_string())?;
let sealed = seal::seal(&self.kek()?, MAGIC, &offsite_aad(company), &plain).map_err(|e| format!("custody: {e}"))?;
let tmp = self.dir.join(format!(".{company}{OFFSITE_SUFFIX}.tmp"));
write_private(&tmp, &sealed)?;
std::fs::rename(&tmp, self.offsite_path(company)).map_err(|e| format!("custody: {e}"))
}
/// **The hand-off's forgetting** (lane O1): every secret of the estate forgotten as
/// [`Self::forget`] forgets it — the installer link kept in its own seal when `keep_link`
/// (a narrated site, [`Self::forget_keeping_link`]) — except the estate's off-site pair,
/// which the operator keeps to open the estate's copies ([`OFFSITE_SUFFIX`]).
pub fn forget_handed_off(&self, company: &str, keep_link: bool) -> Result<(), String> {
self.keep_offsite(company)?;
if keep_link {
return self.forget_keeping_link(company);
}
for p in [self.sealed_path(company), self.dir.join(format!("{company}{LEGACY_READER_SUFFIX}")), self.dir.join(format!("{company}{LINK_SUFFIX}"))] {
match std::fs::remove_file(&p) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(format!("custody: {e}")),
}
}
Ok(())
}
/// **Forget an estate** — one never delivered (its job was released or
/// refunded), one deleted, or one handed off (the owner via the
/// coordinator, 2026-10-08: after a PASSed hand-off the shop keeps only its
/// own facts key and the estate's PUBLIC client key): every secret — the
/// seeds, bearer, backup key, invite, client seed, the CA's key and the
/// operator's certificate — and a 5.3.1 health reader left behind.
pub fn forget(&self, company: &str) -> Result<(), String> {
for p in [self.sealed_path(company), self.dir.join(format!("{company}{LEGACY_READER_SUFFIX}")), self.dir.join(format!("{company}{LINK_SUFFIX}")), self.offsite_path(company)] {
match std::fs::remove_file(&p) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(format!("custody: {e}")),
}
}
Ok(())
}
}
/// **The unsealed files of one step.** Deleted when dropped — whatever the
/// step's outcome, a crash of the step included.
#[derive(Debug)]
pub struct HandOff {
dir: PathBuf,
}
impl HandOff {
pub fn dir(&self) -> &Path {
&self.dir
}
}
impl Drop for HandOff {
fn drop(&mut self) {
// Overwrite before unlinking: a deleted file's blocks are not erased
// (the files of `return/` and `material/` included).
fn wipe(dir: &Path) {
if let Ok(rd) = std::fs::read_dir(dir) {
for e in rd.flatten() {
match e.metadata() {
Ok(m) if m.is_file() => {
let _ = std::fs::write(e.path(), vec![0u8; m.len() as usize]);
}
Ok(m) if m.is_dir() => wipe(&e.path()),
_ => {}
}
}
}
}
wipe(&self.dir);
let _ = std::fs::remove_dir_all(&self.dir);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// ★ Two estates paid in the same second: one custody key, every seal opens.
#[test]
fn estates_sealed_at_once_share_one_custody_key() {
let dir = std::env::temp_dir().join(format!("monetize-custody-race-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let names: Vec<String> = (0..8).map(|i| format!("co{i}")).collect();
let threads: Vec<_> = names
.iter()
.cloned()
.map(|n| {
let d = dir.clone();
std::thread::spawn(move || Custody::open(d).unwrap().mint(&n).unwrap())
})
.collect();
for t in threads {
t.join().unwrap();
}
let c = Custody::open(&dir).unwrap();
for n in &names {
c.unseal(n).unwrap_or_else(|e| panic!("{n}: {e}"));
}
let _ = std::fs::remove_dir_all(&dir);
}
fn tmp(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("monetize-custody-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
#[test]
fn a_sealed_estate_opens_only_under_its_own_name_and_key() {
let dir = tmp("seal");
let c = Custody::open(&dir).unwrap();
c.mint("acme").unwrap();
let first = c.unseal("acme").unwrap().signing_seed;
c.mint("acme").unwrap();
assert_eq!(c.unseal("acme").unwrap().signing_seed, first, "a second mint keeps the estate's seed");
// The file is not the secret.
let sealed = std::fs::read(dir.join("acme.sealed")).unwrap();
assert!(sealed.starts_with(b"MCUS1\0"));
assert!(!String::from_utf8_lossy(&sealed).contains(&hex(&first)), "the seed is not in the sealed file");
// Another estate's name does not open it.
std::fs::copy(dir.join("acme.sealed"), dir.join("evil.sealed")).unwrap();
assert!(c.unseal("evil").is_err(), "the company name is bound into the seal");
// Another custody key does not open it.
std::fs::remove_file(dir.join(KEK_FILE)).unwrap();
assert!(c.unseal("acme").is_err(), "only this custody key opens it");
let mode = std::fs::metadata(dir.join(KEK_FILE)).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
}
#[test]
fn a_hand_off_is_private_and_gone_when_dropped_and_the_return_is_sealed() {
let dir = tmp("handoff");
let c = Custody::open(dir.join("custody")).unwrap();
c.mint("acme").unwrap();
let at = dir.join("step");
{
let h = c.hand_off("acme", &at).unwrap();
let seed = std::fs::read(at.join(handoff::ROOT_SEED)).unwrap();
assert_eq!(seed.len(), 32);
assert_eq!(std::fs::metadata(at.join(handoff::ROOT_SEED)).unwrap().permissions().mode() & 0o777, 0o600);
assert_eq!(std::fs::metadata(&at).unwrap().permissions().mode() & 0o777, 0o700);
std::fs::write(at.join(handoff::RETURN_DIR).join("ca.key.pem"), "-----BEGIN PRIVATE KEY-----x").unwrap();
assert_eq!(c.take_back("acme", &h).unwrap(), 1);
}
assert!(!at.exists(), "the hand-off is deleted when the step ends");
let s = c.unseal("acme").unwrap();
assert_eq!(s.pairing_material, vec![("ca.key.pem".to_string(), "-----BEGIN PRIVATE KEY-----x".to_string())]);
assert!(!format!("{s:?}").contains("BEGIN"), "Debug names, never values");
c.forget("acme").unwrap();
assert!(!c.holds("acme"));
}
/// ★ **A later step gets the material the install handed back**: the
/// hand-off carries `material/` (the kit's installer material directory),
/// private, and gone — overwritten — when the step ends.
#[test]
fn a_later_hand_off_carries_the_handed_back_material_and_wipes_it() {
let dir = tmp("material");
let c = Custody::open(dir.join("custody")).unwrap();
c.mint("acme").unwrap();
let first = dir.join("install");
{
let h = c.hand_off("acme", &first).unwrap();
assert!(!first.join(handoff::MATERIAL_DIR).exists(), "nothing was handed back yet");
std::fs::write(first.join(handoff::RETURN_DIR).join("installer-ca.cert.pem"), "-----BEGIN CERTIFICATE-----ca").unwrap();
std::fs::write(first.join(handoff::RETURN_DIR).join("installer.key.pem"), "-----BEGIN PRIVATE KEY-----k").unwrap();
c.take_back("acme", &h).unwrap();
}
let later = dir.join("twin");
{
let _h = c.hand_off("acme", &later).unwrap();
let m = later.join(handoff::MATERIAL_DIR);
assert_eq!(std::fs::read_to_string(m.join("installer.key.pem")).unwrap(), "-----BEGIN PRIVATE KEY-----k");
assert_eq!(std::fs::metadata(m.join("installer.key.pem")).unwrap().permissions().mode() & 0o777, 0o600);
assert_eq!(std::fs::metadata(&m).unwrap().permissions().mode() & 0o777, 0o700);
}
assert!(!later.exists(), "the later hand-off is gone with its material");
}
/// ★ **Every estate is minted its buyer's first-login invite** (private
/// ownership, 2026-10-07): sealed with the other secrets, kept across a
/// second mint, handed to the kit as `first-login-invite` (0600) — the
/// kit feeds its hash — and read back for the "ready" mail's link. A seal
/// made before the invite existed is given one by the next mint, every
/// other secret kept.
#[test]
fn every_estate_is_minted_a_first_login_invite_handed_to_the_kit_and_an_older_seal_gets_one() {
let dir = tmp("first-login");
let c = Custody::open(dir.join("custody")).unwrap();
c.mint("acme").unwrap();
let invite = c.first_login("acme").unwrap().expect("minted with the estate's secrets");
assert_eq!(invite.len(), 64);
assert!(invite.bytes().all(|b| b.is_ascii_hexdigit()));
c.mint("acme").unwrap();
assert_eq!(c.first_login("acme").unwrap().as_deref(), Some(invite.as_str()), "a second mint keeps it");
assert!(!String::from_utf8_lossy(&std::fs::read(dir.join("custody/acme.sealed")).unwrap()).contains(&invite), "sealed, never in the clear");
let at = dir.join("install");
{
let _h = c.hand_off("acme", &at).unwrap();
assert_eq!(std::fs::read_to_string(at.join(handoff::FIRST_LOGIN)).unwrap(), invite);
assert_eq!(std::fs::metadata(at.join(handoff::FIRST_LOGIN)).unwrap().permissions().mode() & 0o777, 0o600);
}
// A seal from before the invite existed.
let old = EstateSecrets { signing_seed: [1; 32], pairing_seed: [2; 32], break_glass: [3; 32], first_login: None, estate_client: None, pairing_material: vec![("ca.pem".into(), "x".into())] };
c.seal("bolt", &old).unwrap();
assert_eq!(c.first_login("bolt").unwrap(), None);
c.mint("bolt").unwrap();
let s = c.unseal("bolt").unwrap();
assert!(s.first_login.is_some(), "the next mint adds the invite");
assert_eq!((s.signing_seed, s.pairing_seed, s.break_glass), ([1; 32], [2; 32], [3; 32]), "every other secret kept");
assert_eq!(s.pairing_material.len(), 1);
}
/// ★ **A sold estate's client key** (one shop, 2026-10-07): minted once
/// with the estate's other secrets (and given to a seal from before it
/// existed), sealed, handed off as 64 hex for the kit to feed the estate's
/// front, and named by its PUBLIC half — the key the shop verifies the
/// estate's calls under.
#[test]
fn a_sold_estates_client_key_is_minted_once_handed_off_and_named_by_its_public_half() {
let dir = tmp("estate-client");
let c = Custody::open(dir.join("custody")).unwrap();
c.mint("acme").unwrap();
let public = c.estate_client_public("acme").unwrap().expect("minted with the estate");
assert_eq!(public.len(), 64);
c.mint("acme").unwrap();
assert_eq!(c.estate_client_public("acme").unwrap().as_deref(), Some(public.as_str()), "a second mint keeps it");
let at = dir.join("install");
{
let _h = c.hand_off("acme", &at).unwrap();
let seed = std::fs::read_to_string(at.join(handoff::ESTATE_CLIENT)).unwrap();
assert_eq!(seed.len(), 64);
assert_eq!(std::fs::metadata(at.join(handoff::ESTATE_CLIENT)).unwrap().permissions().mode() & 0o777, 0o600);
let seed: [u8; 32] = unhex(&seed).unwrap().try_into().unwrap();
assert_eq!(hex(&monetize::Signer::from_bytes(&seed).public_key()), public, "the handed-off seed is the named key's");
assert!(!String::from_utf8_lossy(&std::fs::read(dir.join("custody/acme.sealed")).unwrap()).contains(&hex(&seed)), "sealed, never in the clear");
}
let old = EstateSecrets { signing_seed: [1; 32], pairing_seed: [2; 32], break_glass: [3; 32], first_login: Some([5; 32]), estate_client: None, pairing_material: Vec::new() };
c.seal("bolt", &old).unwrap();
assert_eq!(c.estate_client_public("bolt").unwrap(), None);
c.mint("bolt").unwrap();
let s = c.unseal("bolt").unwrap();
assert!(s.estate_client.is_some(), "the next mint adds the client key");
assert_eq!((s.signing_seed, s.first_login), ([1; 32], Some([5; 32])), "every other secret kept");
}
/// ★ **This server's own credential** (a DNS writer's zone and token) is
/// sealed under its name and the custody key, opens only under both, and
/// is never in the file in the clear; an unsealed name says how to seal it.
#[test]
fn a_credential_is_sealed_under_its_name_and_opens_only_there() {
let dir = tmp("credential");
let c = Custody::open(&dir).unwrap();
let plain = br#"{"zone_id": "z0ne-id", "api_token": "cf-s3cr3t-token"}"#;
let missing = c.unseal_credential("dns.cloudflare").unwrap_err();
assert!(missing.contains("--custody-credential dns.cloudflare"), "{missing}");
c.seal_credential("dns.cloudflare", plain).unwrap();
assert_eq!(c.unseal_credential("dns.cloudflare").unwrap(), plain.to_vec());
let file = std::fs::read(dir.join("credential.dns.cloudflare.sealed")).unwrap();
assert!(!String::from_utf8_lossy(&file).contains("cf-s3cr3t"), "the token is not in the sealed file");
assert_eq!(std::fs::metadata(dir.join("credential.dns.cloudflare.sealed")).unwrap().permissions().mode() & 0o777, 0o600);
std::fs::copy(dir.join("credential.dns.cloudflare.sealed"), dir.join("credential.dns.other.sealed")).unwrap();
assert!(c.unseal_credential("dns.other").is_err(), "the name is bound into the seal");
std::fs::copy(dir.join("credential.dns.cloudflare.sealed"), dir.join("acme.sealed")).unwrap();
assert!(c.unseal("acme").is_err(), "a credential is not an estate's seal");
assert!(c.seal_credential("../escape", plain).is_err() && c.seal_credential("", plain).is_err());
}
/// ★ **After the hand-off the shop keeps nothing of the estate** (ONE monetize, F4
/// 2026-10-10: the twin is read from the estate's reports, so no health reader is kept):
/// the seal with the seeds, bearer, backup key, the CA's key and the operator's certificate
/// is gone, and a 5.3.1 reader left beside it goes too.
#[test]
fn after_the_hand_off_nothing_of_the_estate_is_kept() {
let dir = tmp("reader");
let c = Custody::open(dir.join("custody")).unwrap();
c.mint("acme").unwrap();
let at = dir.join("step");
{
let h = c.hand_off("acme", &at).unwrap();
for (name, text) in [("ca.cert.pem", "CA CERT"), ("ca.key.pem", "CA KEY"), ("installer.cert.pem", "OP CERT"), ("link.toml", "LINK")] {
std::fs::write(at.join(handoff::RETURN_DIR).join(name), text).unwrap();
}
assert_eq!(c.take_back("acme", &h).unwrap(), 4);
}
std::fs::write(dir.join("custody").join("acme.reader.sealed"), b"a 5.3.1 reader").unwrap();
c.forget("acme").unwrap();
assert!(!c.holds("acme"), "the estate's seal is gone");
assert!(c.unseal("acme").is_err() && c.first_login("acme").is_err(), "no seed, bearer, invite or key opens any more");
let left: Vec<_> = std::fs::read_dir(dir.join("custody")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).filter(|n| n.starts_with("acme")).collect();
assert!(left.is_empty(), "no reader kept beside it: {left:?}");
}
/// ★ **F4: the pairing's bundle is kept beside the material, until the estate takes it**:
/// a take-back adds the files handed back and keeps the others (the material minted at the
/// payment stands); the bundle reads back while the seal stands, and is gone with it.
#[test]
fn the_installer_material_the_estate_keeps_is_held_beside_the_material_until_the_hand_off() {
let dir = tmp("bundle");
let c = Custody::open(dir.join("custody")).unwrap();
c.mint("acme").unwrap();
c.seal_pairing("acme", vec![("ca.cert.pem".into(), "CA CERT".into()), ("ca.key.pem".into(), "CA KEY".into())]).unwrap();
assert_eq!(c.installer_material("acme").unwrap(), None, "none handed back yet");
let at = dir.join("step");
{
let h = c.hand_off("acme", &at).unwrap();
std::fs::write(at.join(handoff::RETURN_DIR).join(handoff::INSTALLER_MATERIAL), "{\"v\":\"installer-material/1\"}").unwrap();
assert_eq!(c.take_back("acme", &h).unwrap(), 1);
}
let names: Vec<String> = c.pairing_files("acme").unwrap().into_iter().map(|(n, _)| n).collect();
assert_eq!(names, ["ca.cert.pem", "ca.key.pem", handoff::INSTALLER_MATERIAL], "the material minted at the payment stands beside the bundle");
assert_eq!(c.installer_material("acme").unwrap().as_deref(), Some("{\"v\":\"installer-material/1\"}"));
c.forget("acme").unwrap();
assert!(c.installer_material("acme").is_err(), "forgotten at the hand-off with the rest");
}
/// ★ **A narrated site's hand-off keeps its installer link file, and ONLY it** (lane N2):
/// read in place from the estate's seal before, from its own seal after; every other
/// secret is forgotten, and a full forget (terminate, refund) removes the link seal too.
#[test]
fn a_narrated_hand_off_keeps_the_link_file_alone_and_forget_ends_it() {
let dir = tmp("narrate-link");
let c = Custody::open(&dir).unwrap();
assert_eq!(c.installer_link("site").unwrap(), None, "nothing held: no link");
c.mint("site").unwrap();
let link = "installer_private = \"secret-ish\"\nvm_public = \"pub\"\n".to_string();
c.seal_pairing("site", vec![(LINK_KEYS_FILE.into(), link.clone()), ("ca.key".into(), "CA KEY".into())]).unwrap();
assert_eq!(c.installer_link("site").unwrap().as_deref(), Some(link.as_str()), "read in place from the estate's seal");
c.forget_keeping_link("site").unwrap();
assert!(!c.holds("site"), "every other secret is forgotten");
assert!(c.unseal("site").is_err() && c.pairing_files("site").is_err());
assert_eq!(c.installer_link("site").unwrap().as_deref(), Some(link.as_str()), "the link file alone survives, in its own seal");
let on_disk: Vec<u8> = std::fs::read_dir(&dir).unwrap().flatten().filter(|e| e.path().is_file()).flat_map(|e| std::fs::read(e.path()).unwrap()).collect();
assert!(!on_disk.windows(10).any(|w| w == b"secret-ish"), "never a plain copy on disk");
c.forget("site").unwrap();
assert_eq!(c.installer_link("site").unwrap(), None, "a full forget ends the link seal too");
let _ = std::fs::remove_dir_all(&dir);
}
/// ★ **Lane O1: the hand-off keeps the estate's OFF-SITE pair with the operator** — read
/// from the estate's seal while it is held, from its own seal after the hand-off (with or
/// without the narrated link); every other secret is forgotten; a full forget (refund,
/// deletion) shreds it. An estate whose material carries no pair keeps nothing.
#[test]
fn the_hand_off_keeps_the_estates_offsite_pair_and_forget_shreds_it() {
let dir = tmp("offsite-pair");
let c = Custody::open(&dir).unwrap();
let (key, public) = (hex(&random32().unwrap()), hex(&random32().unwrap()));
for (co, narrated) in [("plain", false), ("narrated", true)] {
assert_eq!(c.offsite_recipient(co).unwrap(), None, "nothing held: no recipient");
c.mint(co).unwrap();
c.seal_pairing(co, vec![(OFFSITE_KEY_FILE.into(), key.clone()), (OFFSITE_PUB_FILE.into(), public.clone()), (LINK_KEYS_FILE.into(), "LINK".into()), ("ca.key".into(), "CA KEY".into())]).unwrap();
assert_eq!(c.offsite_recipient(co).unwrap().as_deref(), Some(public.as_str()), "{co}: read in place while held");
c.forget_handed_off(co, narrated).unwrap();
assert!(!c.holds(co) && c.pairing_files(co).is_err(), "{co}: every other secret is forgotten");
assert_eq!(c.offsite_recipient(co).unwrap().as_deref(), Some(public.as_str()), "{co}: the recipient is kept past the hand-off");
assert_eq!(c.offsite_identity(co).unwrap().as_deref(), Some(key.as_str()), "{co}: so is the private half, for the operator's restore");
assert_eq!(c.installer_link(co).unwrap().is_some(), narrated, "{co}: the link only for a narrated site");
}
let on_disk: Vec<u8> = std::fs::read_dir(&dir).unwrap().flatten().filter(|e| e.path().is_file()).flat_map(|e| std::fs::read(e.path()).unwrap()).collect();
assert!(!on_disk.windows(key.len()).any(|w| w == key.as_bytes()), "never a plain copy on disk");
c.forget("plain").unwrap();
assert_eq!((c.offsite_recipient("plain").unwrap(), c.offsite_identity("plain").unwrap()), (None, None), "a full forget shreds the pair");
c.mint("none").unwrap();
c.forget_handed_off("none", false).unwrap();
assert_eq!(c.offsite_recipient("none").unwrap(), None, "no pair minted: none kept");
assert!(!dir.join("none.offsite.sealed").exists());
let _ = std::fs::remove_dir_all(&dir);
}
}