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.
Re-arm flow
Section titled “Re-arm flow”- Replace the mesh cartridge on the box.
- 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=…. - The organization Owner opens that page and seals. The seal is bound to the organization, the box ID and the nonce.
- Axowl returns the ticket. Enter it on the box with
rearm_client.py apply --ticket <ticket>within 10 minutes. - 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.
Endpoints
Section titled “Endpoints”| Method · Route | Purpose | Source |
|---|---|---|
POST /api/org/{slug}/roost/rearm-ticket | Issue a re-arm ticket. Body { box, nonce, stepUpAssertion }. Owner only. | RoostEndpoints.cs:28 |
GET /api/roost/rearm-public-key | Public key the demo box pins: { alg: "ES256", publicKeyPem, fingerprint }. Anonymous. | RoostEndpoints.cs:100 |
GET /api/roost/spec-sheet | Roost specification sheet, application/pdf (attachment Axowl-Roost-Specification.pdf). Any signed-in user; 401 otherwise. | RoostEndpoints.cs |
POST /api/org/{slug}/roost/rearm-ticket
Section titled “POST /api/org/{slug}/roost/rearm-ticket”box— 4 to 40 characters ofA–Z,0–9and-, exactly as the box prints it.nonce— 32 lowercase hex characters, exactly as the box prints it.stepUpAssertion— a passkey step-up with purposeroost.rearmand subjectStepUpPurpose.RoostRearmSubject(slug, box, nonce)(Axowl.Core/Constants/Auth/StepUpPurpose.cs). Request the challenge fromPOST /api/passkey/stepup/optionswith 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:…"}| Status | error | When |
|---|---|---|
| 400 | INVALID_REARM_REQUEST | box or nonce is malformed |
| 401 | STEPUP_REQUIRED · STEPUP_FAILED | No seal, a seal from another user, or a seal bound to another box or nonce |
| 403 | OWNER_REQUIRED | The caller is a member but not the Owner |
| 503 | ROOST_REARM_DISABLED | The 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 format
Section titled “Ticket format”ticket = base64url(payload) "." base64url(signature) ; no paddingpayload = 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)byis the Owner’s user ID.iatandexpare Unix seconds;expis 10 minutes afteriat.- 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,
typisroost-rearm,boxis its own ID andnonceis 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.