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

nordisk/Skidbladnir

crates/installer/plugin-door-wire/tests/attach_enrolment.rs

//! f-attach-enrol (money 2026-09-28): an estate on another cloud enrols with
//! the CENTRAL monetize once — a one-shot token, the owner's SSHSIG, its own
//! key's CSR signed under the attach CA — and every way to get it wrong is
//! refused by name.

use std::time::Duration;

use nordisk_pairing::mtls::{box_request, InstallerCa};
use plugin_door_wire::attach::{enrol, EnrolReply, EnrolRequest, Refusal, Tokens, NAMESPACE, VERSION};

const NOW: i64 = 1_790_600_000_000;

fn owner(seed: u8) -> ssh_key::PrivateKey {
    ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::from_seed(&[seed; 32]))
}

fn request(token: &str, estate: &str, key: &ssh_key::PrivateKey, at: i64) -> EnrolRequest {
    let (csr, _key) = box_request().unwrap();
    EnrolRequest {
        v: VERSION,
        token: token.into(),
        estate: estate.into(),
        product: "gunnar".into(),
        box_name: "main".into(),
        csr_pem: csr,
        endpoint: "gunnar.acme.example:443".into(),
        owner_key: key.public_key().to_openssh().unwrap(),
        estate_ca_pem: "-----BEGIN CERTIFICATE-----\nESTATE\n-----END CERTIFICATE-----\n".into(),
        nonce: "n0nce-0123456789abcdef".into(),
        at_unix_ms: at,
        owner_sig: String::new(),
    }
    .sign(key)
    .unwrap()
}

fn tokens() -> (tempfile::TempDir, Tokens) {
    let dir = tempfile::tempdir().unwrap();
    let t = Tokens::at(dir.path().join("attach-tokens.json"));
    (dir, t)
}

fn reply(ca: &InstallerCa) -> impl FnOnce(String) -> EnrolReply + '_ {
    move |cert| EnrolReply {
        cert_pem: cert,
        attach_ca_pem: ca.cert_pem.clone(),
        monetize_identity: "monetize-central".into(),
        monetize_identity_key: owner(9).public_key().to_openssh().unwrap(),
        monetize_pubkey: "00".repeat(32),
        plugin_door: "monetize.ignalina.dk:8475".into(),
        product: "gunnar".into(),
        estate_bearer: String::new(),
    }
}

#[test]
fn an_estate_enrols_once_with_a_token_and_its_owners_signature_and_gets_a_certificate_under_the_attach_ca() {
    let (_d, t) = tokens();
    let ca = InstallerCa::mint("monetize attach", Duration::from_secs(3600)).unwrap();
    let token = t.mint("acme", "gunnar", NOW, 3_600_000, [7u8; 32]).unwrap();
    assert!(token.starts_with("att1."));
    assert!(!std::fs::read_to_string(_d.path().join("attach-tokens.json")).unwrap().contains(token.rsplit('.').next().unwrap()), "the store holds no usable token");
    let key = owner(1);
    let req = request(&token, "acme", &key, NOW);
    let out = enrol(&req, &t, NOW + 1_000, |csr, cn| ca.sign_attached(csr, cn, "gunnar.acme.example", Duration::from_secs(3600)).map_err(|e| e.to_string()), reply(&ca)).unwrap();
    assert!(out.cert_pem.contains("BEGIN CERTIFICATE"));
    assert_eq!(nordisk_pairing::mtls::cert_cn(&out.cert_pem).unwrap(), "acme/main", "CN names the estate and the box");
    // The same token again: used.
    let again = enrol(&req, &t, NOW + 2_000, |_, _| Ok(String::new()), reply(&ca)).unwrap_err();
    assert_eq!(again, Refusal::TokenUsed);
}

#[test]
fn every_way_to_get_an_enrolment_wrong_is_refused_by_name_and_redeems_nothing() {
    let (_d, t) = tokens();
    let ca = InstallerCa::mint("monetize attach", Duration::from_secs(3600)).unwrap();
    let token = t.mint("acme", "gunnar", NOW, 3_600_000, [9u8; 32]).unwrap();
    let key = owner(2);
    let sign = |csr: &str, cn: &str| ca.sign_attached(csr, cn, "gunnar.acme.example", Duration::from_secs(3600)).map_err(|e| e.to_string());
    // Another key signed it than the one it names.
    let mut forged = request(&token, "acme", &key, NOW);
    forged.owner_key = owner(3).public_key().to_openssh().unwrap();
    assert!(matches!(enrol(&forged, &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Signature(_)));
    // A field changed after signing — the endpoint, or the CA the central is
    // to trust the estate's control plane by.
    let mut edited = request(&token, "acme", &key, NOW);
    edited.endpoint = "evil.example:443".into();
    assert!(matches!(enrol(&edited, &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Signature(_)));
    let mut swapped_ca = request(&token, "acme", &key, NOW);
    swapped_ca.estate_ca_pem = "-----BEGIN CERTIFICATE-----\nEVIL\n-----END CERTIFICATE-----\n".into();
    assert!(matches!(enrol(&swapped_ca, &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Signature(_)), "the estate's CA is signed too");
    // A signature made under another namespace (a git commit signature) never verifies here.
    let mut other_ns = request(&token, "acme", &key, NOW);
    other_ns.owner_sig = key.sign("git", ssh_key::HashAlg::Sha512, &other_ns.signed_bytes()).unwrap().to_pem(ssh_key::LineEnding::LF).unwrap();
    assert!(matches!(enrol(&other_ns, &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Signature(_)));
    assert_ne!(NAMESPACE, "git");
    // A stale request.
    assert!(matches!(enrol(&request(&token, "acme", &key, NOW - 3_600_000), &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Clock { .. }));
    // A token minted for another estate.
    assert!(matches!(enrol(&request(&token, "other", &key, NOW), &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::TokenOtherEstate { .. }));
    // An unknown token, and an expired one.
    assert_eq!(enrol(&request("att1.0000.ffff", "acme", &key, NOW), &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::TokenUnknown);
    let late = t.mint("acme", "gunnar", NOW - 10_000, 1_000, [5u8; 32]).unwrap();
    assert_eq!(enrol(&request(&late, "acme", &key, NOW), &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::TokenExpired);
    // A bad shape.
    let mut shape = request(&token, "acme", &key, NOW);
    shape.box_name = "Main Box".into();
    assert!(matches!(shape.verify(NOW).unwrap_err(), Refusal::Shape(_)));
    // None of those spent the good token.
    assert!(enrol(&request(&token, "acme", &key, NOW), &t, NOW, sign, reply(&ca)).is_ok(), "the token was still good after every refusal");
    // Every refusal says its clause.
    assert!(Refusal::TokenUsed.to_string().starts_with("REFUSED [attach-token-used]"));
}