PETRA 1.0 Zenoh and DDIL data plane¶
This page is the normative PETRA 1.0 contract for operating the tactical data plane through multi-day disconnection. It applies the Zenoh abstractions to PETRA: live delivery is publish/subscribe, and delayed delivery is a Storage (subscriber plus queryable) behind PETRA's validation boundary.
Coordinated release boundary
This is the only PETRA 1.0 data-plane contract. The component hard-cut changes must land at one Common revision and deploy together before traffic resumes. Earlier key shapes, auth queryables, unsigned publication paths, retained readers, and protocol fallbacks are invalid; they are not an interoperability profile.
Static roles and authority¶
Trust and data-plane roles are separate. A privileged Core process can still act as a Zenoh endpoint; a capture-assumed Node can be explicitly provisioned to co-host storage.
| Role | Members | May do | Must not do |
|---|---|---|---|
| Core authority | Directory | Issue identity, policy, publish/query capabilities, revocations, and snapshot authority | Delegate minting authority to Relay + Storage |
| Core application | Web | Publish, subscribe, query, retain authoritative history/audit, and create snapshots under Directory authority | Re-sign an endpoint event or present a snapshot as endpoint-authored traffic |
| Endpoint | Android, ordinary Node, Gateway, and Web's data-plane clients | Publish, subscribe, and query within Directory-issued capability | Mint authority or assume a storage role dynamically |
| Relay + Storage | Server and specifically provisioned Nodes | Route traffic; validate, retain, and return opaque values | Decode application content or infer/enforce membership, ownership, ORBAT, drawing, or plan semantics |
Every fully capable tactical partition has at least one statically provisioned Relay + Storage role. Additional copies and endpoint locators are configured explicitly. PETRA 1.0 has no leader election, consensus, automatic role promotion, or dynamic discovery of storage authority. Automatic reconnect across that static locator list is permitted.
Android router locator contract¶
First-party Android clients receive one ordered, device-scoped MDM locator list for the runout or deployment. The list is the complete transport seed set; it is not an authority grant and it never bypasses ServerToken verification or the per-session TLS trust bundle. For the Infrastructure #125 two-relay fixture the order is fixed as:
- primary
quic/host:port; - primary
tls/host:port; - alternate
quic/host:port; - alternate
tls/host:port.
The client preserves this order on cold start and reconnect. It must not require a user to edit application settings when the primary stops, and it must retain enough device-scoped locator configuration to try the alternate after a cold restart while Directory is unreachable. There is no singular legacy bootstrap fallback in the PETRA 1.0 runout contract.
A verified, unexpired Directory ServerToken may validate or annotate a configured
locator, including its hostname, port, and coverage cells. A token received at runtime
does not add a new route, reorder the configured list, or promote an unconfigured router.
An unknown, expired, revoked, or otherwise unverified router remains unusable. This keeps
transport discovery separate from Directory authority and makes primary-loss behavior
deterministic and operator-understandable.
The runout fixture is deliberately non-HA: co-located Relay + Storage processes have independent identities and state, but there is no election, state replication, promotion, or physical fault isolation. If both configured paths are unavailable, the client reports degraded connectivity and uses only existing cached authority and bounded durable outbox/replay behavior. It does not claim lossless failover or extend authority beyond the signed offline horizon. See the Infrastructure #125 runout contract and docs #125 for the operator procedure and evidence matrix.
Directory remains the only authority even while it is unreachable. Cached, unexpired Directory-signed material lets a partition continue within its configured horizon; an isolated Relay + Storage verifies that authority but never extends or replaces it.
Publish, query, and channel-membership capabilities are exact Directory-issued grants, not endpoint assertions. An endpoint may cache them for no longer than the configured offline horizon. While Directory is unreachable, an endpoint may continue normal shared operation only for channels whose current capabilities were already cached and remain valid. Expiry fails closed.
Creating or activating a shared channel; inviting or joining; kicking, removing, or
authoritatively leaving; transferring ownership; retiring or deleting; and every other
membership or ownership change require Directory connectivity. An edge endpoint may
create an offline local draft, but that draft is not a channel authority fact: it is
visibly marked unactivated, remains local to that endpoint, cannot publish or query
shared traffic, and cannot appear successfully activated. The API returns the stable code
DIRECTORY_CONNECTIVITY_REQUIRED; it neither queues the authority change nor claims
completion. The user may retry after reconnecting. A user may locally mute a channel, stop using it,
or discard cached capability, but that local action does not claim a shared membership or
lifecycle change.
On Directory/Core reconnection, an endpoint refreshes trusted Directory keys and the current revocation state, fetches the committed authority ledger and exact capabilities, validates them, atomically replaces its capability cache (removing every absent grant), re-gates pending content, and only then resumes shared operation or drains authorized content outboxes. A removed member, revoked subject, expired capability, or changed ownership therefore fails closed even if older application state remains cached. A Relay-only reconnect while Core remains unreachable uses cached unexpired authority; it does not perform this control-plane refresh and cannot mutate authority. PETRA does not support endpoint-issued or owner-delegated sub-capabilities. Any future exception requires a new explicit product and threat-model decision; it cannot arise as an implementation extension.
During Core disconnection, cached authority remains usable until its signed effective deadline unless the partition already knows that the subject or signing key is revoked. A membership or capability removal that occurred beyond the partition cannot take effect there before refreshed authority arrives. This bounded malicious-holder window is an explicit residual risk, not an assertion that disconnected revocation is instantaneous. On Core reconnect, endpoints refresh revocation and atomically replace the complete capability set before shared operation or outbox drainage; omission from that signed set removes the cached grant even when its earlier expiry has not arrived.
Delivery and validation¶
Live samples use Zenoh put and pub/sub. A reconnecting endpoint issues get against
queryables. A Relay + Storage subscribes to authorized durable families, validates each
sample, retains the opaque bytes, and answers authorized queries. Non-retained live
samples pass through the native Relay without becoming Storage values.
The validation boundary fails closed in this order:
- Parse the request without interpreting application content. Reject an oversized value, malformed or non-canonical key expression, unknown or duplicate selector parameter, and any selector that widens the authorized expression.
- Verify the Directory signature and validity of the publish or query capability. Bind it to the subject principal and device signing key, action, canonical opaque expression, allowed envelope purpose, classification/audience ceiling, and validity interval.
- Verify a fresh
StorageUseProofV1by that subject over the capability digest, exact canonical publication key or query selector, value digest for a publication, nonce, and timestamp. A reusable capability is not a bearer token; stolen capability bytes alone are inert. - Verify the outer envelope signature and purpose, freshness policy, key/envelope binding, classification, audience, and current revocation state before accepting a publication or returning a stored value.
- Persist or return the original opaque bytes. Never decode membership, ownership, ORBAT, drawings, plans, chat, or other sealed application semantics.
For a non-retained live sample, the publisher performs the same capability, proof, classification, and key-binding checks as a preflight, but that preflight is not the authorization boundary. Every receiving subscriber authoritatively verifies the envelope identity/signature, capability and typed containment, proof freshness/replay, exact key digest, purpose/classification, known revocation, and applicable channel or command policy before decoding the payload.
The native Zenoh Relay remains default-deny at its peer, action, and key-expression ACL.
It cannot inspect an AuthEnvelope, capability, or use proof and therefore makes no claim
to capability or payload verification for non-retained live traffic. PETRA 1.0 does not
add a Relay plugin, payload interception, or intercept-and-republish path. A Relay ACL is
defence in depth; it is not a substitute for authoritative subscriber verification.
No Server-rewrapped voice path or application-plane ACL exception exists in the hard-cut
runtime. Field clients use the one registered wildcard transport subject because stock
Zenoh cannot dynamically update and revoke per-device ACL subjects. That coarse subject is
limited to the exact closed action/key registry; the ACL remains deny by default for every
unregistered action or key expression.
Production Server data-plane listeners accept only tls/ and quic/ locators. Browser
transport reaches the remote-api bridge through HTTPS/WSS. The sole plaintext listener is
the bounded health endpoint, which carries no application value, token, capability,
credential, mutation, or administrative action.
Web uses one browser-facing remote-api bridge for live traffic and one Docker-network-only remote-api query bridge per configured Server. Each private bridge is pinned to one router, so the Web backend can send an authenticated retained query to every router-local store; federation does not pretend to replicate durable history. The query bridge supplies reachability only. Directory capability, fresh use proof, reply validation, and the Server-signed terminal receipt remain authoritative for every accepted page.
Stock-Zenoh live-subscription boundary¶
PETRA 1.0 uses stock Zenoh and adds no capability-aware subscriber-declaration hook,
Relay plugin, subscription-lease wire, or Zenoh fork. Durable get remains different
from live subscription: Relay + Storage verifies the Directory-signed QUERY capability,
fresh exact-selector StorageUseProofV1, current revocation, and reply containment before
serving retained bytes. A live subscriber declaration carries no equivalent PETRA proof
that the native Relay can authoritatively evaluate.
First-party endpoints derive only registered subscriber selectors from their current
verified complete capability set and Directory-owned relationships. Field endpoints
declare separate exact opaque channel, cell, target, or command scopes; the Web machine
may declare only its registered all-cell position selector. Every receiver authoritatively
verifies every received LIVE envelope, capability, proof, typed containment, subject/key
binding, classification, known revocation, and relationship before decode or use. Voice
subscriptions are bounded
to one exact opaque channel at a time; cell-scoped position and heartbeat subscriptions
are bounded to assigned exact cells; command subscriptions are bounded to the endpoint's
exact opaque target; and an acknowledgement reader uses one exact authorized opaque
device source with only the command-identifier slot variable, then verifies the signed
target and correlates each command identifier before accepting the acknowledgement.
The separate durable plan-ack reader uses only the exact retained
record/plan-ack/<opaque-plan-version>/* selector issued for one authorized plan version;
it is not part of the live command-ack family.
There is no waypoint/**, unregistered global position/voice/command/ack selector,
waypoint/global/ack/**, or standalone sensor selector in first-party PETRA 1.0 code.
Those requirements constrain honest PETRA clients and prevent forged or mis-scoped LIVE values from changing application state. They are not a confidentiality boundary against a modified authenticated client. Field endpoints share one coarse wildcard transport subject, limited to the exact closed action/key registry. It controls reachability and topic families and is distinct from the Directory token that establishes PETRA application identity. A transport-reachable endpoint that holds the current deployment group-key epoch can widen its own stock-Zenoh live subscription and decrypt traffic it can observe, including after app-layer revocation but before key rotation. Receiver verification does not stop a key holder from removing its local checks; LIVE publication authentication prevents forgery, not observation.
Consequently, capability omission, membership removal, or app-layer revocation does not claim immediate remote teardown of an already declared live subscription. Future LIVE confidentiality against a captured or modified key holder is restored only when the revocation is known and the deployment group key is rotated so the holder cannot obtain the new epoch. Previously observed ciphertext and epochs already held remain exposed. This bounded audience-confidentiality limitation is an accepted PETRA 1.0 residual and is not extended to retained reads: widened or replayed durable queries still fail at Relay + Storage.
Capabilities contain authority; a selector never creates it. Wildcards are allowed only
when the signed capability contains the requested expression. Reply keys must be within
both the request selector and capability. Non-canonical aliases, _anyke, path-like
traversal, wildcard injection, and selector parameters that alter routing scope are
rejected. The shared positive and negative cases are in
capability-containment-v1.json.
For channel-scoped families, a capability is bound to the exact opaque channel expression. A deployment-wide family wildcard is not a substitute: the deployment group key is shared, so over-broad ciphertext access would also expose content to every holder of that key. Directory issues or refreshes the exact channel grant only after evaluating current membership and ownership. Cached grants never outlive the shorter of their own expiry and the configured offline horizon.
Directory capability contract¶
Directory keeps the authoritative channel-ownership and membership ledger needed to mint exact grants. Web's Comms Matrix and connected edge activation workflows submit requested changes to Directory; they do not become effective merely because Web SQL or an endpoint projection changed. Directory authenticates the requester, applies current role, ownership, membership, revocation, and classification policy, commits the authority change, and only then issues capabilities. Web and endpoints project the committed result. The actor-signed wire, idempotent receipt, narrow Web core-application relationship, and non-impersonation rules are defined in Connected authority mutations and Web application authority. Directory derives and seals the committed result; Web may forward an actor request but cannot sign it, supply authority bytes, or publish a SQL-first approximation.
An authenticated capability refresh returns the current exact grants for that token's principal and principal signing key. It never accepts caller-declared membership, cells, or another requested scope as proof of entitlement. The refresh response supersedes the local capability set atomically: grants not present are removed before shared operation resumes. A capability's lifetime is at most the configured offline horizon.
Authenticated full-set refresh¶
The client first obtains Directory's existing 32-byte, single-use challenge. Challenges
expire after 120 seconds and are shared infrastructure, not bearer authority. The
capability-refresh proof has its own domain so a signature accepted by login, enrollment,
session extension, or another challenge consumer cannot be replayed here. Common owns
these additive messages in server/directory.proto:
message CapabilityRefreshRequestV1 {
uint32 version = 1; // exactly 1
bytes identity_token = 2; // exact canonical IdentityToken bytes
bytes server_nonce = 3; // exactly 32 bytes
uint64 issued_at_ms = 4;
bytes subject_signature = 5; // Ed25519, exactly 64 bytes
}
message CapabilitySetEntryV1 {
bytes directory_signed_capability = 1; // canonical StorageCapabilityV1
bytes committed_canonical_value = 2; // committed mode only; exact stored bytes
waypoint.ConfidentialityLabel committed_classification = 3;
string committed_owner_principal_id = 4;
bytes snapshot_authority_grant = 5; // purpose 3 publication only; canonical SnapshotAuthorityGrantV1
}
// `server/directory.proto` imports the closed enum from `server/authority.proto`.
// The two values below are the only non-UNSPECIFIED version-1 values.
message OperationContextV1 {
string operation_id = 1; // current Directory-canonical operation identifier
repeated AuthorityChannelKindV1 creatable_channel_kinds = 2;
// strictly ascending numeric enum order, unique: CHAT (1), then VOICE (2)
}
message CapabilityRefreshResponseV1 {
uint32 version = 1; // exactly 1
string subject_principal_id = 2;
bytes subject_signing_key = 3; // exactly 32 bytes
bytes identity_token_sha256 = 4; // exact request token bytes
uint64 generation = 5; // positive and strictly monotonic per principal
uint64 issued_at_ms = 6;
uint64 expires_at_ms = 7;
bytes request_nonce = 8; // exact request challenge
repeated CapabilitySetEntryV1 entries = 9;
bytes entries_sha256 = 10; // exactly 32 bytes
bytes directory_key_id = 11; // SHA-256(Directory public key), 32 bytes
bytes directory_signature = 12; // Ed25519, exactly 64 bytes
repeated OperationContextV1 operation_contexts = 13;
bytes operation_contexts_sha256 = 14; // exactly 32 bytes
}
OperationContextV1 is the complete Directory-authorized create-context projection,
not a capability, relationship, or caller assertion. It tells the bound subject only
which current operation contexts permit a connected chat and/or voice creation request.
Its operation_id is the exact immutable value that the subject must place in
ChannelCreateMutationV1.operation_id; it does not authorize a mutation by itself.
An operation identifier is non-empty, at most 256 UTF-8 bytes, has no Unicode control
code point or leading/trailing Unicode whitespace, and is compared byte-for-byte with no
case folding, Unicode normalization, trimming, alias, or display-name substitution.
Directory emits only its current canonical operation identifier. Each context has one or
more creatable_channel_kinds; values must be the closed, non-UNSPECIFIED
AuthorityChannelKindV1 values CHAT (1) and VOICE (2), in strictly ascending numeric
order with no duplicate. Contexts are in strictly ascending bytewise UTF-8
operation_id order and are unique. Empty/noncanonical/duplicate operation ids,
unknown/unspecified/duplicate/unsorted kinds, an empty kind list, unknown protobuf
fields, duplicate scalar fields, or non-deterministic encoding invalidate the whole
response. There are at most 4,096 contexts; the maximum response size below includes both
the capability entries and this projection. Directory must not truncate contexts or kinds.
For a valid context, canonical_context is the deterministic protobuf encoding of
OperationContextV1. Its set digest is:
SHA-256(
ascii("petra-capability-operation-contexts-v1\0")
|| u32be(context_count)
|| concat(bytes32(canonical_context))
)
The digest of the empty ordered context set is required when no create context is currently authorized. It is not an omitted optional field.
The request signing input is:
ascii("petra-capability-refresh-request-v1\0")
|| u32be(version)
|| bytes32(identity_token)
|| server_nonce[32]
|| u64be(issued_at_ms)
Directory requires a canonical, Directory-signed, unexpired IdentityToken; the request
signature must verify under that token's exact signing key, and the token must still match
the current Directory principal/device credential, role, clearance, and
principal/signing-key revocation state. issued_at_ms is accepted only within the shared
60-second clock-skew window. Directory validates the canonical token and proof before it
atomically consumes the matching unexpired challenge; concurrent reuse has exactly one
winner. A missing, expired, already-consumed, cross-protocol, or non-32-byte challenge
fails closed.
For a valid entry, canonical_entry is the deterministic protobuf encoding of
CapabilitySetEntryV1. Entries are sorted by ascending bytewise
directory_signed_capability. Two entries with the same (action, grant_mode,
canonical_expression, canonical purposes, authority_ledger_version,
committed_mutation_sha256) are a semantic duplicate and invalidate the complete response
even if capability ids or signatures differ. The semantic fields and exact canonical key
are read from the verified capability. The entry-set digest is:
SHA-256(
ascii("petra-capability-set-members-v1\0")
|| u32be(entry_count)
|| concat(bytes32(canonical_entry))
)
snapshot_authority_grant is non-empty exactly when the entry's verified publication
capability permits ENVELOPE_PURPOSE_AUTHORITATIVE_STATE_SNAPSHOT. It contains the
canonical deterministic bytes of the matching SnapshotAuthorityGrantV1. Its Web
principal/signing key, family/scope, audience segment/digest, classification ceiling,
validity, and Directory key must match the capability and refresh subject. A purpose-3
capability without the grant, a grant on another purpose/action, an orphan grant, or a
capability/grant mismatch invalidates the complete response. Each signed
family/scope/audience grant has exactly two matching entries: the bounded sequence
selector .../<audience>/*/manifest and chunk selector
.../<audience>/*/chunk/*. Common permits those wildcards only in the sequence/index
slots and contains only canonical concrete snapshot keys with the same exact
family/scope/audience. The grant is repeated byte-identically in both matching entries;
normal semantic-duplicate rejection still applies to the capability entries.
There is no separate snapshot-authority refresh or cache. The verified complete-set replacement installs each snapshot grant and its matching publication capability as one entry and removes both by omission. Snapshot work, shared traffic, and outbox drainage remain blocked until that atomic replacement succeeds. The 4,096-entry and 16 MiB limits include field 5.
The Directory response signing input is:
ascii("petra-capability-set-response-v1\0")
|| u32be(version)
|| str32(subject_principal_id) || subject_signing_key[32]
|| identity_token_sha256[32]
|| u64be(generation) || u64be(issued_at_ms) || u64be(expires_at_ms)
|| request_nonce[32] || entries_sha256[32] || operation_contexts_sha256[32]
|| directory_key_id[32]
The response and every contained capability use the same trusted Directory signing key.
The response subject/key and token digest must equal the verified request, and every
capability must bind that subject/key and lie within the response validity interval.
The response requires issued_at_ms < expires_at_ms; its expiry is no later than the
verified identity expiry, the configured offline horizon from response issuance, and any
earlier Directory policy deadline. A consumer rejects at now_ms >= expires_at_ms.
Directory allocates generation under a per-principal row lock and never reuses a value;
overflow fails closed. Clients persist and compare generations in the namespace
(subject_principal_id, subject_signing_key), reject a response whose generation is not
strictly greater than the last accepted value in that namespace, and never use arrival
time to order sets.
COMMITTED_AUTHORITY_MUTATION entries require every committed field. The capability
expression must be one exact concrete canonical key, and the exact persisted once-sealed value,
classification, owner, purpose, key digest, and ledger version must reproduce the signed
commitment. Directory reads these bytes and metadata from the current authority head and
never decrypts, re-seals, reconstructs, or re-encodes them during issuance. A reusable or
live capability requires an empty value, absent/default classification, and empty owner.
A malformed mode/metadata combination invalidates the whole response.
A response contains at most 4,096 entries, its deterministic encoded form is at most 16,777,216 bytes, and one committed canonical value is at most 1,048,576 bytes. Common checks encoded length before decoding and checks count, per-entry length, cumulative length, and integer arithmetic before allocation or hashing. Exactly-at-limit values are valid; overflow or one byte/entry over fails closed. Directory rejects, in the authority mutation transaction, any change that would make any affected subject's complete set unissuable. It never truncates a set.
Only a fully decoded, canonical, bounded, correctly signed, fresh, strictly newer response causes one atomic cache replacement of both capability entries and operation contexts. A verified signed empty set is a successful replacement and removes all cached authority and all create eligibility. Transport failure, timeout, oversize, decode/canonicalization failure, invalid signature or binding, stale generation, or one invalid entry/context is not an empty set: the prior cache stays byte-for-byte unchanged, the endpoint remains visibly degraded, and shared traffic and outbox drain remain blocked. Omission from a successful complete set removes the grant or operation context before either operation can resume; a client also clears any selected context that is absent or no longer permits the selected channel kind.
Operation-context selection and connected activation¶
The complete context projection has the same subject/key binding, echoed nonce, generation, absolute expiry, offline-horizon bound, Directory signature, revocation handling, rollback floor, persistence/sealing, session wipe, and reconnect barrier as the capability entries. It has no separate endpoint, signature, cache, refresh lifetime, or fallback reader.
- Zero contexts means the subject currently has no chat or voice creation eligibility. The UI may retain or edit a local draft, but must show it as unactivated and cannot submit, publish shared state, alter membership, or enqueue authority work.
- One context may be visibly preselected as a convenience. The selected
operation_idremains a non-authorizing local preference, and the UI must show the selected operation and supported channel kinds. - Multiple contexts require an explicit operator selection from the verified ordered list before create is enabled. The client never guesses a default from the first list item, a nearby ORBAT unit, a prior selection, or a display label.
The client may use only the selected verified operation_id and a kind present in that
context to construct the actor-signed ChannelCreateMutationV1. Directory revalidates
the operation's existence, the actor's current relationship, effective capability,
clearance, selected kind, and all ordinary create rules in the serializable commit
transaction; a context can therefore become invalid between display and submit. A valid
receipt is evidence of a commit, not permission to optimistically activate. After receipt
the client obtains and verifies a new complete refresh, atomically installs its higher
generation, and only then displays the channel as activated or begins its shared traffic.
On Directory loss, an expired/offline-horizon context, a failed or stale refresh, a revocation, or context omission/removal, a channel draft is visibly inert. It creates no authority outbox, Zenoh authority sample, channel definition, membership, or publication; there is no retry queue that later commits an unreviewed create. On reconnect the client refreshes revocation and the full set before enabling a draft submission, shared operation, or outbox drain. A removed context invalidates its saved selection rather than silently retaining, remapping, or substituting it.
No alternate operation-authority source
IdentityToken, ProfileBundle, ORBAT/content projections, local settings, user
input, display labels, and a prior receipt are not operation-context authority. A
client must not infer, mint, normalize, alias, persist as a fallback, or manually
type an operation id. There is no separate eligible-operations endpoint, second
cache, token claim, or compatibility reader. Only the verified current complete
refresh supplies the selectable IDs; Directory remains the final authority at commit.
Finite Directory-owned entitlements¶
Directory derives the complete set only from current ledgers, roles, channel memberships and ownership, operation assignments, clearance, connected cell provisioning, and principal/signing-key revocation state. Its per-subject entitlement index is a transactional materialized index over those facts, is rebuildable, and is never an authority source.
Channel membership has one complete current semantic record and one retained, once-sealed membership projection at the exact channel-membership key. Common owns the projection shape:
message ChannelMembershipMemberV1 {
string principal_id = 1;
bool is_admin = 2;
}
message ChannelMembershipSetV1 {
uint32 version = 1; // schema version, exactly 1
string channel_id = 2;
repeated ChannelMembershipMemberV1 members = 3; // principal UTF-8 order, unique
}
The ownership projection remains separate. The current owner receives owner authority
from that projection; is_admin is not an owner flag. Authority ordering exists only in
the enclosing StorageCapabilityV1.authority_ledger_version; the membership payload has
no second semantic revision.
Every membership change publishes the complete sorted set at the next channel aggregate
ledger version and atomically updates the current semantic state and every affected
subject-index row. Removal is a higher-version full projection plus index removal in the
same transaction; a delta, caller-maintained index, or lower/equal projection cannot
grant or preserve access.
Connected provisioning may assign one subject at most 256 exact canonical geohash-5 cells. ServerToken routing coverage is a distinct deployment configuration and does not confer endpoint drawing or target authority. Cell provisioning is a connected, transactional Directory-internal operation in 1.0; there is no temporary caller-declared HTTP provisioning surface.
The entitlement matrix always composes action authority with exact scope. When token
roles supply the action, Directory resolves them only through Common's
effectiveCapabilities; it never checks raw role-name strings. Current owner and command
appointments are separate Directory-owned action authorities where the matrix names them,
not token roles. A current Directory membership, active channel ledger, ORBAT visibility,
cell, invite, appointment, or target assignment supplies the exact relationship scope.
Both the named action authority and relationship must pass. Unknown roles and missing
relationships grant nothing. Every emitted capability remains bound to the exact subject
and machine signing key, bounded by current clearance and the offline horizon, and
disappears by omission from the next verified complete set when either gate stops passing.
Channel matrix¶
| Current Directory relationship | Required action authority | Exact grant |
|---|---|---|
| channel member | view_operation |
query that channel's retained chat and current definition, membership, ownership-transfer, and lifecycle projections |
| channel member | send_chat |
publish retained chat for that channel |
| channel member | transmit_voice |
publish the closed non-retained LIVE voice expression for that channel and the subject's fixed opaque source |
| exact pending invitee | view_operation |
query only the exact invite and the minimum channel definition needed to display it |
| current owner | current owner authority | query that channel's control projections required for connected management; no content grant follows without membership |
| current active channel ledger | manage_channels |
query that channel's control projections required for connected management; no content grant follows without membership |
| current active channel ledger | manage_members plus (view_operation or manage_channels) |
deployment-wide retained-chat and control-projection query, or connected management override; content publication still requires membership and its content action |
Invite scope never includes chat, voice, membership, another invite, or another channel.
Channel creation, membership change, transfer, and retirement remain connected Directory
mutations. A reusable capability never authorizes any of them; their current retained
heads are exact COMMITTED_AUTHORITY_MUTATION entries.
Cell and COP matrix¶
| Current Directory relationship | Required action authority | Exact grant |
|---|---|---|
| assigned exact geohash-5 cell | view_operation |
query draw/**, target/**, and their tombstones in that cell |
| assigned exact geohash-5 cell | draw_annotations or manage_annotations |
publish drawings and drawing tombstones in that cell |
| assigned exact geohash-5 cell | manage_targets |
publish targets and target tombstones in that cell |
nominate_target is a Web targeting-board action and grants no COP publication. The
subscriber still enforces drawing ownership after capability verification:
draw_annotations may mutate only the verified sender's drawings, while
manage_annotations is the cross-owner override. There is no retained cross-cell or
moving-cell wildcard. Common's closed retained-family registry includes exact-cell target
and target-tombstone forms alongside drawing forms; no standalone sensor family is added.
ORBAT and record matrix¶
Directory constructs Common's visible_unit_ids closure from current complete ORBAT
records and the subject's assigned units: each assignment contributes itself, its direct
parent, and every descendant. It does not interpret membership as deployment-wide record
access. The deployment administrator keeps the existing oversight scope across active
units.
| Current Directory relationship | Required action authority | Exact grant |
|---|---|---|
unit in visible_unit_ids |
view_operation |
query the exact per-unit ORBAT, plan, report, and report-requirement families |
| current command authority over the exact scope | current command appointment authority | publish plans and report requirements for that exact unit scope |
unit in visible_unit_ids inside an accessible operation |
publish_plan |
publish plans and report requirements for that exact unit scope |
| current appointment on the reporting unit itself | current command appointment authority | publish reports for that exact unit, including the documented provisional-team lead case |
| subject entitled to one exact published plan version | exact plan-version receipt entitlement | publish one PlanAcknowledgementV1 under the subject-bound exact key |
| authorized commander/author reader for one exact plan version | exact plan-version acknowledgement readership | query only that version's typed record/plan-ack/<opaque-plan-version>/* prefix |
| current active unit | manage_members plus view_operation |
deployment-wide oversight query for that unit's ORBAT, plan, report, and report-requirement families; publication still requires its ordinary action authority and scope |
An appointment is the explicit Directory-owned action authority for these commander
publications; it does not make an ordinary observer/member write-capable outside that
appointment. author_plan permits drafting only and never wire publication. Membership
or publish_plan alone never permits report publication. Appointment removal removes the
report grant by full-set omission. ORBAT changes remain exact #145 committed mutations;
there is no reusable ORBAT publication authority.
Command and self-publication matrix¶
A subject with command_device_camera or command_device_movement receives an exact
non-retained LIVE command target only when the active device principal is inside the
subject's current ORBAT visibility/operation scope. A device absent from ORBAT requires a
connected, explicit Directory-owned command-target assignment. No command capability is
derived from the global active-device list. The transport capability may carry either
command action, but the receiving Node must still authorize the decoded subtype:
camera-only authority never permits movement.
An active device principal receives only its self-bound live command-acknowledgement source. Plan-ack authority is separately derived for one exact published version under the durable plan-ack contract. Every active, non-revoked, provisioned tactical endpoint may receive self-bound position and heartbeat LIVE grants. A browser/human bearer-only session receives none. A current FIDO-bound browser endpoint is a separate short-lived issuance class: it may receive an exact self-bound heartbeat, but receives no position grant, seven-day offline horizon, Web-core grant, committed publication grant, or snapshot authority. Its direct human chat, voice, drawing, command, and query authority remains subject/key-bound and requires the ordinary action-plus-relationship gates. The complete contract is FIDO-bound browser endpoint capabilities. Gateway retains one authenticated opaque position source; external track identity stays inside the verified payload. Cached grants remain usable during Directory disconnection only until their signed expiry; reconnect performs complete-set reconciliation and omission before shared operation resumes.
This contract is tracked by docs #140, Common #198, and Directory #147, which must land before Directory #144.
Common owns this deterministic protobuf:
enum StorageActionV1 {
STORAGE_ACTION_V1_UNSPECIFIED = 0;
STORAGE_ACTION_V1_PUBLISH = 1;
STORAGE_ACTION_V1_QUERY = 2;
}
enum StorageGrantModeV1 {
STORAGE_GRANT_MODE_V1_UNSPECIFIED = 0;
STORAGE_GRANT_MODE_V1_REUSABLE_CONTENT = 1;
STORAGE_GRANT_MODE_V1_COMMITTED_AUTHORITY_MUTATION = 2;
STORAGE_GRANT_MODE_V1_LIVE_PUBLICATION = 3;
STORAGE_GRANT_MODE_V1_REGISTERED_CONTENT = 4;
}
message StorageCapabilityV1 {
uint32 version = 1; // exactly 1
StorageActionV1 action = 2;
string subject_principal_id = 3;
bytes subject_signing_key = 4; // Ed25519, exactly 32 bytes
string canonical_expression = 5; // exact opaque key or contained wildcard scope
repeated EnvelopePurpose purposes = 6; // ascending unique, no unspecified value
Clearance classification_ceiling = 7;
uint64 issued_at_ms = 8;
uint64 expires_at_ms = 9;
bytes capability_id = 10; // exactly 16 random bytes
bytes directory_key_id = 11; // SHA-256(Directory public key), 32 bytes
bytes directory_signature = 12; // Ed25519, exactly 64 bytes
StorageGrantModeV1 grant_mode = 13;
uint64 authority_ledger_version = 14; // committed mutation only; otherwise zero
bytes committed_mutation_sha256 = 15; // committed mutation only, exactly 32 bytes
bytes registered_content_sha256 = 16; // registered content only, exactly 32 bytes
}
One capability authorizes one action and one canonical expression. Channel capabilities
contain the exact opaque channel segment; a trailing /** may contain that channel's
registered subkeys but never a sibling channel. purposes is a set, encoded in ascending
numeric order. The exact expression is also the opaque audience boundary in version 1;
there is no separate caller-controlled audience claim.
str32(s) = u32be(len(utf8(s))) || utf8(s) and bytes32(b) =
u32be(len(b)) || b.
canonical_clearance(c) is defined here so the foundational storage capability does not
depend on a later recovery implementation:
str32(policy_id) || str32(max_classification)
|| u32be(category_count)
|| concat(
u32be(category_type) || str32(tag)
|| u32be(value_count) || concat(str32(value))
)
Categories are sorted by (category_type, UTF-8 tag) with no duplicate type/tag; values
are unique and sorted by UTF-8 bytes. Missing/malformed clearance or a non-canonical set
fails closed. Directory signs these bytes:
ascii("petra-storage-capability-v1\0")
|| u32be(version) || u8(action)
|| str32(subject_principal_id) || subject_signing_key[32]
|| str32(canonical_expression)
|| u32be(purpose_count) || concat(u32be(purpose))
|| bytes32(canonical_clearance(classification_ceiling))
|| u64be(issued_at_ms) || u64be(expires_at_ms)
|| capability_id[16] || directory_key_id[32]
|| u32be(grant_mode) || u64be(authority_ledger_version)
|| u8(len(committed_mutation_sha256)) || committed_mutation_sha256
|| if grant_mode == REGISTERED_CONTENT:
u8(len(registered_content_sha256)) || registered_content_sha256
The final conditional append is the version-1 extension point for
REGISTERED_CONTENT. Capabilities in the three earlier modes retain their exact historical
signing bytes. Every earlier mode requires registered_content_sha256 to be empty;
REGISTERED_CONTENT requires committed_mutation_sha256 to be empty. The two fields and
their domains are never interchangeable.
directory_signed_capability is the deterministic protobuf encoding of the message.
Common's encoder emits ascending fields, one encoding of every scalar, one packed
purposes field, minimal varints, and no unknown fields. A verifier decodes, validates,
deterministically re-encodes, and requires byte-for-byte equality with the received bytes;
this rejects reordered fields, duplicate scalars, alternate default encodings, unknown
fields, and non-canonical nested clearance bytes before hashing the capability.
All verifier modes require version/action validity, canonical ordering and expression
grammar, non-empty subject id, exact key and signature lengths, lifetime no longer than
the horizon, a trusted Directory key whose SHA-256 equals directory_key_id, a valid
Directory signature, current subject/signing-key revocation state, and exact equality
with the proof or envelope signer's verified principal and device key.
- Active use (new publish/query attachment) requires
issued_at_ms <= use_proof.issued_at_ms < expires_at_msand a fresh proof timestamp at current time. - Retained authority (restart/serve/consumer verification) requires
issued_at_ms <= envelope.issued_at_ms < expires_at_ms; it does not compare capability expiry with the later catch-up time, but current revocation remains mandatory.
Publication requires Common::decide(envelope.classification,
capability.classification_ceiling) == Permit; missing or malformed classification or
ceiling, a policy mismatch, or any other CMBAC denial fails closed. Unknown enum values
are rejected.
There are exactly four grant modes:
REUSABLE_CONTENTauthorizes a read-onlyQUERYover any registered retained family, orPUBLISHfor ordinary non-authority content, within the exact expression, purpose set, classification ceiling, and validity interval. It requiresauthority_ledger_version == 0and an empty commitment. ForPUBLISH, it is never valid for channel definition, membership/invite/join/leave/kick, ownership transfer, retire/delete, ORBAT appointment/membership, or another authority/lifecycle key. A query grant never authorizes a mutation.COMMITTED_AUTHORITY_MUTATIONis publish-only and represents one mutation that Directory has already validated and committed to its authority ledger while connected. It requires a positive ledger version, exactly one allowed authority purpose, and a 32-byte commitment. Re-signing or replaying it can only reproduce that already committed mutation; it cannot create, expand, remove, or transfer different authority offline.REGISTERED_CONTENTis publish-only and represents one exact immutable content value that Directory authorized through a connected, dual-signed registration. It permits exactly the registered plan purposeDURABLE_GRANT, requires zero authority-ledger version and a separate 32-byteregistered_content_sha256, and is retained and replayable within the offline horizon. It is exact-key only and valid solely for the closed registered-content family, initially Web-authored plan versions. It never authorizes an authority mutation,LIVEtraffic,QUERY, or arbitrary reusable content. Read authority for the same exact key remains a separateREUSABLE_CONTENTcapability.LIVE_PUBLICATIONis publish-only, permits exactly the singleLIVEpurpose, and is valid only for a family in the closed non-retained registry below. It requiresauthority_ledger_version == 0and an empty commitment. It never authorizesQUERY, Relay + Storage persistence, mesh catch-up, restart loading, historical mesh serve, transport-outbox replay, or another envelope purpose. Its normative cryptographic deadline ismin(capability.expires_at_ms, identity.expires_at_ms, checked_add(capability.issued_at_ms, offline_horizon_ms)); overflow fails closed. A deployment may impose an earlier locally configured policy deadline, but that local limit cannot extend the signed grant and is not part of the canonical wire calculation or cross-language vectors. Directory folds any earlier signed policy deadline intocapability.expires_at_ms. A fresh envelope and proof are required for every new publication.
The exact committed-authority commitment is:
SHA-256(
ascii("petra-authority-mutation-v1\0")
|| storage_key_digest[32]
|| SHA-256(payload)[32]
|| bytes32(canonical_label(classification))
|| str32(owner_principal_id)
|| u32be(purpose)
|| u64be(authority_ledger_version)
)
canonical_label(l) is str32(policy_id) || str32(classification) ||
u32be(category_count) || concat(u32be(category_type) || str32(tag) ||
u32be(value_count) || concat(str32(value))) || u64be(creation_dtg_ms) ||
str32(originator_id), with categories and values sorted/unique by the same rules as
clearance. Directory returns this grant only after committing the exact semantic authority request. Relay +
Storage and consumers recompute the commitment from the signed envelope metadata and
payload bytes. A mode/key-family mismatch, zero ledger version, or commitment mismatch
fails closed. Directory is the only component that advances the ledger version.
The registered-content commitment is deliberately different from the final-envelope digest:
SHA-256(
ascii("petra-registered-content-v1\0")
|| str32(exact_canonical_publication_key)
|| bytes32(exact_sealed_payload_bytes)
|| bytes32(canonical_label(classification))
|| str32(owner_principal_id)
|| u32be(purpose)
|| storage_key_digest[32]
)
For the registered plan family, owner_principal_id is required empty and purpose is
exactly DURABLE_GRANT. The payload input is the exact sealed payload bytes, not a
plaintext PlanRecord and not a completed AuthEnvelope. Directory's registration
binds this digest to the semantic plan tuple, exact classification, recipients, and
readers before the capability exists. Relay + Storage can therefore recompute it without
decrypting, while the capability and final envelope do not form a commitment cycle.
An authority-ledger version is allocated within exactly one concealed Directory aggregate. A channel aggregate covers definition, membership, invites, ownership, and lifecycle/tombstone mutations for the same semantic channel. Its logical projection streams are the channel definition, membership set, each invitee's invite, ownership, and lifecycle/tombstone. Directory assigns a distinct next aggregate version to every committed channel mutation, including multiple records produced by one user action.
ORBAT is deliberately different because Common defines one full OrbatRecord and one
retained key per unit. A Directory-issued unit_id is canonical and deployment-global:
it is never reused by another operation, and Directory state binds it to exactly one
operation. The one key
waypoint/global/record/orbat/<opaque-record-orbat-unit> carries the atomic full unit
record: definition/name, parent, membership, commander appointment, origin/provisional
state, classification, ownership, and lifecycle. These subfields are not independent
wire projection streams. Every accepted change republishes the complete record at
seq + 1; StorageCapabilityV1.authority_ledger_version equals that exact
OrbatRecord.seq. Retirement republishes the complete record with retired=true at a
higher sequence. A multi-unit Directory transaction commits all affected full records
atomically, with each unit advancing its own sequence exactly once.
The acceptance high-water mark is per channel logical projection stream and per atomic ORBAT unit record. Within the same stream or unit, a lower version is rollback; equal version plus the same commitment is an idempotent duplicate; equal version plus a different commitment is Directory equivocation and fails closed. A higher channel version observed in another channel stream does not reject a delayed non-superseded record. Cross-stream semantic precedence still uses the channel aggregate version: a valid channel tombstone suppresses earlier definition, membership, invite, and ownership records, while an unrelated later membership version does not suppress an earlier invite. For ORBAT, there is no cross-subfield merge: the higher complete unit record replaces the lower one, so membership cannot erase definition or appointment and appointment cannot erase definition or membership. Relay + Storage can compare repeated values on one exact key but does not decode or correlate the cross-key channel aggregate or semantic ORBAT operation.
A compromised disposable Storage may still withhold a newer value from a cold consumer, which remains an availability/rollback attack handled by replicas, persisted endpoint state, Directory refresh, and snapshot recovery rather than by trusting storage arrival order.
Closed grant-mode registry¶
The generic retained store accepts only these PETRA families in version 1. Every
<opaque-...> component is exactly 32 lowercase hexadecimal characters derived under
the named registered domain; <cell> retains the canonical five-character geohash
grammar. Snapshot <family>, <snapshot-seq>, and <index> use the exact grammar in
#126.
| Exact canonical opaque publication family | Envelope purpose | PUBLISH grant mode |
QUERY grant mode |
|---|---|---|---|
waypoint/global/chat/<opaque-chat-channel>/<opaque-chat-message> |
LIVE |
REUSABLE_CONTENT |
REUSABLE_CONTENT |
waypoint/global/channel/<opaque-channel-id> |
DURABLE_GRANT |
COMMITTED_AUTHORITY_MUTATION |
REUSABLE_CONTENT |
waypoint/global/channel/del/<opaque-channel-id> |
DURABLE_GRANT |
COMMITTED_AUTHORITY_MUTATION |
REUSABLE_CONTENT |
waypoint/global/channel/members/<opaque-channel-id> |
DURABLE_GRANT |
COMMITTED_AUTHORITY_MUTATION |
REUSABLE_CONTENT |
waypoint/global/channel/transfer/<opaque-channel-id> |
DURABLE_GRANT |
COMMITTED_AUTHORITY_MUTATION |
REUSABLE_CONTENT |
waypoint/global/invite/<opaque-invite-invitee>/<opaque-invite-channel> |
DURABLE_GRANT |
COMMITTED_AUTHORITY_MUTATION |
REUSABLE_CONTENT |
waypoint/<cell>/draw/<opaque-drawing-id> |
DURABLE_CONTENT |
REUSABLE_CONTENT |
REUSABLE_CONTENT |
waypoint/<cell>/draw/del/<opaque-drawing-id> |
DURABLE_CONTENT |
REUSABLE_CONTENT |
REUSABLE_CONTENT |
waypoint/<cell>/target/<opaque-target-id> |
DURABLE_CONTENT |
REUSABLE_CONTENT |
REUSABLE_CONTENT |
waypoint/<cell>/target/del/<opaque-target-id> |
DURABLE_CONTENT |
REUSABLE_CONTENT |
REUSABLE_CONTENT |
waypoint/global/record/plan/<opaque-record-plan-unit>/<opaque-record-plan-id> |
DURABLE_GRANT |
exact REGISTERED_CONTENT for a registered Web-authored version; existing non-Web paths remain REUSABLE_CONTENT |
exact REUSABLE_CONTENT |
waypoint/global/record/plan-ack/<opaque-plan-ack-version>/<opaque-plan-acknowledger> |
DURABLE_CONTENT |
exact REUSABLE_CONTENT |
typed exact-version-prefix REUSABLE_CONTENT |
waypoint/global/record/report/<opaque-record-report-unit>/<opaque-record-report-id> |
DURABLE_CONTENT |
REUSABLE_CONTENT |
REUSABLE_CONTENT |
waypoint/global/record/requirement/<opaque-record-requirement-unit>/<opaque-record-requirement-id> |
DURABLE_GRANT |
REUSABLE_CONTENT |
REUSABLE_CONTENT |
waypoint/global/record/orbat/<opaque-record-orbat-unit> |
DURABLE_GRANT |
COMMITTED_AUTHORITY_MUTATION |
REUSABLE_CONTENT |
waypoint/global/snapshot/<family>/<opaque-snapshot-scope>/<opaque-snapshot-audience>/<snapshot-seq>/manifest |
AUTHORITATIVE_STATE_SNAPSHOT |
REUSABLE_CONTENT plus the #126 field-9 grant |
REUSABLE_CONTENT |
waypoint/global/snapshot/<family>/<opaque-snapshot-scope>/<opaque-snapshot-audience>/<snapshot-seq>/chunk/<index> |
AUTHORITATIVE_STATE_SNAPSHOT |
REUSABLE_CONTENT plus the #126 field-9 grant |
REUSABLE_CONTENT |
Snapshot PUBLISH and QUERY capabilities use the two bounded selector forms defined by
126 rather than preallocating a concrete sequence. Action remains exact and only the¶
PUBLISH entries carry the field-5 snapshot grant.
While at least one operation is current, the Directory-managed automatic Web relationship may also carry these closed all-cell ordinary selectors:
| Automatic Web selector | Actions | Purpose |
|---|---|---|
waypoint/*/draw/** |
PUBLISH, QUERY |
DURABLE_CONTENT |
waypoint/*/target/** |
PUBLISH only |
DURABLE_CONTENT |
waypoint/*/pos/** |
QUERY only |
LIVE |
In each form, * contains exactly one canonical five-character geohash cell and **
contains only the registered descendants of that family. Drawing authority includes its
typed add and tombstone keys; target authority includes its typed value and tombstone keys.
Target query is deliberately absent. Position authority is read-only and does not retain
position samples. None of these selectors contains another key family, a global subtree,
an extra action, or another purpose. Manual relationships and field endpoints remain
cell-exact, and Directory removes all three through complete-set replacement after the
final current operation retires.
The Directory-managed automatic Web relationship has a separate closed aggregate
retained-query profile. While at least one operation is current, Directory may issue Web
QUERY + REUSABLE_CONTENT for exactly these selectors and purposes:
| Automatic Web selector | Purpose |
|---|---|
waypoint/global/channel/** |
DURABLE_GRANT |
waypoint/global/record/plan/** |
DURABLE_GRANT |
waypoint/global/record/report/** |
DURABLE_CONTENT |
waypoint/global/record/requirement/** |
DURABLE_GRANT |
waypoint/global/record/orbat/** |
DURABLE_GRANT |
For these five query capabilities only, Common classifies the audience as the reserved
single sentinel ['*']: Relay + Storage scans every concrete retained key in that one
family, globally sorts and deduplicates the merged results, then applies the ordinary
signed pagination and reply bounds. The sentinel never matches chat, invites, plan
acknowledgements, a generic record/** tree, or another family.
Automatic Web may also receive PUBLISH + REUSABLE_CONTENT for the sole global-record
aggregate selector waypoint/global/record/requirement/** with DURABLE_GRANT, because Web
is the privileged HQ author of operation-wide report tasking. The other four global
aggregates remain query-only; they cannot publish channel or ORBAT mutations, plans, or
reports. Manual relationships, field endpoints, and browser principals remain exact.
Directory removes the aggregate profile through complete-set replacement after the final
current operation retires.
This is Web's privileged HQ publication role. A current Web service identity can publish
drawings and targets in any canonical cell without an administrator pre-enumerating map
cells, and it can read the closed all-cell position and aggregate retained families above.
The privilege is still a finite Directory-issued family/action contract: it does not grant
waypoint/**, arbitrary record publication, endpoint impersonation, group-key issuance,
or connected authority mutation. Browser sessions never inherit it.
Plan acknowledgements use an exact concrete PUBLISH expression and only
waypoint/global/record/plan-ack/<exact-opaque-plan-version>/* for QUERY/live read. The
single * is the typed 32-lowercase-hex plan-acknowledger slot. A wildcard plan version,
**, and operation- or deployment-wide containment are not registered. See the
plan-acknowledgement contract.
A family, purpose, action, or mode combination absent from this table is rejected by the generic retained store. The non-retained families registered below are not thereby made durable. Router token/liveliness and revocation control retain only their closed, non-storage behavior.
Closed non-retained LIVE registry¶
LIVE_PUBLICATION accepts exactly these signed capability expressions. In this table,
* is a PETRA typed placeholder for one canonical segment in the declared position;
it is not evaluated through generic Zenoh key-expression containment.
| Directory-signed capability expression | Concrete publication requirement |
|---|---|
waypoint/*/pos/<exact-source> |
* is one canonical five-character geohash; source is unchanged |
waypoint/*/heartbeat/<exact-source> |
* is one canonical five-character geohash; source is unchanged |
waypoint/global/voice/<exact-channel>/<exact-source>/* |
* is one canonical opaque session; channel and source are unchanged |
waypoint/global/cmd/<exact-target>/* |
* is one canonical opaque command id; target is unchanged |
waypoint/global/ack/<exact-source>/* |
* is one canonical opaque command id; source is unchanged |
<exact-source>, <exact-channel>, and <exact-target> are exact 32-character
lowercase-hex opaque segments from their registered domains. The source in position,
heartbeat, voice, and acknowledgement authority is bound to the capability's verified
subject and signing key by Directory. Command target authority is exact; there is no
cmd/*/* grant.
The typed parser rejects **, $*, a wildcard in any other slot, aliases, missing or
extra segments, malformed cells or opaque segments, generic prefix containment, and any
attempt to cross families. A concrete publication key contains no wildcard. Its
AuthEnvelope.storage_key_digest binds that exact key, and the fresh
StorageUseProofV1 binds the capability digest, exact concrete key, digest of the
complete encoded envelope bytes, nonce, and timestamp. Capability bytes in the proof
attachment are byte-identical to signed envelope field 11.
Closed non-retained live-read QUERY registry¶
QUERY is PETRA's read action. In addition to bounded retained get, an exact
Directory-issued QUERY capability may be used by first-party code to derive an exact
live subscriber selector. Version 1 registers only these non-retained live-read forms:
Directory-signed QUERY expression |
Exact audience boundary |
|---|---|
waypoint/<exact-cell>/pos/** |
One canonical geohash-5 cell; only position-source children in that cell |
automatic Web waypoint/*/pos/** |
Every canonical geohash-5 cell; only position-source children |
waypoint/global/voice/<exact-channel>/** |
One exact opaque voice channel; only source/session children of that channel |
waypoint/global/ack/<exact-source>/* |
One exact opaque device source; only its command-identifier slot varies |
Each uses REUSABLE_CONTENT, exactly the LIVE purpose, zero authority-ledger version,
and no mutation commitment. These entries are read authority only: they never authorize
publication, turn position/voice/acknowledgement into retained content, admit a Storage
query, or create replay/restart/outbox behavior. Retained chat remains in the closed
retained registry and uses the exact per-channel
waypoint/global/chat/<opaque-chat-channel>/** selector for both catch-up and live
ingestion.
Common's closed classifier accepts an already verified capability and returns its typed
family and exact channel, cell, or device audience with no generic string fallback. The
all-cell position audience is reserved to Directory's automatic Web relationship; manual
input cannot request it. For the non-retained live-read registry it rejects
waypoint/**, any other wildcard or noncanonical cell, waypoint/global/voice/**,
waypoint/global/ack/**, heartbeat, sensor, command, snapshot, membership, and every
unregistered family. Exact retained invite QUERY classification remains registered for
its ordinary invitee path; it is not a Web recorder grant. The complete-set consumer
layer admits classifier output only after Directory signature, subject/session signing-key
binding, generation, validity, classification ceiling, and known revocation pass.
Consumer setup therefore admits only exact audiences from the current complete set, so
substituting a sibling channel or cell that has no entry fails exact matching rather than
being inferred.
Because stock Zenoh carries no capability attachment on subscriber declarations, this
classification constrains first-party subscription setup but is not a Relay security
boundary. A durable query still requires a fresh StorageUseProofV1; a live subscriber
instead verifies every delivered publisher envelope/capability/proof and the read
capability's current classification/audience constraints before opening or using the
sample. Full-set replacement closes omitted first-party selectors atomically, subject to
the accepted stock-Zenoh residual above.
Retained chat remains deliberately distinct. A chat message has envelope purpose LIVE
but uses the retained REUSABLE_CONTENT row above so Storage may preserve and serve its
bounded history. It never uses LIVE_PUBLICATION. No other non-retained LIVE family may
enter Relay + Storage persistence, restart, serve, mesh replay/query, or transport-outbox
paths. A verified trusted application subscriber may archive a live value under its own
application retention/audit policy; that archive is not a Zenoh Storage value and cannot
be republished as original endpoint traffic.
Gateway bridge identity¶
Directory grants each authenticated Gateway principal one opaque position source. A
Gateway publishes every external track under that source while the geohash cell alone may
vary. The external origin_uid is carried only inside the signed and sealed
TacNetMessage payload during source migration; the
direct-payload contract assigns it to the future
canonical position field. It never appears in a key expression or operational log. A
consumer identifies a bridged track by (verified Gateway principal, origin_uid), not by
an endpoint identity inferred from the publication key. Two external tracks therefore
share the Gateway source segment but remain distinct signed payload origins.
Only a Directory-authorized Gateway identity may emit a non-empty origin_uid; native
position publishers leave it empty. It is valid only when its UTF-8 encoding is at most
256 bytes, contains no Unicode control character and none of /, *, ?, #, or $,
and otherwise preserves exact code points and case. Android, Web, and every other position
consumer must key, deduplicate, expire, and render Gateway tracks by that tuple before
Gateway switches to its one-source key. A sender-key-only projection would collapse
multiple external tracks and is not release-compatible. This dependency is tracked by
Android #280 and
Web #617.
The Gateway may continue publishing newly observed external tracks while disconnected inside its cached grant horizon. It receives no source wildcard and does not mint a grant for an external origin.
Retired standalone sensor plane¶
PETRA 1.0 has no waypoint/<cell>/sensor/<sensor-id> publication family and no
SensorInfo or SensorRemoved data plane. A sensor represented on the tactical map is a
normal drawing/annotation; removal is its signed drawing tombstone. Web and Android derive
sensor-specific views from that one retained model instead of maintaining a second
outbox, subscriber, key expression, or deletion stream.
Consumer parity lands before deleting the duplicate producer/outbox/subscriber/key paths,
and the Common message types are removed only after every production reference has
migrated. Android #281 and
Web #618 own consumer parity and
duplicate-path removal; Common #196
then removes the unused messages and key helpers. Node's existing live
camera-advertisement metadata is a different transient surface and remains unchanged.
LIVE_PUBLICATION registers no sensor family, alias, or compatibility exception.
Capability-use proof¶
Publications and queries carry this deterministic-protobuf attachment as the Zenoh publication/query attachment. It is transport authorization, not part of the stored value:
message StorageAuthorizationV1 {
bytes directory_signed_capability = 1;
StorageUseProofV1 use_proof = 2;
}
message StorageUseProofV1 {
uint32 version = 1; // exactly 1
uint32 action = 2; // 1 = publish, 2 = query
bytes capability_sha256 = 3; // SHA-256 of field 1, exactly 32 bytes
string canonical_key_or_selector = 4;
bytes value_sha256 = 5; // publish: SHA-256(value); query: empty
bytes nonce = 6; // exactly 12 random bytes
uint64 issued_at_ms = 7;
bytes subject_signature = 8; // Ed25519, exactly 64 bytes
}
subject_signature uses the device signing key bound into the capability. It signs these
bytes, independent of protobuf field order:
ascii("petra-storage-use-v1")
|| u8(action)
|| capability_sha256[32]
|| u32be(len(utf8(canonical_key_or_selector)))
|| utf8(canonical_key_or_selector)
|| u8(len(value_sha256)) || value_sha256
|| nonce[12]
|| u64be(issued_at_ms)
For a publication, value remains the signed AuthEnvelope; the value digest binds the
authorization to those exact bytes and the canonical key binds it to the transport
address. For a query, the selector string is the exact canonical routed key expression;
allowed selector parameters are normalized and included only when the capability
contract explicitly names them. Relay + Storage verifies retained-publication and query
attachments; a receiving subscriber verifies a non-retained live attachment. Neither
boundary persists or replays the request use proof. For a publication, attachment field 1 must be
byte-identical to the canonical publication capability retained inside the envelope. For
a query, the request is not an AuthEnvelope: it is a QUERY
StorageCapabilityV1 plus fresh StorageUseProofV1 bound to the exact canonical selector,
and it remains ephemeral. A reply is eligible only when its concrete key is contained by
both the request selector and query capability; its original stored envelope must then
pass its own publication authority, signature, purpose, classification, revocation, and
exact key-digest verification. A query capability or proof never substitutes for the
original publisher's authority.
Before each reply, Relay + Storage also requires
Common::decide(stored_envelope.classification,
query_capability.classification_ceiling) == Permit. A missing/malformed stored label or
query ceiling, policy mismatch, or any CMBAC denial suppresses that reply and records a
fail-closed diagnostic without exposing payload content.
Authenticated retained-page completion¶
Relay + Storage returns one canonical RetainedReplayReceiptV2 exactly once as the final
successful reply on the exact requested selector, then closes the stream. This applies to
every page, including an empty one. The receipt commits to the exact selector, parameters,
request authorization bytes, ordered reply sequence, fresh issue time, and selected
Directory-signed ServerToken. The token carries replay_signing_key; the Server signs
with its matching private key from the active atomic credential generation.
A requester keeps the page provisional until Common verifies the receipt against the exact
selected ServerToken and the replies it observed in order. Any error reply, missing,
early, duplicate, malformed, stale, or wrongly signed terminal, reply after the terminal,
selector escape, or different selected ServerToken fails the page. The receipt embeds no
service IdentityToken; a replay result never discloses a reusable Directory API bearer.
The executable positive cross-language bytes for reusable-content and committed-mutation
capability encoding, Directory signatures, envelope key binding, subject proof, and Zenoh attachment are in
storage-authorization-v1.json.
That fixture also pins purpose-3 field ordering and enumerates the required hostile
mutations; Common's executable corpus materializes each checklist item into changed bytes.
Common #195 extends the shared
Rust/TypeScript corpus with canonical bytes for all five
LIVE_PUBLICATION expressions and every typed-grammar/proof failure listed below before
any endpoint adopts the new Common revision.
Envelope-to-key binding¶
Every AuthEnvelope is valid for exactly one canonical opaque publication key and retains
the Directory authority under which it was signed. PETRA adds these fields; field 9
remains the snapshot authority grant defined by #126:
bytes storage_key_digest = 10;
bytes publication_capability = 11; // canonical encoded StorageCapabilityV1, action=PUBLISH
storage_key_digest is exactly:
SHA-256(
ascii("petra-storage-key-v1\0")
|| u32be(len(utf8(canonical_publication_key)))
|| utf8(canonical_publication_key)
)
The digest is exactly 32 bytes. The canonical envelope signing input appends, after the
framed field-9 snapshot grant, storage_key_digest[32] ||
u32be(len(publication_capability)) || publication_capability. It binds the signed value
to the exact transport address and Directory publication authority without exposing
the semantic identifier or reverse-addressing salt.
The publication capability grants its bound subject signing key permission to sign values
under its exact expression, allowed purposes, and classification ceiling during
[issued_at_ms, expires_at_ms). It does not authorize another subject and does not make
HLC or storage arrival order authoritative. Verification requires
capability.issued_at_ms <= envelope.issued_at_ms < capability.expires_at_ms, exact
subject/signing-key equality with the envelope's verified IdentityToken, PUBLISH action,
purpose inclusion, key containment, classification permit, Directory signature, and
current revocation state. A retained capability may have expired by a later catch-up; its
historical publication remains verifiable because it was valid at the signed envelope
time. LIVE_PUBLICATION has no historical mesh authorization mode and is rejected
after its active-use deadline. A trusted application archive may retain the already
verified value under separate application policy, but it cannot use the expired live
grant to republish or reconstruct mesh traffic. New publication and query use require a
currently valid capability and fresh proof.
Publishers compute the digest only after constructing and validating the final canonical key. The publication digest input is always a concrete key and can never contain a wildcard; a query proof may bind an authorized canonical selector. Every returned concrete key must satisfy both request and capability containment, and its envelope digest must independently match that concrete key. A retry may reuse an envelope only on that same key. Semantic fan-out to another key requires a freshly signed envelope and a fresh storage-use proof. Aliasing one signed envelope under multiple keys is invalid.
Relay + Storage recomputes and compares the digest before persistence, when reopening and loading every retained entry after restart, and immediately before returning a value. A mismatch, missing/wrong-length digest, cleartext legacy key, or non-canonical key fails closed and the value is never returned. At those same boundaries it re-verifies the retained publication capability, envelope signature, subject/key binding, purpose, classification, key containment, and current revocation. Consumers perform the same retained publication-authority and reply-key checks. The incoming publication key must match the envelope digest and be contained by the retained Directory capability and fresh use proof; query authorization is checked separately. Tombstones and snapshots follow the same rule and remain bound to their own exact canonical keys and authority.
For non-retained live traffic, no restart or serve boundary exists. The receiving subscriber recomputes the digest against the concrete delivered key and completes every live authorization check before payload decode or application dispatch.
The 1.0 flag-day reset has no verifier for envelopes lacking fields 10–11 and no fallback to semantic payload inspection. Existing production flows do not intentionally reuse one signed envelope under multiple key expressions; introducing such fan-out would require a new explicit wire-contract decision.
When more than one storage can answer, the requester queries every applicable provisioned
copy and disables HLC-based Latest consolidation. It verifies all replies, removes
byte-identical duplicates, then merges by opaque key and signed semantic version,
snapshot cut, and tombstone precedence. Zenoh HLC timestamps are useful transport
ordering metadata; they never confer application authority. HLC consolidation is allowed
only for a family whose contract explicitly makes every candidate byte-identical.
Opaque addressing¶
Operational key expressions must not contain stable cleartext principal, channel, unit, or operation identifiers. Geohash cells, fixed protocol literals, classification labels, timing, sizes, and traffic volume remain observable; this is metadata minimisation, not traffic-flow confidentiality.
PETRA 1.0 uses one framed, domain-separated derivation for every semantic key slot:
opaque_segment = lower_hex(
first_16_bytes(HMAC-SHA256(
opaque_keyspace_salt,
ascii("petra-opaque-v1\0")
|| u16be(len(utf8(domain))) || utf8(domain)
|| u32be(len(utf8(identifier))) || utf8(identifier)
))
)
opaque_keyspace_saltis a fresh 32-byte deployment-static PETRA 1.0 secret. Directory mints it during the 1.0 reset and distributes it only to eligible non-storage endpoints. Relay + Storage never receives it. Salt possession is not per-expression authority; Directory-issued capabilities provide that boundary. The dependent Common/Directory change replacesGroupKeyBundle.invite_keyspace_saltat field 12 withbytes opaque_keyspace_salt; the old name and semantics are not retained.domainis an exact ASCII string from the registry below. Length framing makes every(domain, identifier)pair unambiguous;lenis a byte length, not a character count.- Output is exactly 32 lowercase hexadecimal characters. Empty identifiers and salts not exactly 32 bytes fail closed.
- The salt is independent of content-encryption keys and remains unchanged across ordinary group-key rotation. Later salt rotation is a flag-day re-addressing operation, not a dual-read migration.
- A uniformly random identifier may remain clear only when it carries no principal, channel, unit, operation, or human-assigned semantics. Otherwise each key slot uses its registered domain before it enters the expression.
The context registry deliberately separates linkability between planes. Definition and tombstone forms of the same object reuse a context so a client can merge them; unrelated planes use different contexts even when their clear identifier happens to be equal.
| Family or slot | Registered domain |
|---|---|
| deployment audience | audience-deployment |
| auth subject | auth-principal |
| operation | operation-id |
| position / heartbeat subject | position-principal / heartbeat-principal |
| drawing / target object | drawing-id / target-id |
| chat channel / message | chat-channel / chat-message |
| channel definition, membership, transfer, tombstone | channel-id |
| channel-member liveliness subject | channel-member-principal |
| invitee / invited channel | invite-invitee / invite-channel |
| voice channel / subject / session | voice-channel / voice-principal / voice-session |
| command target / command / live command-acknowledgement source | command-target / command-id / ack-source |
| plan unit / plan | record-plan-unit / record-plan-id |
| plan-ack version / plan-scoped acknowledger | plan-ack-version / plan-acknowledger |
| SitRep unit / report | record-report-unit / record-report-id |
| requirement unit / requirement | record-requirement-unit / record-requirement-id |
| ORBAT unit | record-orbat-unit |
| router data-plane subject | router-principal |
| snapshot scope / audience | snapshot-scope / snapshot-audience |
Registered domains are unique and exhaustive; length framing means they need not be
prefix-free. New families register contexts here and in the shared corpus before
implementation. The
generic state/<class>/<key> namespace is not an escape hatch: it cannot carry durable
PETRA 1.0 data until its class, opaque contexts, envelope purpose, and recovery behavior
are registered.
The derivation and rejection corpus is
opaque-addressing-v1.json. Common
owns the executable cross-language copy when the generalized helper lands; Rust,
TypeScript, Kotlin/JNI, and all consumers must run the same corpus rather than reproduce
the algorithm independently.
1.0 addressing reset. This is an intentional flag-day wire and storage cutover. Before runout, operators erase retained development/test keyspaces and endpoint caches, deploy the new contract across every component, and re-enrol devices to receive the new salt and capabilities. Implementations must not read, write, probe, translate, or retain either cleartext legacy addresses or addresses produced by the old unframed invite derivation. There is no dual-read window, migration sweep, or retained-address compatibility promise.
Transport peers still know their own and directly connected peer certificate identities.
Opaque router-principal data-plane segments prevent that transport identity from being
published as a globally searchable operational key; they do not pretend the mTLS peer
relationship itself is anonymous.
Retention and recovery¶
One deployment policy sets the offline horizon across Relay + Storage retention, endpoint outboxes, and group-key backfill. The PETRA 1.0 default is seven days. For the demo rig, the release lock renders the same value into every component and records the effective value in runout evidence; startup and CI must reject incompatible values rather than silently shortening recovery.
The lossless claim is bounded. It applies only while all of these are true:
- traffic remains inside the declared maximum retained bytes and values, maximum value size, sustained/peak ingress, reconnect concurrency, and resource envelope;
- the endpoint's local durable outbox survives until its existing
Senttransition; - at least one provisioned storage copy that accepted the traffic survives; and
- the endpoint returns before expiry with valid cached identity, keys, revocation state, and capabilities.
No universal hardware capacity is claimed. Each release/runout records the horizon,
limits above, replica count, storage placement, disk reserve, and measured CPU, memory,
battery, storage, and recovery time. Outside that envelope, PETRA reports degradation
rather than claiming lossless replay. Sent means the upstream Zenoh put returned
success; it is not a storage or end-to-end acknowledgement. Endpoints cannot infer retained
availability from it, and PETRA 1.0 adds no receipt/HWM protocol. The lossless claim is
therefore conditional on verified persistence by a surviving Storage copy, as stated
above.
Sealed endpoint-cache rollback boundary¶
An endpoint seals cached identity, Directory trust, revocation, capability, and group-key state at rest. The seal provides confidentiality and integrity under the endpoint's wrapping key: altered bytes, a changed authentication tag, or ciphertext copied from a different wrapping-key context fail closed. It does not provide freshness or rollback resistance. An older byte-identical cache image produced under the same wrapping key still has a valid seal.
PETRA 1.0 accepts that older-valid image only as a bounded same-session endpoint-capture
residual. An honest endpoint may load it only before the original signed identity,
capability, configured offline-horizon, and any applicable signed absolute key deadline;
while the Directory key remains trusted, the subject signing key remains bound and
unrevoked, and each cached group-key epoch remains inside the unchanged Directory-served
epoch/backfill range. No independent trust-key, subject-key, or backfill-entry TTL is
invented. Here, same-session means the same authenticated subject and wrapping-key lineage;
logout, authentication expiry or rejection, reassignment, re-enrolment, or wrapping-key
destruction ends it. Loading or copying the image never rebases its issued_at_ms, expiry,
group-key range, or horizon deadline; the earliest applicable original absolute deadline
continues to win. A known subject/signing-key revocation still fails immediately. If the
endpoint can detect that wall time is earlier than an authenticated issue time or than an
in-memory same-process time floor, it treats that as clock rollback or an issue-time
violation and blocks cached authority until a connected refresh succeeds.
Rollback of one endpoint never rolls back a peer. Every receiver applies its own current revocation snapshot and rejects a sender that it already knows is revoked, even when the sender restored an older locally valid cache. Once Directory is reachable, the endpoint refreshes trusted keys and revocation, verifies a complete signed capability response, and atomically replaces the old set before shared traffic, snapshot publication, or content outbox drainage. Omission supersedes an older grant before that grant's original expiry.
Android binds its sealed operational and authority cache to the current user session. Every session-wipe path destroys the session wrapping key and clears its in-memory copy before deleting the ciphertext. Restoring prior-session cache bytes after logout, expiry, rejection, fresh login, or reassignment therefore produces undecryptable bytes, not a prior operator's valid cache. Device/bootstrap configuration remains outside that session key.
Restoring an old group-key bundle does not provide a new epoch. Honest consumers enforce the original epoch/backfill and horizon bounds; an old key can open only ciphertext that was already sealed under an epoch it contains. Rotation is not retroactive: an attacker who extracted an old group key can continue to decrypt captured ciphertext from that old epoch, but cannot decrypt content sealed under a later epoch or fetch that epoch once revoked.
PETRA 1.0 makes no rollback-resistance claim against an attacker who controls both endpoint storage and the wall clock. Such an attacker can restore a mutually consistent old cache and report a time before its unchanged absolute deadlines. A local hash chain, duplicated cache, software high-water/monotonic file, or sequence sidecar can be restored with the cache and must not be added as a purported security boundary. Stronger protection requires a trusted hardware monotonic counter or an online freshness authority and is outside the 1.0 contract.
Domain deletion is a signed retained tombstone written with put. Zenoh delete is not
used for application deletion. Authoritative snapshots are also signed retained put
values. Signed semantic sequence/version, snapshot cut, and tombstone rules determine
precedence; transport arrival time and HLC do not resurrect older state.
The authoritative snapshot contract owns the complete, exactly-one-class family inventory plus deterministic cut, rollback, and anti-resurrection rules. Web can retain class-3 history for a tactical class-1 or class-2 family, but it never republishes that history as endpoint-authored traffic.
Supported and degraded topologies¶
flowchart LR
D["Directory<br/>authority"]
W["Web<br/>authority + audit"]
S1["Relay + Storage A"]
S2["Relay + Storage B"]
E["Tactical endpoints"]
D -. "signed authority" .-> W
D -. "signed authority" .-> E
E <--> S1
E <--> S2
S1 <--> S2
W <--> S1
| State | Required behavior |
|---|---|
| Connected | Directory and Web are reachable. Live pub/sub and query catch-up operate; authority and snapshots refresh normally. |
| Core disconnected | A tactical partition continues existing authorized channels within the horizon using valid cached authority, including non-retained live publication under the closed registry. It cannot mint identity, capabilities, channel-membership changes, revocations, or snapshots. An unknown remote removal remains effective only after refresh or signed expiry; a known subject/key revocation fails immediately. A newly created offline channel remains a visibly unactivated local draft and carries no shared traffic. |
| Alternate relay/storage | Endpoints use a statically configured surviving path. There is no election or promotion. Query duplicates are verified and merged by semantic rules. |
| Storage unavailable | Permitted non-retained live peer pub/sub may continue, but catch-up and delayed delivery are unavailable. Eligible retained content remains in its durable outbox; non-retained LIVE_PUBLICATION is never queued or replayed. A successful Sent transition is not proof of storage. The operator sees the degraded state. |
| Relay path restored within horizon | Reconnect is automatic and uses cached unexpired authority while Core remains unreachable. Authorized catch-up resumes without a control-plane round trip; no authority changes are possible. |
| Core restored within horizon | Endpoints refresh Directory trust/revocation/ledger/capability state and atomically replace cached grants before shared operation resumes. Only then do authorized outboxes drain idempotently; query-all catch-up merges verified values and tombstones without resurrection. |
| Restored after horizon | Transient history is visibly incomplete. Declared durable models recover through a verified Web-signed snapshot under #126; Web never impersonates an endpoint. |
Threat model¶
| Threat | Required containment |
|---|---|
| Stolen capability | Capability bytes lack the subject's signing key; fresh proof is required, validity is bounded, and revocation is rechecked on publish and query. |
| Offline authority change | Directory is the only issuer. New channels, invitations, ownership transfers, and other grants fail explicitly while Directory is unreachable; local drafts cannot publish, query, or masquerade as activated channels. |
| Stale cached membership | Reconnect refreshes exact capabilities and reconciles Directory membership before shared operation resumes. Expired, removed, or revoked authority fails closed. |
| Older valid sealed endpoint cache | The seal detects tamper but not restoration of authentic older bytes. Original signed token/capability/configured-horizon and any applicable signed absolute key deadline, current trust/binding/revocation, the unchanged cached group-key range, session-key destruction, independently newer peer revocation, and refresh-before-traffic bound the residual; PETRA 1.0 does not claim resistance when the attacker controls both storage and wall time. |
| Capability replay or selector widening | Nonce/timestamp replay gate plus exact canonical-expression containment; unknown/duplicate parameters and replies outside selector are rejected. |
| Revoked or captured endpoint | Known subject/signing-key revocation fails closed. A disconnected partition cannot know a remote removal until refresh, so a malicious holder may exercise an otherwise valid cached grant until its signed effective deadline. Atomic full-set replacement removes omitted grants before shared operation/outbox drain on Core reconnect. Previously cached ciphertext and old key epochs remain bounded capture residuals. |
| Modified live subscriber with a deployment group key | First-party code declares only relationship-derived exact opaque scopes and verifies every LIVE value before decode, but stock Zenoh has no PETRA capability-aware subscription-admission boundary. A transport-reachable current-epoch key holder can widen its local live subscription and decrypt observed ciphertext, including after app-layer revocation but before rotation. It cannot forge an accepted publication; durable queries remain Relay-enforced. Revocation plus group-key rotation cuts it off from newly sealed epochs, without erasing traffic or epochs already observed. |
| Traffic analysis | Opaque per-domain segments remove stable semantic identifiers, but timing, cell, size, repetition, and domain literals remain observable and must be treated as residual metadata. |
| Compromised disposable storage | It can copy, drop, delay, duplicate, or roll back opaque bytes, but cannot decrypt, forge signatures, mint authority, reverse addresses, or make HLC authoritative. Consumers detect invalid and semantically stale replies. Availability attacks remain possible. |
| Salt compromise | The attacker can dictionary-map address segments. Content remains encrypted and signatures remain unforgeable, but the deployment requires an explicit re-addressing migration; ordinary key rotation is insufficient. |
| Malformed key or selector | Canonical parser, fixed domain registry, size bound, selector containment, and reply-key checks reject injection and wildcard expansion before storage access. |
Required contract and runout tests¶
Common supplies executable Rust/TypeScript positive and hostile vectors; every producer and consumer adds integration coverage for its registered families. The release-candidate runout records these exact outcomes:
- each of the five
LIVE_PUBLICATIONexpressions accepts a canonical concrete key and rejects**,$*, a wildcard in the wrong slot, missing/extra segments, aliases, malformed cell/opaque segments, generic containment, and family crossing; - a position and heartbeat source moves across cells during Core disconnection while its exact opaque source remains fixed; substituting another source fails before decode;
- voice cannot change channel/source, command cannot change target, and acknowledgement cannot change source; fresh session/command ids work only in their one declared slot;
- a moved envelope, changed complete-envelope digest, capability/envelope attachment mismatch, stolen capability, proof signed by another key, stale proof, and replayed nonce all fail closed;
- capability refresh vectors execute canonical signed empty and mixed sets, exact token bytes, full 32-byte Directory key id, echoed nonce, subject/key binding, and exact once-sealed committed ciphertext; token/timestamp/key/signature tamper, cross-protocol challenge use, replayed or wrongly echoed challenge, forged omission, malformed/noncanonical protobuf including unknown or duplicate scalar fields, reordered or semantically duplicate entries, stale/equal generation, and generation overflow all fail closed;
- capability refresh accepts exactly 4,096 entries, a 16,777,216-byte encoded response, and a 1,048,576-byte committed value, while rejecting each limit plus one and every checked-arithmetic overflow before allocation; an entitlement mutation that would make the full set exceed a bound rolls back rather than truncating the later response;
- operation-context vectors accept canonical signed zero, one, and multiple-context sets; prove bytewise UTF-8 context ordering, numeric CHAT/VOICE kind ordering, exact kind selection, and a required empty-set digest; reject empty/duplicate/noncanonical context ids, empty/duplicate/unsorted/unknown kinds, reordered contexts, context-digest/signature tamper, stale/equal generation, token/ProfileBundle/ORBAT/user-authored substitutions, and a caller-selected kind absent from its context. Higher-generation replacement removes an omitted context and clears its selection atomically with capability omission; offline, expiry, revocation, failed refresh, or removal leaves only an inert draft, and a Directory receipt refreshes before activation while commit revalidation defeats a relationship/capability change after UI selection;
- a verified signed empty capability set atomically clears the cache; transport, decode, signature, canonicalization, bound, or entry-validation failure preserves the prior cache unchanged, retains degraded status, and blocks shared traffic and outbox drain;
- a complete channel membership join/kick/role change publishes one higher-version once-sealed full set and updates the subject entitlement index in the same transaction; concurrent mutation, retry, and rollback expose neither a lost member nor a torn set, and removal is absent from the next verified refresh;
- an endpoint assigned 256 canonical geohash-5 cells receives only the corresponding exact drawing/target grants allowed by its effective actions; 257, a noncanonical or unassigned cell, and any retained moving-cell wildcard fail closed;
- a connected authority mutation binds the exact actor token/key, challenge, time, request id, kind, aggregate scope, and typed semantic body; actor substitution, conflicting retry, stale current role/appointment, machine-only mutation, caller-supplied opaque/committed authority, and partial ORBAT batch commit fail closed;
- a Web core-machine relationship produces only its exact operation/cell/family/audience grants; wrong platform/key/scope/cell, relationship removal, committed-value resealing, snapshot-audience confusion, browser delegation, and machine impersonation fail closed;
- a FIDO-bound browser set is tied to the current credential, human principal, token and session signing key, expires within one hour, grants heartbeat but not position, and contains no Web-core, committed-publication, plan/requirement-publication, or snapshot authority; bearer-only sessions, wrong-key/replayed proofs, removed credentials, and unsigned member liveliness fail closed;
- every purpose-3
CapabilitySetEntryV1carries its matching verified snapshot grant in field 5; missing/orphan/mismatched grants fail the whole refresh and full-set omission removes the grant and publication capability atomically; - entitlement-matrix tests prove both gates independently: role without relationship and relationship without the required action grant nothing; an observer/member is read-only, a non-member cannot publish chat or voice, an invite cannot escape its exact invite and minimum definition, and admin override never turns a reusable grant into an authority mutation;
- COP tests preserve sender-owned drawing mutation versus the
manage_annotationscross-owner override, separatemanage_targetsfromnominate_target, and reject target publication outside an assigned exact cell; - ORBAT tests use Common's
visible_unit_idsclosure, reject a command target outside the subject's current operation scope, distinguish camera from movement at the decoded Node command gate, remove report publication when the exact unit appointment is removed, and preserve a cached valid DDIL grant only until its signed expiry; - capability purpose/action/mode, classification, identity/signing-key binding, known revocation, and every effective expiry boundary fail closed at the earliest applicable deadline; vectors pin capability expiry first, identity expiry first, horizon first, exact-boundary rejection, and checked-add overflow;
- non-retained live samples are absent after Relay + Storage query/restart and cannot
enter a transport outbox; retained
LIVEchat still persists and catches up only underREUSABLE_CONTENT, while durable plan acknowledgements useDURABLE_CONTENT, retry the original outbox envelope, remain retained only within the horizon, and enter Web's verified application archive; - the native Relay denies disallowed peers/actions/keys without claiming to inspect live capability bytes, while a receiving subscriber rejects an invalid capability/proof before any payload decoder or application handler runs; Android voice receives and verifies the original subject envelope/capability/proof, with no Server re-wrap or device-signature bypass;
- the automatic Web recorder receives only the registered all-cell position and closed aggregate retained-query selectors; manual Web relationships receive chat and voice selectors only for exact channels, position only for exact cells, and plan acknowledgements only for an exact related plan version. Unregistered families, caller-declared AOIs, cross-operation scope, heartbeat, sensor, command, live command acknowledgement, invite, and membership recorder grants are absent;
- Web atomically rebuilds exact chat/voice/position/plan-ack subscriptions when a verified complete generation adds or omits an entry, stops all affected ingestion at the earliest absolute deadline, and preserves a previous generation after refresh failure only while its original deadlines remain valid;
- exact retained chat, exact-version durable plan acknowledgements, and live voice/position samples continue to populate verified Web replay rows, while the deleted invite/membership recorder has no startup declaration, compatibility fallback, or broad selector; connected mutation receipts and Directory audit events still populate the membership activity/AAR timeline;
- first-party live subscribers declare only registered relationship-derived opaque
scopes: one exact channel or field-cell scope, Web's closed all-cell position scope, one
exact command target, and one exact acknowledgement device source with only its
command-identifier slot variable. Source
changes, sibling channel/cell
or target scopes,
waypoint/**, global family selectors,waypoint/global/ack/**, and every standalone sensor selector are absent or rejected by the first-party adapters; - a runout-only modified authenticated endpoint widens a stock-Zenoh live subscription
while it still holds the deployment group key and demonstrates the accepted residual:
it can observe and decrypt matching LIVE ciphertext but cannot forge a value accepted
by an honest receiver, use the live path to enter Relay + Storage, or turn the widened
selector into a retained
QUERYwithout a valid capability and fresh exact proof; - after that endpoint's revocation is known but before the automatically triggered, coalesced group-key rotation completes, prove its existing transport path is not claimed to close: the modified client can still decrypt an observed current-epoch LIVE sample, while honest receivers reject its new publications;
- after the rotation completes, prove the revoked endpoint cannot obtain or decrypt a newly sealed LIVE sample under the new epoch. The evidence does not claim that app-layer revocation, capability omission, membership removal, or native Relay subscription teardown alone withdraws already observed ciphertext or an old epoch;
- a query request is a
QUERYcapability plus exact-selector proof, never anAuthEnvelope; an out-of-selector/out-of-capability reply or an invalid original stored envelope is rejected; - two external tracks use the Gateway's one opaque source, remain distinct by the signed
and sealed
(verified Gateway principal, origin_uid)identity, and expose noorigin_uidin keys or logs; Android and Web independently prove tuple-based key/dedup/expiry/render behavior, and a non-Gateway publisher with non-emptyorigin_uidfails closed; empty, 256-byte boundary, over-limit, Unicode-control, forbidden-character, case-preservation, and code-point-preservation cases execute; - standalone sensor publications/deletions produce no accepted sample or duplicate outbox/notification; sensor map objects and removal use drawing values/tombstones, while Node live camera metadata remains interoperable;
- ORBAT membership and appointment mutations each republish a complete unit record and
preserve every unchanged definition, parent, membership, commander, origin/provisional,
classification, ownership, and lifecycle field; retirement is a complete
retired=truerecord at the next sequence, and a delayed pre-retirement record cannot recreate the unit; - lower ORBAT unit sequences, equal sequence with different bytes, and a capability whose
authority_ledger_versiondiffers fromOrbatRecord.seqfail closed; equal sequence with identical bytes is idempotent, and concurrent Directory mutations cannot lose an accepted membership or appointment update; - live/retained catch-up and authoritative snapshot reconciliation apply the same atomic per-unit key/sequence rule; a multi-unit Directory transaction is all-or-nothing while each affected unit independently advances exactly once; and
- while Core is absent, an otherwise valid cached grant whose remote removal is unknown continues only until signed expiry; known subject/key revocation fails immediately. On reconnect before expiry, revocation refresh plus atomic full-set replacement removes an omitted grant before shared operation or outbox drain. The evidence records the bounded malicious-holder window as residual risk rather than claiming instant disconnected removal;
- tampering with any sealed endpoint-cache ciphertext, authentication tag, or wrapping-key context fails closed without changing the last accepted cache or releasing shared traffic/outbox work;
- restoring an authentic older cache image under the same session wrapping key before its original signed token, capability, configured horizon, and any applicable signed absolute key deadline is accepted as the documented bounded endpoint-capture residual only while its Directory key remains trusted, its subject key remains bound and unrevoked, and its cached group-key epochs remain inside the unchanged served range;
- that same older image fails closed at and after the earliest original absolute deadline; restart or restore does not extend any deadline;
- a detectable wall-clock rollback or authenticated issue-time violation blocks cached authority until connected refresh;
- a peer holding a newer revocation snapshot rejects the restored endpoint independently, even while the endpoint's older local cache remains inside its otherwise valid window;
- on Directory reconnect, refreshed revocation and the verified complete capability set supersede the restored cache before shared traffic, snapshot publication, or content outbox drainage;
- after every Android session-wipe trigger, destruction of the prior session wrapping key makes restored prior-session cache bytes undecryptable while device/bootstrap settings remain available; and
- restoring an old group-key bundle creates no later epoch and never expands or rebases its cached range or overall horizon. Ciphertext from an already held old epoch still opens, ciphertext from a later missing epoch does not, and a revoked endpoint cannot fetch that later epoch.
Non-goals for 1.0¶
- Dynamic relay election or storage promotion.
- Endpoint-issued or owner-delegated publish, query, or channel-membership authority; changing this permanent trust invariant requires an explicit product and threat-model decision.
- Activating or silently queueing a shared channel-authority change while Directory is unreachable.
- Consensus or a global total order.
- Unlimited offline retention or a lossless claim outside the measured envelope.
- Server-side application membership, ownership, ORBAT, or drawing interpretation.
- Complete transient history after the configured horizon.
- A standalone or compatibility sensor publication plane separate from drawings.
- Rollback resistance against an attacker controlling both endpoint storage and wall time, or a local hash-chain, duplicate-file, or software-monotonic substitute for trusted time.
- Hiding traffic timing, volume, geohash cells, or directly connected transport-peer identity.
- Audience-confidential live subscription admission against a modified authenticated deployment-group-key holder, or immediate remote withdrawal of an already declared stock-Zenoh subscription.
- A PETRA subscription-lease protocol, capability-aware Relay plugin, payload interception, or Zenoh fork. Audience-specific encryption and any upstream admission hook are post-1.0 security work, not hidden release dependencies.