Protocol Specification v0.6

Validation

Validation connects original bytes to one authorized product action. Canonical parsing, signature verification and product authorization answer different questions. An implementation must preserve their documented refusal rules and recheck the current authority where the effect becomes visible.

Admission order

Checks run in one fixed order with exact refusal codes, and only a frame that passes them all updates the binding.

Checks run in one fixed order with exact refusal codes, and only a frame that passes them all updates the binding. A received frame passes nine checks in order: declared version, canonical bytes, selected profile, envelope scope, issue time and TTL, signature, the host's source identity check, operand decoding against the Core and the transport binding. A failed check stops the frame with the code written in its box: a wire error from 70 to 76, or the delivery status TTL_EXPIRED (54) for a passed expiry. Every refusal up to the signature check comes before the frame's own signature is verified. Only a frame that passes every check is bound under its key, as First or as an Identical retry; a refused frame leaves the binding untouched. Signature not yet verified 6 Declared version first bytes only, within the size limit INVALID_FORMAT (70) over the size limit UNSUPPORTED_VERSION (71) another version 1 Canonical bytes fixed field order; re-encoding is equal INVALID_FORMAT (70) Selected profile family and atomicity in the profile UNSUPPORTED_VERSION (71) 2 Envelope scope matches this channel, key and epoch INVALID_SOURCE (72) 3 Issue time and TTL allowed skew; asset TTL floor applies TTL_INVALID (75) too far ahead or overflow TTL_EXPIRED (54) delivery status: expired Signature THRESHOLD refused; strict Ed25519 INVALID_THRESHOLD_SIGNATURE (74) INVALID_SIGNATURE (73) 4 Source identity host check of the admitted Core INVALID_SOURCE (72) Operands decoded and checked against the Core INVALID_FORMAT (70) malformed INVALID_SOURCE (72) wrong endpoint Transport binding deployment, source, principal, UUID REPLAY_CONFLICT (76) changed Core 5 Admitted frame First, or Identical for an exact retry Refused with its code the binding is not touched
  1. 1

    Version before decoding

    The declared version is read from the first bytes, so a frame that declares another version gets UNSUPPORTED_VERSION whatever follows.

  2. 2

    Profile before source

    A message family or atomicity outside the negotiated profile is refused before any source or signature check.

  3. 3

    Scope and time

    The Core must carry the expected deployment, domains, principal, key, epoch and route scope. The TTL is at least the asset floor, and the expiry second itself is still accepted.

  4. 4

    Signature

    THRESHOLD has no verifier and is refused first. Otherwise the scheme must be in the profile and Ed25519 must verify the signing input.

  5. 5

    Binding last

    The same Core bytes under the same key are an Identical retry. Different bytes under that key get REPLAY_CONFLICT.

  6. 6

    Unsigned refusals

    Every refusal in the shaded band comes before the frame's own signature is verified, so a receiver answers it only with an unsigned error.

  • Path of an admitted frame
  • Refusal with the code in its box
  • Signature not yet verified
  • Check supplied by the host

Schematic: the transport receive of both references, Rust receive and Go Receive, in their shared order. Go ReceiveAccepting adds a configured operation check before the profile step, and the authority entry path has an order of its own.

Figure 1: Transport admission checks in their fixed order, with the refusal codes each check returns

Version check and unknown fields#

