ROOSTby Axowl

Docs

Binding and replacement

Every write needs the organization Owner and a passkey seal bound to the exact action (403 OWNER_REQUIRED for other members; 401 STEPUP_REQUIRED / STEPUP_FAILED when the seal is missing or bound to something else). Request the challenge from POST /api/passkey/stepup/options with the purpose and subject shown. Every write is recorded as an audit event; the ledger rows themselves are not sealed, the events are.

Method · RoutePurpose · subjectEffect · audit event
POST /api/org/{slug}/roost/bind/challenge— (no seal; Owner)Verifies productCert against the hash recorded at the factory; returns { serial, nonce, expiresAt, payloadToSign, attestationKeyRegistered }. Only a box Axowl shipped to this organization can be bound. 403 BIND_REJECTED for every refusal — unknown serial, wrong number, a box shipped to another organization or not shipped yet, or a box already bound; the answer never says which. 429 after 10 attempts in 10 minutes
POST /api/org/{slug}/roost/bindroost.bind · RoostBindSubject(slug, serial, nonce)Verifies boxSignature (the box signs payloadToSign with its attestation key), moves the box to Enrolling, starts an enrollment ceremony. roost.device.bound, roost.device.lifecycle_changed
POST /api/org/{slug}/roost/pairsroost.pair · RoostPairSubject(slug, "new", "create", serialA+serialB)Creates the pair. roost.pair.registered
POST /api/org/{slug}/roost/pairs/{id}/replacementroost.pair · RoostPairSubject(slug, pairId, action, serial)One replacement step in order: degraded → stage → register → verify-envelope → remove → resealed. roost.pair.replacement_step (+ lifecycle and envelope events)
POST /api/org/{slug}/roost/pairs/{id}/envelopesroost.pair · RoostPairSubject(slug, pairId, "envelope", serial+":"+fingerprint)Records a DB key envelope (Issued). roost.envelope.issued
POST /api/org/{slug}/roost/envelopes/{id}/verifyroost.pair · RoostPairSubject(slug, pairId, "verify", envelopeId)Verified. roost.envelope.issued with verified: true
POST /api/org/{slug}/roost/envelopes/{id}/revokeroost.pair · RoostPairSubject(slug, pairId, "revoke", envelopeId)Revoked. roost.envelope.revoked
POST /api/org/{slug}/roost/enrollments/{id}/advanceroost.enrollment · RoostEnrollmentSubject(slug, serial, enrollmentId) (last step only)ArrivalCheck {transportSealOk, shockIndicatorOk} → MeasurementCompare {actualMeasurements} (compared per target with the factory record; mismatch fails) → QuorumRegistration {keyHolders, fidoKeys, axowlTicketRef, stepUpAssertion} → Completed, box Active. roost.enrollment.completed, roost.device.lifecycle_changed

Binding, step by step

  1. Axowl ships the box to the organization that bought it. Only that organization can bind it, and its Roost menu shows Connect a box until the box is bound.
  2. The Owner enters the serial and the product certificate number from the card shipped with the box → /bind/challenge returns a one-time number and payloadToSign (base64url of {"v":1,"typ":"roost-bind","serial","nonce","org"}).
  3. On the box controller: bind_client.py sign <payloadToSign> — the box signs the bytes with its attestation key (ECDSA P-256 / SHA-256, r‖s, base64url). The attestation public key was recorded at the factory.
  4. The Owner pastes the signature and seals → /bind. The seal subject carries the serial and the one-time number, so a seal cannot be reused for another box or another attempt. The number works once and expires after 10 minutes.

A certificate number alone cannot bind a box (a leaked card photo is not enough), and a box signature alone cannot either (the Owner must seal).

Replacement

A failed box is never opened on site. The pair runs 2-of-3 (degraded), a new box is bound and staged as a transient third (stage), the customer and Axowl register it together — three physical keys, Owner quorum seal, Axowl ticket (register, axowlTicketRef required) — the new box's envelope is verified against the pair (verify-envelope), the old box is removed, set to RmaReturned, its envelopes revoked and a Replacement case moved to FactoryReseal (remove), and finally resealed closes the case. Steps out of order are refused with 409 REPLACEMENT_STEP_OUT_OF_ORDER.