crates/gunnar-grpc/tests/machine_enrolment.rs
//! **A machine asks; an admin approves with a fresh step-up; gunnar decides**
//! (2026-10-05: the ops kit's console credential without the owner's copied
//! session cookie — "WHY THIS WAY?? SOLVE IT", and an outside review called
//! the cookie hand-over an account-takeover pattern).
//!
//! The kit makes its own P-256 key, answers a creation ceremony with it and
//! asks for a role under a machine name (`kit-*`). The request is PENDING and
//! changes nothing until a PERSON who is a server admin approves it with a
//! fresh, user-verified assertion of their own passkey — verified here, by
//! gunnar — or the installer's break-glass token approves it while the
//! operator's first invite is pending (a fresh box). The touch is the identity
//! core's (IDENTITY-HOST): the approver's own core session, stepped up — the
//! call carries no assertion of its own. Then gunnar makes the
//! MACHINE principal, grants it the role and stores the credential, never on a
//! person. Requests expire, can be refused, and every step is audited.
//!
//! Every assertion is a real ES256 signature over the bytes a browser (or
//! the kit) sends; every refusal is also asserted on what did NOT happen.
mod machine_enrolment_common;
use gunnar_grpc::ControlPlane as _;
use gunnar_grpc::Identity as _;
use gunnar_proto::access_server::Access as _;
use gunnar_proto::*;
use machine_enrolment_common::*;
use tonic::Code;
/// ★ **A request is PENDING and changes nothing**: a code for the eye, an id
/// to poll with, listed to a server admin — and no principal, no grant, no
/// credential until somebody approves it.
#[tokio::test]
async fn a_request_is_pending_and_enrols_nothing_until_approved() {
let h = Harness::new();
let kit = Authenticator::new(9, b"kit-dwarf-key");
let asked = h.ask(&kit, "kit-dwarf", GrantLevel::Admin).await;
assert_eq!(state(&asked), MachineEnrolmentState::Pending);
assert_eq!(asked.machine, "kit-dwarf");
assert_eq!(asked.label, "installer kit on dwarf");
assert_eq!(asked.server_level, GrantLevel::Admin as i32);
assert_eq!(asked.request_id.len(), 32, "{asked:?}");
let code = asked.code.as_bytes();
assert!(
code.len() == 7
&& code[3] == b'-'
&& code[..3].iter().all(u8::is_ascii_uppercase)
&& code[4..].iter().all(u8::is_ascii_digit),
"a code is ABC-123: {:?}",
asked.code
);
assert!(asked.key_fingerprint.starts_with("SHA256:"), "{asked:?}");
assert!(
asked.expires_at_unix_ms > now_ms() + 14 * 60_000,
"fifteen minutes: {asked:?}"
);
let listed = h
.access()
.list_machine_enrolments(Harness::authed(&h.kim, ListMachineEnrolmentsRequest {}))
.await
.expect("a server admin lists the pending requests")
.into_inner()
.requests;
assert_eq!(listed.len(), 1);
assert_eq!(
(listed[0].request_id.as_str(), listed[0].code.as_str()),
(asked.request_id.as_str(), asked.code.as_str())
);
assert_eq!(
state(&h.status(&asked.request_id).await),
MachineEnrolmentState::Pending
);
h.nothing_for("kit-dwarf", &kit);
// A login with the pending credential is a stranger's.
let refused = kit
.login(&h.identity, 1)
.expect_err("a pending credential logs nobody in");
assert_eq!(refused.code, nornir_identity::Code::Ceremony, "{refused:?}");
}
/// ★ **An approval is a fresh step-up of the approver's own core session.**
/// No session at all (a signed key), a session that did not step up, or an
/// approval for another code: each refused, and nothing lands. (The touch
/// itself — user verification, a challenge the core issued, the approver's
/// own passkey — is the core's step-up, held by its conformance suite.)
#[tokio::test]
async fn approve_without_a_fresh_step_up_is_refused_and_enrols_nothing() {
let h = Harness::new();
let kit = Authenticator::new(9, b"kit-dwarf-key");
let asked = h.ask(&kit, "kit-dwarf", GrantLevel::Admin).await;
let none = h
.approve(&h.kim, &asked, None)
.await
.expect_err("no step-up");
assert_eq!(none.code(), Code::PermissionDenied, "{none:?}");
assert!(
none.message().contains("[stepup]"),
"the refusal names the step-up: {none:?}"
);
// kim's core session, logged in without user verification: no step-up.
let c = h.identity.login_start(&relay()).unwrap();
let challenge = gunnar_grpc::webauthn::from_b64url(&c.challenge).unwrap();
let plain = h
.identity
.login_finish(h.kim_key.assert(&c.id, &challenge, UP, 1))
.expect("kim logs in with a plain touch");
assert!(!plain.session.stepped_up);
let refused = h
.approve_with_session(&plain.token, &asked)
.await
.expect_err("a session that did not step up");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
assert!(refused.message().contains("[stepup]"), "{refused:?}");
let mut wrong = asked.clone();
wrong.code = "ZZZ-999".into();
let refused = h
.approve(&h.kim, &wrong, Some(h.kims_step_up().await))
.await
.expect_err("another code");
assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
h.nothing_for("kit-dwarf", &kit);
assert_eq!(
state(&h.status(&asked.request_id).await),
MachineEnrolmentState::Pending,
"still waiting"
);
// kim's user-verified login is a step-up: through the real gate it approves.
let fresh = kim_login(&h, 2);
h.approve_with_session(&fresh, &asked)
.await
.expect("kim's stepped-up session approves");
}
/// kim's user-verified login through the core: her session token.
fn kim_login(h: &Harness, counter: u32) -> String {
h.kim_key
.login(&h.identity, counter)
.expect("kim logs in")
.token
}
/// ★ **Approved, the credential is a MACHINE's**: gunnar makes `kit-dwarf`
/// (kind MACHINE), grants it server Admin, stores the credential there and
/// nowhere else — kim's own credential set is untouched — and the kit then
/// logs in as its machine, with its own key.
#[tokio::test]
async fn an_approval_makes_the_machine_and_its_credential_and_the_kit_logs_in_as_it() {
let h = Harness::new();
let kit = Authenticator::new(9, b"kit-dwarf-key");
let asked = h.ask(&kit, "kit-dwarf", GrantLevel::Admin).await;
let kims_before = h.plane.credentials(Some("kim")).expect("read").len();
let approved = h
.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect("kim approves with a fresh step-up");
let principal = approved.principal.expect("the machine");
assert_eq!(
(principal.id.as_str(), principal.kind),
("kit-dwarf", PrincipalKind::Machine as i32)
);
let credential = approved.credential.expect("the credential");
assert_eq!(
(
credential.principal_id.as_str(),
credential.credential_id.as_slice()
),
("kit-dwarf", kit.id.as_slice())
);
assert_eq!(credential.label, "installer kit on dwarf");
let request = approved.request.expect("the request");
assert_eq!(state(&request), MachineEnrolmentState::Approved);
assert_eq!(request.decided_by, "kim");
assert_eq!(
h.plane.credentials(Some("kim")).expect("read").len(),
kims_before,
"kim's own set is untouched"
);
assert!(
h.plane
.may(
"kit-dwarf",
&gunnar_grpc::auth::Access::Server,
GrantLevel::Admin
)
.expect("ask"),
"the machine holds server Admin"
);
assert_eq!(
state(&h.status(&asked.request_id).await),
MachineEnrolmentState::Approved
);
let listed = h
.access()
.list_machine_enrolments(Harness::authed(&h.kim, ListMachineEnrolmentsRequest {}))
.await
.expect("listed")
.into_inner()
.requests;
assert!(
listed.is_empty(),
"an approved request is pending no more: {listed:?}"
);
let login = kit
.login(&h.identity, 1)
.expect("the kit logs in with its own key through the core");
assert_eq!(login.session.user_id, "kit-dwarf");
let as_kit = h
.gate_session(&login.token)
.expect("the session authenticates");
assert_eq!(as_kit.principal_id, "kit-dwarf");
let who = h
.access()
.who_am_i(Harness::authed(&as_kit, WhoAmIRequest {}))
.await
.expect("who am I")
.into_inner();
assert_eq!(
who.server_level,
GrantLevel::Admin as i32,
"the kit's whoami: admin"
);
// Approved once: a second approval finds nothing pending.
let again = h
.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect_err("approved already");
assert_eq!(again.code(), Code::FailedPrecondition, "{again:?}");
// An operator's request is a server Write, on its own machine.
let ops = Authenticator::new(10, b"kit-ops-key");
let asked = h.ask(&ops, "kit-ops", GrantLevel::Write).await;
h.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect("approved");
assert!(h
.plane
.may(
"kit-ops",
&gunnar_grpc::auth::Access::Server,
GrantLevel::Write
)
.expect("ask"));
assert!(
!h.plane
.may(
"kit-ops",
&gunnar_grpc::auth::Access::Server,
GrantLevel::Admin
)
.expect("ask"),
"an operator is not an admin"
);
}
/// **A request expires**: after its time nobody approves it, and its machine
/// reads `expired`.
#[tokio::test]
async fn a_request_expires_and_is_approved_by_nobody() {
let h = Harness::with_ttl(300);
let kit = Authenticator::new(9, b"kit-dwarf-key");
let asked = h.ask(&kit, "kit-dwarf", GrantLevel::Admin).await;
std::thread::sleep(std::time::Duration::from_millis(400));
assert_eq!(
state(&h.status(&asked.request_id).await),
MachineEnrolmentState::Expired
);
let late = h
.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect_err("expired");
assert_eq!(late.code(), Code::FailedPrecondition, "{late:?}");
assert!(late.message().contains("expired"), "{late:?}");
h.nothing_for("kit-dwarf", &kit);
}
/// **A refused request enrols nothing**, and is approved by nobody after.
#[tokio::test]
async fn a_refused_request_enrols_nothing() {
let h = Harness::new();
let kit = Authenticator::new(9, b"kit-dwarf-key");
let asked = h.ask(&kit, "kit-dwarf", GrantLevel::Admin).await;
let refused = h
.access()
.refuse_machine_enrolment(Harness::authed(
&h.kim,
RefuseMachineEnrolmentRequest {
request_id: asked.request_id.clone(),
},
))
.await
.expect("kim refuses")
.into_inner()
.request
.expect("the request");
assert_eq!(
(state(&refused), refused.decided_by.as_str()),
(MachineEnrolmentState::Refused, "kim")
);
assert_eq!(
state(&h.status(&asked.request_id).await),
MachineEnrolmentState::Refused
);
let late = h
.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect_err("refused");
assert_eq!(late.code(), Code::FailedPrecondition, "{late:?}");
h.nothing_for("kit-dwarf", &kit);
}
/// ★ **Only a PERSON who is a server admin approves.** lee — a person with a
/// passkey and a perfect step-up, but no admin — is refused at every verb an
/// admin has; and a machine that holds Admin (an approved kit, stepping up
/// with its own key file) never approves another: the human is the point.
#[tokio::test]
async fn a_non_admin_and_a_machine_never_approve() {
let h = Harness::new();
let kit = Authenticator::new(9, b"kit-dwarf-key");
let asked = h.ask(&kit, "kit-dwarf", GrantLevel::Admin).await;
let lees = h
.lee_key
.login(&h.identity, 1)
.expect("lee logs in, user-verified")
.token;
let refused = h
.approve_with_session(&lees, &asked)
.await
.expect_err("lee is no admin");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
let refused = h
.access()
.list_machine_enrolments(Harness::authed(&h.lee, ListMachineEnrolmentsRequest {}))
.await
.expect_err("lee lists nothing");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
let refused = h
.access()
.refuse_machine_enrolment(Harness::authed(
&h.lee,
RefuseMachineEnrolmentRequest {
request_id: asked.request_id.clone(),
},
))
.await
.expect_err("lee refuses nothing");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
// And asking is the relay's: lee cannot ask on a machine's behalf.
let refused = h
.ask_as(
&h.lee,
&Authenticator::new(11, b"other"),
"kit-other",
GrantLevel::Admin,
)
.await
.expect_err("lee is no relay");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
h.nothing_for("kit-dwarf", &kit);
// kim approves the first kit; the kit, an admin machine, tries the next.
h.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect("approved");
let kits = kit.login(&h.identity, 1).expect("the kit logs in").token;
let second = Authenticator::new(12, b"kit-second-key");
let next = h.ask(&second, "kit-second", GrantLevel::Admin).await;
let refused = h
.approve_with_session(&kits, &next)
.await
.expect_err("a machine never approves");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
assert!(refused.message().contains("person"), "{refused:?}");
h.nothing_for("kit-second", &second);
}
/// ★ **On a fresh box the OWNER registers first — with the break-glass
/// token, his first invite — and then approves the kit** (2026-10-05, the
/// stage-L constraint: the token IS the owner's first invite, and an invite
/// never makes an admin, so a kit that spent it would leave the owner no way
/// to admin). Before he holds a passkey nothing approves the kit's request —
/// not the token, which no approval carries — and his first invite stays
/// pending; he registers with it; it is spent; his step-up approves the kit.
#[tokio::test]
async fn the_owner_registers_first_with_the_token_then_approves_the_kit() {
let h = Harness::new();
let operator = h
.plane
.claim_operator("rickard")
.expect("the operator")
.principal;
let hash = gunnar_grpc::webauthn::sha256_hex(TOKEN.as_bytes());
h.plane
.operator_bootstrap_invite(
&operator.id,
&hash,
now_ms(),
gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS,
)
.expect("the first invite is recorded");
let first_invite_pending = |h: &Harness| h.plane.first_invite().expect("read").is_some();
// A kit asks on the fresh box: nothing approves it without a person's touch.
let kit = Authenticator::new(9, b"kit-fresh-key");
let asked = h.ask(&kit, "kit-fresh", GrantLevel::Admin).await;
// The console's own key (what the break-glass bearer acts with) is no
// person: the token approves nothing, it is the owner's first invite.
let refused = h
.approve(&h.console, &asked, None)
.await
.expect_err("no person, no approval");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
assert!(
refused.message().contains("not a person"),
"the refusal says why the token is no approval: {refused:?}"
);
h.nothing_for("kit-fresh", &kit);
assert!(
first_invite_pending(&h),
"the owner's first invite is untouched"
);
// The owner registers his YubiKey with the token — his first invite —
// through the core, which reads the token's hash as a fed invite onto him.
let mut yubikey = nornir_identity::test_host::SoftPasskey::new(20);
let c = h
.identity
.redeem_start(TOKEN, "", &relay())
.expect("the token opens his ceremony");
assert_eq!(c.user_id, operator.id, "onto the operator");
let owner = h
.identity
.redeem_finish(yubikey.register(&c, ORIGIN))
.expect("the token registers the owner's first passkey");
assert_eq!(owner.session.user_id, operator.id);
assert!(
!first_invite_pending(&h),
"the first invite was redeemed by the OWNER"
);
assert_eq!(
h.plane
.operator_bootstrap_invite(
&operator.id,
&hash,
now_ms(),
gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS
)
.expect("asked"),
gunnar_grpc::BootstrapInvite::Spent
);
// He logs in, user-verified, and approves the kit from that session.
let c = h.identity.login_start(&relay()).unwrap();
let session = h
.identity
.login_finish(yubikey.login(&c, ORIGIN, true))
.expect("the owner logs in")
.token;
let approved = h
.approve_with_session(&session, &asked)
.await
.expect("the owner approves the kit");
assert_eq!(
approved.request.map(|r| r.decided_by),
Some(operator.id.clone())
);
assert_eq!(
approved.principal.map(|p| (p.id, p.kind)),
Some(("kit-fresh".to_owned(), PrincipalKind::Machine as i32))
);
let held: Vec<Vec<u8>> = h
.plane
.credentials(Some(&operator.id))
.expect("read")
.into_iter()
.map(|c| c.credential_id)
.collect();
assert_eq!(
held,
vec![yubikey.id.clone()],
"the owner's set holds his key only"
);
}
/// **A request is checked like a registration**: a machine name (never a
/// person's or a console's), ADMIN, WRITE or READ, a creation ceremony over a
/// challenge gunnar issued, at the relay's origin. And an invite still never
/// makes an admin, nor lands on a machine: the approval is the one door.
#[tokio::test]
async fn a_request_is_checked_and_the_approval_is_the_only_door() {
let h = Harness::new();
let kit = Authenticator::new(9, b"kit-dwarf-key");
for (machine, level) in [
("rickard", GrantLevel::Admin),
("console-x", GrantLevel::Admin),
("kit-", GrantLevel::Admin),
("kit-Dwarf", GrantLevel::Admin),
] {
let refused = h
.ask_as(&h.console, &kit, machine, level)
.await
.expect_err(machine);
assert_eq!(
refused.code(),
Code::InvalidArgument,
"{machine} {level:?}: {refused:?}"
);
}
// READ and no role at all are asked for now (2026-10-18: no role is the
// lowest, READ — `a_request_that_names_no_role_asks_the_lowest_…`); a
// level gunnar does not know is still refused.
let unknown = h
.ask_level(&kit, "kit-dwarf", 99)
.await
.expect_err("an unknown level");
assert_eq!(unknown.code(), Code::InvalidArgument, "{unknown:?}");
let base = |challenge: Vec<u8>, kind: &str, origin: &str| RequestMachineEnrolmentRequest {
machine: "kit-dwarf".into(),
label: "kit".into(),
server_level: GrantLevel::Admin as i32,
credential_id: kit.id.clone(),
public_key: kit.public(),
client_data_json: created(&challenge, kind, origin),
rp_id: RP_ID.into(),
origin: ORIGIN.into(),
sign_count: 0,
attestation_object: gunnar_grpc::webauthn::attestation_none(
RP_ID,
&kit.id,
&kit.public(),
0,
),
};
for (what, body) in [
(
"a login, not a creation",
base(h.challenge().await, "webauthn.get", ORIGIN),
),
(
"another origin",
base(
h.challenge().await,
"webauthn.create",
"https://evil.example",
),
),
(
"a challenge gunnar never issued",
base(vec![3; 32], "webauthn.create", ORIGIN),
),
// Stinky row 13 (2026-10-10): the relayed key must be the attested one.
(
"no attestation object",
RequestMachineEnrolmentRequest {
attestation_object: Vec::new(),
..base(h.challenge().await, "webauthn.create", ORIGIN)
},
),
(
"an attestation naming another key",
RequestMachineEnrolmentRequest {
attestation_object: gunnar_grpc::webauthn::attestation_none(
RP_ID,
&kit.id,
&Authenticator::new(66, b"x").public(),
0,
),
..base(h.challenge().await, "webauthn.create", ORIGIN)
},
),
] {
let refused = h
.access()
.request_machine_enrolment(Harness::authed(&h.console, body))
.await
.expect_err(what);
assert_eq!(
refused.code(),
Code::PermissionDenied,
"{what}: {refused:?}"
);
}
assert!(h
.access()
.list_machine_enrolments(Harness::authed(&h.kim, ListMachineEnrolmentsRequest {}))
.await
.expect("listed")
.into_inner()
.requests
.is_empty());
// A person may not carry a machine's name.
let refused = h
.access()
.add_principal(Harness::authed(
&h.kim,
AddPrincipalRequest {
name: "kit-dwarf".into(),
kind: PrincipalKind::User as i32,
id: String::new(),
},
))
.await
.expect_err("`kit-*` is a machine's");
assert_eq!(refused.code(), Code::InvalidArgument, "{refused:?}");
// Approved, the machine holds its credential; the other doors stay shut.
let asked = h.ask(&kit, "kit-dwarf", GrantLevel::Admin).await;
h.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect("approved");
// An invite never makes an admin (the core's rule: `Code::InviteWouldBeAdmin`,
// `invites.rs`); a passkey lands on a machine only by approval:
let other = Authenticator::new(13, b"sneaked-in");
let refused = h
.access()
.add_credential(Harness::authed(
&h.kim,
AddCredentialRequest {
principal_id: "kit-dwarf".into(),
credential_id: other.id.clone(),
public_key: other.public(),
label: "x".into(),
sign_count: 0,
},
))
.await
.expect_err("no credential lands on a machine but by approval");
assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
}
/// **An admin lists the kit's credential and revokes it**; its sessions end
/// and the kit no longer logs in.
#[tokio::test]
async fn an_admin_lists_and_revokes_the_kits_credential() {
let h = Harness::new();
let kit = Authenticator::new(9, b"kit-dwarf-key");
let asked = h.ask(&kit, "kit-dwarf", GrantLevel::Admin).await;
h.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect("approved");
let session = kit.login(&h.identity, 1).expect("the kit logs in").token;
let listed = h
.access()
.list_credentials(Harness::authed(
&h.kim,
ListCredentialsRequest {
principal_id: String::new(),
},
))
.await
.expect("kim lists every credential")
.into_inner()
.credentials;
let row = listed
.iter()
.find(|c| c.credential_id == kit.id)
.expect("the kit's credential is listed");
assert_eq!(
(row.principal_id.as_str(), row.label.as_str()),
("kit-dwarf", "installer kit on dwarf")
);
h.access()
.revoke_credential(Harness::authed(
&h.kim,
RevokeCredentialRequest {
credential_id: kit.id.clone(),
},
))
.await
.expect("kim revokes it");
assert!(
h.gate_session(&session).is_err(),
"the kit's session ended with its credential"
);
kit.login(&h.identity, 2)
.expect_err("revoked: the kit logs in no more");
}
/// ★ **A request that names no role asks for the LOWEST, and the admin grants
/// more on approval** (owner order 2026-10-18). `/auth/machine/enrol` is a
/// door anybody can knock on; what a knock asks for by default must be the
/// least a machine can hold — a server READ — never Admin. The approving
/// admin, touching their own key, decides what the machine becomes: the role
/// asked, or the one they give.
#[tokio::test]
async fn a_request_that_names_no_role_asks_the_lowest_and_the_admin_grants_more() {
let h = Harness::new();
let kit = Authenticator::new(9, b"kit-quiet-key");
let asked = h.ask(&kit, "kit-quiet", GrantLevel::Unspecified).await;
assert_eq!(
asked.server_level,
GrantLevel::Read as i32,
"no role asked is the lowest: {asked:?}"
);
h.approve(&h.kim, &asked, Some(h.kims_step_up().await))
.await
.expect("approved as asked");
let server = gunnar_grpc::auth::Access::Server;
assert!(
h.plane
.may("kit-quiet", &server, GrantLevel::Read)
.expect("ask"),
"the machine reads"
);
assert!(
!h.plane
.may("kit-quiet", &server, GrantLevel::Write)
.expect("ask"),
"and nothing more"
);
// The admin gives more than was asked, on the approval itself.
let ops = Authenticator::new(10, b"kit-raised-key");
let asked = h.ask(&ops, "kit-raised", GrantLevel::Read).await;
h.approve_at(
&h.kim,
&asked,
Some(h.kims_step_up().await),
GrantLevel::Write,
)
.await
.expect("the admin grants Write");
assert!(
h.plane
.may("kit-raised", &server, GrantLevel::Write)
.expect("ask"),
"granted Write on approval"
);
assert!(
!h.plane
.may("kit-raised", &server, GrantLevel::Admin)
.expect("ask"),
"not Admin"
);
}