NormativeBytes, test vectors and proofs

  • On a version 6 channel, a receiver MUST read the declared version of a frame within its size limit before canonical decoding of the rest of the frame. The declared version is the varint value of Core field 1 when the frame starts with Packet field 1 and the embedded Core starts with field 1.
  • A frame that declares a version other than 6 MUST receive UNSUPPORTED_VERSION, whatever fields follow. A frame that declares no version continues to the canonical checks. This check does not precede checks of the installed channel or host configuration.
  • The position and encoding of Packet field 1, Core field 1 and its varint form the version selector of the canonical protobuf format. The reference version 5 compatibility path uses its configured JSON record decoder without this protobuf version selector; a malformed compatibility record receives INVALID_FORMAT.
  • Within a version, a receiver MUST reject an unknown field, an unknown wire type or an out-of-order field as INVALID_FORMAT. A new field can enter the protocol only through a new version that both peers negotiate.
  • After canonical decoding, an operation outside the selected version profile MUST receive UNSUPPORTED_VERSION before its source, signature or authority is checked. The selected version profile is the negotiated version together with the operations that the receiver's configuration accepts.
  • The reference configuration accepts 25 of the 31 operations, listed in the "Reference accepts" column of operations (opens in a new tab). Rust checks the configured operation set in the authority entry paths, including response generation and exact Debit resolution.
  • The generic Rust transport decoder has no configuration-specific operation set. Go exposes the configured check through ReceiveAccepting; its generic Receive path does not select that set.

Both references read the declared version from the first bytes, within the size limit, before the canonical decoder runs:

Declared versionsrc/wire/codec.rsL353-L365Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// The version check that runs in front of the canonical decoder and the/// admission gate. A frame within the size limit that declares a version/// other than 6 receives UnsupportedVersion, whatever fields follow. Every/// other frame continues to the canonical checks unchanged.pub fn check_declared_version(bytes: &[u8], max_frame: usize) -> Result<()> {    if bytes.len() > max_frame {        return Err(BAD);    }    match declared_version(bytes) {        Some(version) if version != 6 => Err(WireError::UnsupportedVersion),        _ => Ok(()),    }}

In transport admission (opens in a new tab), both references refuse a message family (opens in a new tab) or atomicity (opens in a new tab) outside the negotiated profile before they check the source or the signature:

