crates/monetize-admin/src/product.rs
//! **The admin box, as the installer sees it** — the FRONT of an estate of one
//! (Skidbladnir `nordisk-pairing::frontside`): kind `front`, no boxes behind it,
//! so no relay to run; it reports the relay "up" once it is fed (there is
//! nothing to relay), and the kit closes it when it is FED.
//!
//! | offer | value | who takes it |
//! |---|---|---|
//! | `version` | this build | the operator's install check |
//! | `monetize_pubkey` | monetize's signing key, PUBLIC half (hex) | the kit records it; every estate verifies monetize's signed orders with it |
//!
//! Its SECRET offer: the web certificate(s) kept on the data disk (edda
//! `estate-front::web::certs::handover`) — a successor box asks Let's Encrypt
//! nothing. Never monetize's signing key: it does not leave this box.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use nordisk_pairing::bootstrap::SecretOffer;
use nordisk_pairing::frontside::FrontProduct;
use nordisk_pairing::join::JoinReply;
use nordisk_pairing::seed::TunnelKind;
/// Where the box keeps its tunnel key (the presentation's), under the data root.
pub const WG_REL: &str = "wg";
/// **The admin box.**
pub struct AdminFront {
root: PathBuf,
tunnel: TunnelKind,
}
impl AdminFront {
pub fn new(root: &Path, tunnel: TunnelKind) -> AdminFront {
AdminFront { root: root.to_path_buf(), tunnel }
}
}
/// **What the box offers** — its build and monetize's PUBLIC signing key:
/// the installer reads it at the install, the operator's door (`GET /offer`)
/// after the close.
pub fn offers_of(root: &Path) -> BTreeMap<String, String> {
let mut o = monetize_front::intake::offers(root);
o.insert(crate::OFFER_VERSION.to_string(), crate::GIT_HASH.to_string());
o
}
impl FrontProduct for AdminFront {
fn tunnel_pubkey(&self) -> String {
if self.tunnel != TunnelKind::Wireguard {
return String::new();
}
match wg_appliance_core::ceremony::load_or_mint_identity(&self.root.join(WG_REL)) {
Ok(id) => id.public_b64,
Err(e) => {
tracing::error!(error = %e, "admin: its tunnel key cannot be minted — the join carries none");
String::new()
}
}
}
fn offer(&self, _fed: Option<&JoinReply>) -> BTreeMap<String, String> {
offers_of(&self.root)
}
fn secret_offer(&self) -> SecretOffer {
let (values, hosts) = estate_front::web::certs::handover(&self.root, std::time::SystemTime::now());
if !hosts.is_empty() {
tracing::info!(hosts = ?hosts, "installer: the web certificate(s) and key(s) handed over for the next box (values not logged)");
}
SecretOffer::new(values)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// **What the box offers is public**: its build and monetize's public
/// signing key — never a private key, which the offer (a logged value)
/// must not carry.
#[test]
fn the_offer_is_the_build_and_monetizes_public_key_and_never_a_secret() {
let dir = tempfile::tempdir().unwrap();
monetize_front::intake::prepare(dir.path()).unwrap();
let o = AdminFront::new(dir.path(), TunnelKind::None).offer(None);
assert_eq!(o.get(crate::OFFER_VERSION).map(String::as_str), Some(crate::GIT_HASH));
let key = o.get(monetize_front::intake::OFFER_MONETIZE_PUBKEY).expect("monetize's public key is offered");
assert_eq!(key.len(), 64, "32 bytes, hex: {key}");
let token = std::fs::read_to_string(dir.path().join(monetize_front::intake::TOKEN_REL)).unwrap();
assert!(o.values().all(|v| !v.contains("PRIVATE KEY") && !v.contains(token.trim())), "{o:?}");
assert_eq!(o.len(), 2, "{:?}", o.keys().collect::<Vec<_>>());
}
}