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

nordisk/monetize

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<_>>());
    }
}