Selected profilesrc/wire/admission.rsL35-L53Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub(super) fn check_versioned_scope(    core: &Core,    profile: &Profile,    boundary: &Boundary,    version: u64,) -> Result<()> {    if profile.version != version        || core.version != Some(version)        || boundary.version_floor > version        || !body(core).is_ok_and(|(family, op)| {            op.opcode                .is_some_and(|opcode| profile.allows(family, opcode))        })        || !core            .atomicity            .is_some_and(|mode| profile.atomicities.contains(&mode))    {        return Err(WireError::UnsupportedVersion);    }

The reference authority applies its configured operation set right after canonical decoding:

RustConfigured operation setsrc/authority/recovery/wire.rsL193-L204Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Refuse an operation outside the selected version profile of this/// configuration. The check reads only the decoded frame.fn check_operation<S: WireSecurity>(    security: &S,    channel: &Channel,    core: &Core,) -> Result<(), WireError> {    if !security.accepts_operation(channel, core) {        return Err(WireError::UnsupportedVersion);    }    Ok(())}

The reference configuration accepts 25 of the 31 operations:

Reference operation setsrc/authority/recovery/wire/ed25519.rsL455-L465Source (opens the lines on GitHub in a new tab)
fn supported_frame(core: &Core) -> bool {    match core.body.as_ref() {        Some(Body::StateSync(op)) => matches!(op.opcode, Some(1..=6)),        Some(Body::RegulatoryAction(op)) => matches!(op.opcode, Some(1..=3)),        Some(Body::Ack(op)) => op.opcode == Some(1),        Some(Body::Query(op)) => matches!(op.opcode, Some(1..=8)),        Some(Body::LockManagement(op)) => matches!(op.opcode, Some(1..=6)),        Some(Body::Governance(op)) => op.opcode == Some(1),        _ => false,    }}

Admission, replies and retry#

NormativeBytes, test vectors and proofs

  • Apply the trusted asset-class minimum TTL (opens in a new tab) when it exceeds the supplied TTL. Accept the inclusive expiry cut and reject expiration, disallowed future skew, arithmetic overflow and obsolete authorization before state effects. The skew allowed for an issued time ahead of the receiver clock is a configuration value. Delivery TTL neither ends a lock nor establishes a source outcome.
  • The reference persistent authority MAY configure a maximum signed delivery TTL. A frame whose signed TTL exceeds it MUST receive TTL_INVALID after the signature check and before operand decoding, any transport binding or effect. The default is no limit. In the reference configuration the value is max_delivery_ttl_seconds, and an absent value sets no limit. This authority check is not part of the generic Rust or Go transport admission API. This maximum applies to the persistent-authority delivery receive path. Exact authorized Debit resolution does not repeat that maximum check; it revalidates the original using its issued time as the admission time.
  • Canonical format, version, domain, current key and epoch (opens in a new tab), signature and separate source identity checks MUST precede any transport deduplication update. Key that update by authenticated origin and complete UUID. Compare the retained exact signed core for the same key and reject a changed request. Transport deduplication cannot replace caller-bound durable results or original-event (opens in a new tab) consumption.
  • A verified request MUST enter the same current product authority and execution path as the matching product action. A raw caller-supplied request cannot replace the validated body. Maintain normal, administrative, force-inclusion (opens in a new tab), upgrade and adapter authorization at the final consumer.
  • The execution credentials that accompany a reference delivery (session token, signature credential, maintenance token, owner endorsement and handover consents) come from host adapters. They are not wire fields. Debit result recovery is an API of the reference authority host that takes the original signed Debit. It is not a wire operation.
  • A state-changing request MUST carry the global revision and authority epoch of its signed cut (opens in a new tab), and the authority MUST execute it only when they equal the current cut. When another write commits in between, the request receives Busy without effect.
  • A recorded outcome consumes its call identifier (opens in a new tab), including a recorded Busy or Unavailable. Sending the same request again returns the recorded outcome. Executing the work again requires a new signed cut, a new call sequence number, a new transport UUID and a new signature.
  • In the ordered v0.6 reference configuration, the signed delivery TTL bounds admission. The service deadline bounds how long the caller waits. A wait timeout leaves the result unresolved until an authorized read confirms it.
  • Publication MUST compare the exact observed control state, including the authenticated head, writer generation, revocation and cancellation, in the same authority transaction that selects the flushed record. A prior authority-ordered revocation or cancellation prevents that publication. Publication can occur after the delivery TTL or wait deadline.
  • An expired packet remains ineligible for new delivery.
  • Debit result recovery authenticates the current caller and binds the original caller, call identifier and complete canonical signed request. An exact authorized query can recover a published receipt after expiry.
  • A durable rejection of an unresolved original uses the same authority transaction as publication and loses if the effect has already been published. Once an original Debit is known to the authority, another call identifier for the same origin and generation can proceed only after the authorized original is durably rejected.
  • A configured strict publication provider separately enforces its time and cancellation conditions at its durable visibility cut. The reference ordered configuration requires an explicit authority binding and does not select the strict or legacy path as a fallback.
  • Generate Rust and Go bindings from one pinned schema and regenerate them without edits. Both compiled consumers MUST use the same canonical bytes, semantic values, signing inputs and refusal vectors for the wire and admission functions they implement. Keep format, authorization, version, expiry and conflict errors distinct.
  • The request-admission disclosure described in constraints of this version (opens in a new tab) remains a limitation of the reference profile (opens in a new tab).

Rust receive and Go Receive run the same checks in the same order and touch the transport binding last. Go ReceiveAccepting is that path with a configured operation check in front of the admission gate, and Receive passes it no check:

Transport admissionsrc/wire/admission.rsL164-L187Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn receive(    &mut self,    bytes: &[u8],    profile: &Profile,    boundary: &Boundary,    attest: impl FnOnce(&Core) -> bool,) -> Result<(Arrival, CanonicalPacket)> {    if profile.version != 6 {        return Err(WireError::UnsupportedVersion);    }    check_declared_version(bytes, boundary.max_frame)?;    let packet = decode_packet(bytes, boundary.max_frame)?;    admit_packet(&packet, profile, boundary)?;    if !attest(packet.core()) {        return Err(WireError::InvalidSource);    }    super::operands::decode(packet.core(), boundary.max_nesting)?;    let key = TransportKey::for_core(packet.core())?;    let arrival = compare_binding(self.0.get(&key), packet.core_bytes())?;    if arrival == Arrival::First {        self.0.insert(key, packet.core_bytes().to_vec());    }    Ok((arrival, packet))}

The time checks raise the TTL to the asset floor, refuse arithmetic overflow and an issue time beyond the allowed skew, and still accept the expiry second itself:

Issue time and expirysrc/wire/admission.rsL66-L85Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
    let sent = core.issued_seconds.ok_or(WireError::InvalidFormat)?;    let expiry = wire_expiry(core, boundary)?;    let latest = boundary        .now        .checked_add(boundary.future_skew)        .ok_or(WireError::TtlInvalid)?;    if sent > latest {        return Err(WireError::TtlInvalid);    }    if expiry < boundary.now {        return Err(WireError::TtlExpired);    }    Ok(())}pub(crate) fn wire_expiry(core: &Core, boundary: &Boundary) -> Result<u64> {    let sent = core.issued_seconds.ok_or(WireError::InvalidFormat)?;    let ttl = core.ttl_seconds.ok_or(WireError::InvalidFormat)?.max(boundary.asset_ttl_floor);    sent.checked_add(ttl).ok_or(WireError::TtlInvalid)}

