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

nordisk/gunnar

crates/gunnar-server/tests/web_credentials.rs

//! **A person's passkeys are rows in gunnar's identity table** (Stage L,
//! 2026-10-04; the identity core's ceremony over them since IDENTITY-HOST,
//! 2026-10-27), so they survive what the identity table survives: a restart of
//! the box, and — riding the same document the twin replicates
//! (`refs/gunnar/auth`) — a restore. Driven through the real plane over real
//! stores, reopened from disk between the write and the read.

use std::sync::Arc;

use gunnar_grpc::ControlPlane as _;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;

fn passkey(seed: u8) -> Vec<u8> {
    let key = p256::ecdsa::SigningKey::from_slice(&[seed; 32]).expect("a scalar");
    key.verifying_key().to_sec1_point(false).as_bytes().to_vec()
}

fn person(stores: &ServerStores, id: &str, kind: gunnar_core::PrincipalKind) {
    stores
        .auth()
        .add_principal(gunnar_core::Principal {
            id: id.to_owned(),
            name: id.to_owned(),
            kind,
            created: 0,
        })
        .expect("principal");
}

/// ★ **A passkey written before a restart is read after it, key and all**,
/// and goes when its principal goes.
#[test]
fn a_passkey_survives_a_restart_and_goes_with_its_principal() {
    let dir = tempfile::tempdir().expect("tempdir");
    {
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        person(&stores, "kim", gunnar_core::PrincipalKind::User);
        let plane = CoreControlPlane::new(Arc::clone(&stores));
        let added = plane
            .add_credential("kim", b"kim-yubikey", &passkey(1), "YubiKey", 7)
            .expect("kim's passkey");
        assert_eq!(added.credential_id, b"kim-yubikey".to_vec());
        assert_eq!(added.sign_count, 7);
    }
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
    let plane = CoreControlPlane::new(Arc::clone(&stores));
    let listed = plane.credentials(Some("kim")).expect("listed");
    assert_eq!(listed.len(), 1, "{listed:?}");
    assert_eq!(listed[0].label, "YubiKey");
    let stored = plane
        .credential(b"kim-yubikey")
        .expect("read")
        .expect("the passkey is still there");
    assert_eq!(
        stored.public_key,
        passkey(1),
        "the key came back byte for byte"
    );
    assert_eq!(stored.credential.principal_id, "kim");

    plane.remove_principal("kim").expect("removed");
    assert!(
        plane.credentials(None).expect("listed").is_empty(),
        "the passkey outlived kim"
    );
    assert!(plane.credential(b"kim-yubikey").expect("read").is_none());
}

/// No human credential on a machine principal, and a credential id once.
#[test]
fn a_machine_holds_no_passkey_and_an_id_is_registered_once() {
    let dir = tempfile::tempdir().expect("tempdir");
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
    person(&stores, "ci", gunnar_core::PrincipalKind::Machine);
    person(&stores, "kim", gunnar_core::PrincipalKind::User);
    let plane = CoreControlPlane::new(Arc::clone(&stores));
    let refused = plane
        .add_credential("ci", b"ci-key", &passkey(2), "ci", 0)
        .expect_err("a machine holds no passkey");
    assert!(
        matches!(refused, gunnar_grpc::ControlError::FailedPrecondition(_)),
        "{refused:?}"
    );
    plane
        .add_credential("kim", b"kim-key", &passkey(3), "laptop", 0)
        .expect("kim's");
    let twice = plane
        .add_credential("kim", b"kim-key", &passkey(4), "again", 0)
        .expect_err("an id once");
    assert!(
        matches!(twice, gunnar_grpc::ControlError::AlreadyExists(_)),
        "{twice:?}"
    );
    let ghost = plane
        .add_credential("ghost", b"ghost-key", &passkey(5), "ghost", 0)
        .expect_err("no such principal");
    assert!(
        matches!(ghost, gunnar_grpc::ControlError::NotFound(_)),
        "{ghost:?}"
    );
}

/// The box's identity core over `plane` (IDENTITY-HOST): what `serve` builds.
fn core(
    dir: &std::path::Path,
    plane: &Arc<CoreControlPlane>,
) -> Arc<gunnar_grpc::identity::GunnarIdentity> {
    gunnar_server::identity::build(
        dir,
        plane.clone(),
        &gunnar_grpc::ServerConfig::default(),
        false,
    )
}

fn relay() -> nornir_identity::Relay {
    nornir_identity::Relay {
        rp_id: "console.example".into(),
        origin: "https://console.example".into(),
        client: None,
    }
}

