Skip to content

Roost

A Roost box watches a sensing mesh around its signing boards. When the mesh is cut, a feedthrough is disturbed or the lid is opened, the box erases the keys that unlock its signing keys. Production boxes stay erased and are reinstalled at the factory.

A demo box can be brought back into service on site. The owner of the organization confirms the re-arm with a passkey seal, and Axowl returns a re-arm ticket that only that box, for that one attempt, accepts.

  1. Replace the mesh cartridge on the box.
  2. On the box controller run rearm_client.py nonce. The box prints its box ID and a one-time number (nonce), plus a link to /dashboard/{slug}/roost-rearm?box=…&nonce=….
  3. The organization Owner opens that page and seals. The seal is bound to the organization, the box ID and the nonce.
  4. Axowl returns the ticket. Enter it on the box with rearm_client.py apply --ticket <ticket> within 10 minutes.
  5. The box checks the ticket signature, its own box ID and the nonce, then creates new keys. Each signing board enrolls again and the new signers are registered.
Method · RoutePurposeSource
POST /api/org/{slug}/roost/rearm-ticketIssue a re-arm ticket. Body { box, nonce, stepUpAssertion }. Owner only.RoostEndpoints.cs:28
GET /api/roost/rearm-public-keyPublic key the demo box pins: { alg: "ES256", publicKeyPem, fingerprint }. Anonymous.RoostEndpoints.cs:100
GET /api/roost/spec-sheetRoost specification sheet, application/pdf (attachment Axowl-Roost-Specification.pdf). Any signed-in user; 401 otherwise.RoostEndpoints.cs
  • box — 4 to 40 characters of A–Z, 0–9 and -, exactly as the box prints it.
  • nonce — 32 lowercase hex characters, exactly as the box prints it.
  • stepUpAssertion — a passkey step-up with purpose roost.rearm and subject StepUpPurpose.RoostRearmSubject(slug, box, nonce) (Axowl.Core/Constants/Auth/StepUpPurpose.cs). Request the challenge from POST /api/passkey/stepup/options with that purpose and subject. A seal made for another box or another nonce is rejected.

Response 200:

{
"ticket": "eyJ2IjoxLCJ0eXAiOi....Qc2NMSUK...",
"box": "ROOST-0001",
"nonce": "404142434445464748494a4b4c4d4e4f",
"expiresAt": "2026-10-18T09:10:00Z",
"keyFingerprint": "SHA256:…"
}
StatuserrorWhen
400INVALID_REARM_REQUESTbox or nonce is malformed
401STEPUP_REQUIRED · STEPUP_FAILEDNo seal, a seal from another user, or a seal bound to another box or nonce
403OWNER_REQUIREDThe caller is a member but not the Owner
503ROOST_REARM_DISABLEDThe server has no re-arm signing key

Every issued ticket is recorded as the audit event roost.rearm.ticket_issued with the box ID, nonce, expiry, key fingerprint and the seal proof (see Audit).

ticket = base64url(payload) "." base64url(signature) ; no padding
payload = UTF-8 JSON {"v":1,"typ":"roost-rearm","box","nonce","org","by","iat","exp"}
signature = ECDSA P-256 over the payload bytes with SHA-256, as r‖s (64 bytes, IEEE P1363)
  • by is the Owner’s user ID. iat and exp are Unix seconds; exp is 10 minutes after iat.
  • Verify the signature over the payload bytes as received; do not re-serialize the JSON.
  • The box accepts a ticket only when the signature verifies with its pinned key, typ is roost-rearm, box is its own ID and nonce is the number it issued for this attempt. A nonce works once.

The same envelope (base64url payload, P-256 r‖s signature) is used by Roost for signed boot images.