The persistent authority applies its optional ceiling on the signed TTL:

RustDelivery TTL ceilingsrc/authority/recovery/wire.rsL206-L217Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Refuse a signed delivery TTL above the configured limit.fn check_delivery_ttl<S: WireSecurity>(    security: &S,    channel: &Channel,    core: &Core,) -> Result<(), WireError> {    let ttl = required(core.ttl_seconds)?;    if security.max_delivery_ttl(channel).is_some_and(|limit| ttl > limit) {        return Err(WireError::TtlInvalid);    }    Ok(())}

The binding key is the deployment, source domain, principal and UUID. A later arrival under the same key must carry the same Core bytes:

Retained bindingsrc/wire/admission.rsL145-L151Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn compare_binding(retained: Option<&Vec<u8>>, exact: &[u8]) -> Result<Arrival> {    match retained {        None => Ok(Arrival::First),        Some(bytes) if bytes == exact => Ok(Arrival::Identical),        Some(_) => Err(WireError::ReplayConflict),    }}

A repeated call collects its recorded outcome. A different payload under the same call identifier is rejected, and an outcome that is not ready yet waits:

RustRetained outcomesrc/authority/retained_completion.rsL19-L30Source (opens the lines on GitHub in a new tab)
pub(crate) fn retained_completion<T: Copy>(stored: Option<(bool, bool, T)>) -> (Reply<T>, bool) {    let Some((matches, ready, outcome)) = stored else {        return (Reply::Waiting, false);    };    if !matches {        (Reply::Rejected, false)    } else if ready {        (Reply::Collected(outcome), true)    } else {        (Reply::Waiting, false)    }}

Refusals before signature verification#

NormativeBytes, test vectors and proofs

  • A receiver MUST NOT sign a response to a frame whose own signature it has not verified. A refusal before that verification is an unsigned error.
  • The Rust reference response-generation path checks the original's signature with the peer key of the negotiated channel before it signs anything. Checks of the original before that verification read only the frame and the channel, so these unsigned errors do not disclose registration of the principal, key identifier or authority epoch named by the original.
  • The reference returns request-admission refusals to its local host as error values. Its ordinary request path can select a boundary, check delivery and cancellation, and refuse INVALID_SOURCE before verifying the envelope signature. This version does not provide registration privacy for those local errors. An unsigned error MUST NOT be treated as an authenticated response or as evidence of a product effect.