/// ★ **A login's counter is kept on the real plane, across a restart**, and a
/// counter that did not move is refused (the core's ceremony over Gunnar's
/// passkey; Gunnar's own session keys are gone).
#[test]
fn a_logins_counter_is_kept_and_a_replayed_counter_is_refused() {
    let dir = tempfile::tempdir().expect("tempdir");
    let mut key = nornir_identity::test_host::SoftPasskey::new(6);
    {
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        person(&stores, "kim", gunnar_core::PrincipalKind::User);
        let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
        plane
            .add_credential("kim", &key.id, &key.public_key(), "YubiKey", 0)
            .expect("kim's passkey");
        let identity = core(dir.path(), &plane);
        for _ in 0..4 {
            let c = identity.login_start(&relay()).expect("a login");
            identity
                .login_finish(key.login(&c, "https://console.example", true))
                .expect("kim logs in");
        }
    }
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
    let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
    let stored = plane.credential(&key.id).expect("read").expect("there");
    assert_eq!(
        stored.credential.sign_count, 4,
        "the counter survived the restart"
    );
    assert!(stored.credential.last_used_unix_ms.is_some());
    let identity = core(dir.path(), &plane);
    let mut replay = nornir_identity::test_host::SoftPasskey::new(6);
    let c = identity.login_start(&relay()).expect("a login");
    let refused = identity
        .login_finish(replay.login(&c, "https://console.example", true))
        .expect_err("a counter that went back");
    assert_eq!(refused.code, nornir_identity::Code::Ceremony, "{refused:?}");
}

/// ★ **An invite's grant is applied on the real plane when it is redeemed**
/// (an operator's WRITE) — once; a second redemption is refused.
#[test]
fn an_invite_is_redeemed_once_with_its_grant() {
    use gunnar_grpc::Identity as _;
    use nornir_identity::IdentityHost as _;
    let dir = tempfile::tempdir().expect("tempdir");
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
    person(&stores, "boss", gunnar_core::PrincipalKind::User);
    let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
    plane
        .grant("boss", "", "", gunnar_proto::GrantLevel::Admin)
        .expect("boss is a server admin");
    let identity = core(dir.path(), &plane);
    let mut boss = identity
        .host()
        .store()
        .user("boss")
        .expect("read")
        .expect("boss");
    boss.state = nornir_identity::UserState::Active;
    identity.host().store().put_user(&boss).expect("active");
    let token = identity
        .vouched_session("boss", true)
        .expect("boss's session")
        .token;
    let invited = identity
        .invite(
            &token,
            nornir_identity::InviteAsk {
                display: "kim".into(),
                groups: vec![gunnar_grpc::identity::SERVER_WRITE.into()],
                ..Default::default()
            },
        )
        .expect("an operator's invite");
    let c = identity
        .redeem_start(&invited.secret, "", &relay())
        .expect("the ceremony");
    let kim = identity
        .redeem_finish(
            nornir_identity::test_host::SoftPasskey::new(7).register(&c, "https://console.example"),
        )
        .expect("redeemed");
    let id = kim.session.user_id;
    assert!(
        plane
            .may(
                &id,
                &gunnar_grpc::Access::Server,
                gunnar_proto::GrantLevel::Write
            )
            .expect("asked"),
        "Write applied"
    );
    assert!(!plane
        .may(
            &id,
            &gunnar_grpc::Access::Server,
            gunnar_proto::GrantLevel::Admin
        )
        .expect("asked"));
    let again = identity
        .redeem_start(&invited.secret, "", &relay())
        .expect_err("once");
    assert_eq!(
        again.code,
        nornir_identity::Code::InviteNotOpen,
        "{again:?}"
    );
    assert_eq!(plane.credentials(Some(&id)).expect("listed").len(), 1);
}

/// The kit's passkey (`kit-dwarf-key` is its id below).
fn kit_key() -> nornir_identity::test_host::SoftPasskey {
    nornir_identity::test_host::SoftPasskey::new(3)
}

/// kit-dwarf's approval, its credential the kit's soft passkey.
fn kit_approval() -> gunnar_grpc::MachineApproval {
    let key = kit_key();
    gunnar_grpc::MachineApproval {
        credential_id: key.id.clone(),
        public_key: key.public_key(),
        ..approval("kit-dwarf", b"", 3)
    }
}

fn approval(machine: &str, id: &[u8], seed: u8) -> gunnar_grpc::MachineApproval {
    gunnar_grpc::MachineApproval {
        machine: machine.to_owned(),
        server_level: gunnar_proto::GrantLevel::Admin,
        credential_id: id.to_vec(),
        public_key: passkey(seed),
        label: format!("installer kit on {}", machine.trim_start_matches("kit-")),
        sign_count: 0,
        now_unix_ms: gunnar_core::now_ms(),
        step_up: None,
    }
}

