ROOTVIELROOTVIEL
← Technology

Technology

Implemented12 min

The signed order protocol

The control plane never executes — it mints signed authority. A look at the self-sufficient, Ed25519-signed envelope that authorizes an assessment, how it is canonicalized, how it expires, and the fail-closed gate that checks it.

Minting authority, not executing

The control plane does not run anything. What it does is mint signed authority: it places on an event bus a message that says, in effect, "this assessment is authorized, under these rules, until this moment". The execution engine verifies that message and only then works. There are exactly two points where authority is signed: the request, and the human approval.

Note

The engine never calls back to the control plane. The order is self-sufficient — it carries a frozen snapshot of everything the engine needs, so the decision cannot drift after it is signed.

The signed envelope

An authority event travels in a three-part envelope: a protected header, the canonical event, and a signature.

SignedEnvelope
SignedEnvelope ├── header { alg: "EdDSA", typ: "rootveil-authority-event", kid: <key id> } ├── event AuthorityEvent { ...canonical signed content... } └── signature base64( Ed25519_sign(signing_input) )
  • ▸alg is always EdDSA — never HMAC, never none. The verifier rejects anything else.
  • ▸kid identifies the issuer’s public key, which is what lets keys rotate without downtime.
  • ▸typ is fixed. The event itself carries event_id, event_type, issuer, audience, purpose, tenant, assessment, versions, issued/expires, a correlation id, and a reference-only string map as payload — never secrets.

Canonicalization: signing exact bytes

The signature covers exact bytes, so serialization must be deterministic — a JSON Canonicalization Scheme: sorted keys, no insignificant whitespace, UTF-8.

deterministic serialization
canonical(x) = json(x, sort_keys=True, separators=(",",":"), ensure_ascii=False) signing_input = canonical(header) + "." + canonical(event) signature = base64( Ed25519_sign(privateKey_in_KMS, signing_input) )
Verified

A detail that breaks signatures if omitted: non-ASCII characters are emitted as themselves, not escaped. Any non-ASCII byte in a target reference or a tenant name changes the bytes, and the engine rejects the signature — fail-closed, by design.

A self-sufficient request

The request payload carries an execution plan, the family code, and a signed execution context — a snapshot of the connection and the authorization grant, by value. Because the whole snapshot is inside the signed bytes, the engine never has to ask the control plane anything; the decision is frozen.

  • ▸The plan names a target by reference — a repository or an endpoint — never a credential. The secret is a provider plus a reference, never the material.
  • ▸The family code is re-validated against the catalog before signing: a family that is experimental or planned is refused as "not available to run". The front end is never trusted to have filtered.
  • ▸The two dangerous opt-ins — act actively, and touch production — default to false and must be turned on by a human.

The photo never outlives the permit

A signature does not expire; the snapshot does. The event and the snapshot carry two separate windows. The event has a time-to-live; the snapshot expires at the earlier of its own time-to-live and the authorization grant’s end. So a frozen context can never outlive the permission it portrays, and the engine revalidates both when it dispatches — verifying the signature is not enough.

Authority binding: one signature, one purpose

Every event is bound to an audience and a purpose, so a signature minted for one channel cannot be replayed as another.

EventaudiencepurposeSigned by
assessment.requestedschedulerrequestthe control plane
assessment.approvedworkerexecutionthe control plane (human approval)
assessment.executeworkerexecutionthe engine’s dispatcher — never the control plane

Because a request is bound to the scheduler for the purpose "request", it can never be reused as an execution order to the worker. The approval and the execution order share a binding; what separates them is the event type, and the engine rejects an order that arrives on the approval channel as "not an approval".

The cryptographic gate

Before executing anything, the engine validates the event in a fixed order. Any failure rejects the event and nothing runs:

  1. 1Schema is valid.
  2. 2The algorithm is on the allow-list (never none, never the one the message chose).
  3. 3The issuer and key id are in the trust store.
  4. 4The signature verifies.
  5. 5The signer is neither revoked nor expired.
  6. 6The audience matches.
  7. 7The purpose matches.
  8. 8The event is within its freshness window — and is not a replay of an event id already seen.
Verified

The event id is claimed atomically — an insert-on-conflict, race-safe — only after every check passes. On the verified event the caller then applies business checks: tenant, assessment state, and Rules of Engagement. A valid but unschedulable plan is not rejected; it is recorded as unschedulable, and the engine never invents a slot for it (nor does the interface paint it as "clean"). Keys rotate without downtime: active, then verify-only, then revoked.

Outbox, relay and consumer

Three separate processes, each with the minimum privilege it needs and no more.

Transactional outbox
The signed envelope is written in the same transaction as the state change, so the database and the bus can never diverge: never published without being recorded, never lost, never duplicated.
Relay
A separate process with no signing-key access and its own database role. It drains the queue with a skip-locked select (so it scales to many replicas), publishes, then marks — at-least-once on purpose, with producer idempotency — and verifies the signature before publishing, so a tampered envelope is never retried.
Consumer
A third process with no outbox and no signing-key access. It verifies the engine’s signature against its own trust store — trusting neither the channel nor any producer — and sends anything it cannot verify, understand or apply to a dead-letter queue with a reason, while the batch continues.
Note

Only references and numbers travel the bus, never bodies: a report reference says where the report lives, not the report. A null summary means "the engine has not reported yet" — not zero.

The state machine

assessment lifecycle
DRAFT → REQUESTED → SCHEDULED → RUNNING → HITL_PENDING → COMPLETED | FAILED └─ also: UNVERIFIABLE · UNSCHEDULABLE · CANCELLED · REJECTED

The worker emits only three signed lifecycle events — started, awaiting-approval, failed. The step to a non-terminal RUNNING is bridged over SCHEDULED, which no event asserts, and the bridge is marked "deduced" in the audit. Crucially, COMPLETED is not reachable from the bus: it is minted by the control plane with a human behind it, because if the consumer accepted it as an inbound event, any producer on the topic could close an unreviewed assessment as done.

Human approval is the second and last point where authority is signed. It is bound to the report digest the engine computed, so if the analysis is re-run the digest changes and the approval no longer describes what would be delivered — and the use case refuses to sign without a digest. Segregation of duties still holds: whoever requested the assessment cannot approve it.

References

  1. [1]RFC 8032 — Ed25519 (EdDSA)
  2. [2]RFC 8785 — JSON Canonicalization Scheme
  3. [3]RFC 8725 — JWT Best Current Practices (algorithm confusion)
  4. [4]The transactional outbox pattern; Kafka at-least-once delivery

References point to public, well-known technical sources for further study.

Related articles