On the signed-response path, every check before the signature reads only the frame, the channel and host configuration, never the registration that the frame names. That includes the ceiling on the response size. The original's signature is verified before anything is signed:

RustSigned-response entrysrc/authority/recovery/wire/response.rsL653-L691Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn response_original(    &self,    channel: &Channel,    bytes: &[u8],) -> Result<(CanonicalPacket, CallId), WireError> {    if channel.profile().version != 6 || !channel.profile().allows(5, 2) {        return Err(WireError::UnsupportedVersion);    }    if self.anchor.ordered_wire_control().is_none()        || self.security.ordered_revocation_profile() != Some(self.profile)        || !self.security.channel_current(channel)        || channel.deployment() != self.profile.deployment    {        return Err(WireError::InvalidSource);    }    let frame_limit = self.security.frame_limit(channel);    check_declared_version(bytes, frame_limit)?;    let original = decode_packet(bytes, frame_limit)?;    let core = original.core();    // The same operation check that runs in front of the admission gate.    super::check_operation(self.security.as_ref(), channel, core)?;    if core.version != Some(channel.profile().version)        || core.deployment != Some(channel.deployment())        || core.source_domain != Some(channel.remote())        || core.target_domain != Some(channel.local())    {        return Err(WireError::InvalidSource);    }    let id = caller_id(&decode(core, 1)?)?;    // The response embeds the original Core. A request whose response    // could not fit the frame limit is refused here, before any effect.    if largest_response_len(channel, &original)? > frame_limit {        return Err(WireError::InvalidFormat);    }    // Every check above reads only the frame and the channel. Nothing is    // signed for a frame whose own signature does not verify.    check_original_signature(channel, &original)?;    Ok((original, id))}

That check uses the peer key that the channel authenticated, not a key looked up from the frame:

RustOriginal signature firstsrc/authority/recovery/wire/response.rsL564-L584Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Verify the signature of a received original with the peer key that the/// channel authenticated, before any answer about the frame is signed. A frame/// that fails here receives only an unsigned error. The check reads the frame/// and the channel key, and no registration of the principal, key or epoch/// that the frame names, so the error cannot reveal that registration.fn check_original_signature(channel: &Channel, original: &CanonicalPacket) -> Result<(), WireError> {    let scheme = original.core().signature_scheme.ok_or(BAD)?;    if scheme == 2 {        return Err(WireError::InvalidThresholdSignature);    }    if !channel.profile().schemes.contains(&scheme)        || !verify(            &channel.peer_keys()[1],            &original.signing_bytes()?,            original.signature(),        )    {        return Err(WireError::InvalidSignature);    }    Ok(())}

The ordinary request path first applies the configured operation check and compares the host's signing credential with the exact frame. It then selects the boundary and checks delivery and cancellation before it verifies the envelope, and then applies the TTL ceiling:

RustRequest path ordersrc/authority/recovery/wire.rsL601-L631Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
let frame_limit = product.security.frame_limit(channel);if bytes.len() > frame_limit {    return Err(WireError::InvalidFormat);}let input = match channel.profile().version {    6 => {        check_declared_version(bytes, frame_limit)?;        Input::Modern(Box::new(decode_packet(bytes, frame_limit)?))    }    5 if product.security.legacy_configured(channel) => Input::Legacy {        bytes,        core: Box::new(legacy::decode_shape(bytes, frame_limit)?),    },    _ => return Err(WireError::UnsupportedVersion),};check_operation(product.security.as_ref(), channel, input.core())?;if !product.security.matches_signed_packet(bytes, credentials.signature) {    return Err(WireError::InvalidSignature);}let core = input.core();let boundary = boundary_for(product.security.as_ref(), channel, core, deployment, cut)?;if bytes.len() > boundary.max_frame {    return Err(WireError::InvalidFormat);}gate_delivery(delivery, boundary.now, ordered.is_none())?;if ordered.is_none() && product.security.delivery_cancelled(core) {    return Err(WireError::Cancelled);}let legacy = input.verify(channel, &boundary)?;check_delivery_ttl(product.security.as_ref(), channel, core)?;let operands = decode(core, boundary.max_nesting)?;

A caller accepts a response only after the peer's signature verifies and the response matches the original it kept:

RustAdmit a responsesrc/authority/recovery/wire/response.rsL437-L458Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Verify the peer signature and correlate a reference response against the/// canonical original retained by the caller. The configured boundary/// supplies the current peer, key and envelope policy.pub fn admit_response(    channel: &Channel,    boundary: &Boundary,    expected: &CanonicalPacket,    response: &[u8],) -> Result<VerifiedResponse, WireError> {    if channel.local() != boundary.target        || channel.remote() != boundary.source        || channel.deployment() != boundary.deployment        || boundary.now > channel.expires()        || boundary.public_key != channel.peer_keys()[1]    {        return Err(WireError::InvalidSource);    }    check_declared_version(response, boundary.max_frame)?;    let response = decode_packet(response, boundary.max_frame)?;    admit_packet(&response, channel.profile(), boundary)?;    validate_response(&response, expected, boundary.max_nesting)}

Exact cryptographic and product checks#

NormativeBytes, test vectors and proofs

SINGLE (opens in a new tab) uses Ed25519 (opens in a new tab) under the envelope signing domain defined in Wire Format. The reference checks the exact registered key and rejects invalid or non-canonical signature inputs rather than normalizing them. THRESHOLD (opens in a new tab) has a discriminator but no reference verifier.

The endpoint compares the decoded request, target and credentials against the current authenticated caller. An effect then consumes its own role, lock, footprint versions, evidence and cut. A source proof or legal endorsement must bind the exact operation fields. The host's validated adapter object cannot replace a differently signed request.

The authority authenticates the caller's token and refuses a Core whose principal or source domain is not that caller:

RustCaller against the Coresrc/authority/recovery/wire.rsL632-L641Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
let session = source_session(&operands).map(|session| SessionProof { session, epoch: boundary.epoch });let identity = if let Some(proof) = session {    product.active_identity(credentials.token, proof)} else {    product.authenticate(credentials.token)}.map_err(|_| WireError::InvalidSource)?;if Some(identity.principal) != core.principal || Some(identity.domain) != core.source_domain {    return Err(WireError::InvalidSource);}

Limits and failure certainty#

NormativeBytes, test vectors and proofs

Frame size, nesting depth, permitted clock skew and optional delivery TTL ceilings are host configuration. At the signed-response entry points, a caller-qualified native call that cannot fit its largest possible signed response is refused before its effect. The ceiling is computed from the actual response form; it is not an additional protocol number.

Frame size, nesting depth, clock skew and the asset TTL floor reach both admission functions as host values:

Host boundarysrc/wire/admission.rsL12-L29Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub struct Boundary {    pub deployment: u64,    pub source: u64,    pub target: u64,    pub principal: u64,    pub key_id: u64,    pub epoch: u64,    pub route_scope: u64,    pub now: u64,    pub asset_ttl_floor: u64,    pub future_skew: u64,    pub max_frame: usize,    pub max_nesting: usize,    pub version_floor: u64,    pub critical_allowed: bool,    pub public_key: [u8; 32],}

The error selected for a frame with several defects follows the reference's documented check order. Unsigned request-admission errors can reveal a registered binding before the signature check. This limitation differs from the signed-response path, which verifies the original peer signature before signing.

See Conformance for the scope and Error Reference for effect and retry behavior.

Pipeline design context#

DesignCarried from v0.5, without bytes or test vectors in this version

The broad multi-stage pipeline described in v0.5 is an architecture view. Its parallel prechecks do not permit speculative authority to substitute for final endpoint checks in this edition. The executable reference order and its observable errors govern implementations that claim that profile.


Edition history#

  • 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.