/// ★ **An approved machine enrolment is ONE update on the real plane**
/// (2026-10-05): the MACHINE principal made, its server grant, its credential
/// — read back after a restart, logging in as the machine — while a person's
/// id under the machine's name is refused and nothing lands.
#[test]
fn an_approved_machine_enrolment_makes_a_machine_with_its_credential_and_survives_a_restart() {
    use gunnar_grpc::Identity as _;
    let dir = tempfile::tempdir().expect("tempdir");
    {
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        person(&stores, "kim", gunnar_core::PrincipalKind::User);
        let plane = CoreControlPlane::new(Arc::clone(&stores));
        plane
            .add_credential("kim", b"kim-yubikey", &passkey(1), "YubiKey", 5)
            .expect("kim's passkey");
        // kim's step-up presented a counter that went backwards: refused, nothing made.
        let mut backwards = kit_approval();
        backwards.step_up = Some((b"kim-yubikey".to_vec(), 4));
        let refused = plane
            .approve_machine_enrolment(backwards)
            .expect_err("a rolled-back counter");
        assert!(
            matches!(refused, gunnar_grpc::ControlError::PermissionDenied(_)),
            "{refused:?}"
        );
        assert!(plane.principal("kit-dwarf").expect("read").is_none());
        let mut fresh = kit_approval();
        fresh.step_up = Some((b"kim-yubikey".to_vec(), 6));
        let approved = plane.approve_machine_enrolment(fresh).expect("approved");
        assert!(approved.created);
        assert_eq!(
            approved.principal.kind,
            gunnar_proto::PrincipalKind::Machine as i32
        );
        assert_eq!(approved.credential.principal_id, "kit-dwarf");
        assert_eq!(approved.credential.credential_id, kit_key().id);
        assert_eq!(
            plane
                .credential(b"kim-yubikey")
                .expect("read")
                .expect("there")
                .credential
                .sign_count,
            6,
            "the step-up's counter is kept"
        );
        let again = plane
            .approve_machine_enrolment(kit_approval())
            .expect_err("an id once");
        assert!(
            matches!(again, gunnar_grpc::ControlError::AlreadyExists(_)),
            "{again:?}"
        );
    }
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
    let plane = CoreControlPlane::new(Arc::clone(&stores));
    let machine = plane
        .principal("kit-dwarf")
        .expect("read")
        .expect("the machine is still there");
    assert_eq!(machine.kind, gunnar_proto::PrincipalKind::Machine as i32);
    assert!(plane
        .may(
            "kit-dwarf",
            &gunnar_grpc::Access::Server,
            gunnar_proto::GrantLevel::Admin
        )
        .expect("asked"));
    assert_eq!(
        plane.credentials(Some("kit-dwarf")).expect("listed").len(),
        1
    );
    assert!(plane
        .credentials(Some("kim"))
        .expect("listed")
        .iter()
        .all(|c| c.credential_id != kit_key().id));
    let plane = Arc::new(plane);
    let identity = core(dir.path(), &plane);
    let c = identity.login_start(&relay()).expect("a login");
    let session = identity
        .login_finish(kit_key().login(&c, "https://console.example", true))
        .expect("the machine logs in");
    assert_eq!(session.session.user_id, "kit-dwarf");

    person(&stores, "kit-person", gunnar_core::PrincipalKind::User);
    let refused = plane
        .approve_machine_enrolment(approval("kit-person", b"other-key", 4))
        .expect_err("a person under the id");
    assert!(
        matches!(refused, gunnar_grpc::ControlError::FailedPrecondition(_)),
        "{refused:?}"
    );
    assert!(plane.credential(b"other-key").expect("read").is_none());
}

/// ★ **An approved machine never touches the owner's first invite** (the
/// stage-L constraint, 2026-10-05: the break-glass token IS his first invite).
/// The invite stays pending under its hash, the feed renews it rather than
/// calling it spent, and the operator holds no credential.
#[test]
fn an_approved_machine_leaves_the_owners_first_invite_pending() {
    let dir = tempfile::tempdir().expect("tempdir");
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
    let plane = CoreControlPlane::new(Arc::clone(&stores));
    plane
        .claim_operator_named(Some("rickard"), "")
        .expect("the operator");
    let hash = gunnar_grpc::webauthn::sha256_hex(b"break-glass");
    assert_eq!(
        plane
            .operator_bootstrap_invite(
                "rickard",
                &hash,
                gunnar_core::now_ms(),
                gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS
            )
            .expect("recorded"),
        gunnar_grpc::BootstrapInvite::Recorded
    );
    plane
        .approve_machine_enrolment(approval("kit-fresh", b"kit-fresh-key", 5))
        .expect("approved");
    assert!(
        plane.first_invite().expect("read").is_some(),
        "the owner's first invite is still pending"
    );
    assert_eq!(
        plane
            .operator_bootstrap_invite(
                "rickard",
                &hash,
                gunnar_core::now_ms(),
                gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS
            )
            .expect("asked"),
        gunnar_grpc::BootstrapInvite::Renewed
    );
    assert!(
        plane
            .credentials(Some("rickard"))
            .expect("listed")
            .is_empty(),
        "nothing on the operator"
    );
}