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.
- 1
Version before decoding
The declared version is read from the first bytes, so a frame that declares another version gets
UNSUPPORTED_VERSIONwhatever follows. - 2
Profile before source
A message family or atomicity outside the negotiated profile is refused before any source or signature check.
- 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
Signature
THRESHOLDhas no verifier and is refused first. Otherwise the scheme must be in the profile and Ed25519 must verify the signing input. - 5
Binding last
The same Core bytes under the same key are an
Identicalretry. Different bytes under that key getREPLAY_CONFLICT. - 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.
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 genericReceivepath does not select that set.
Both references read the declared version from the first bytes, within the size limit, before the canonical decoder runs:
/// 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(()), }}// CheckDeclaredVersion is 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 ErrVersion, whatever fields// follow. Every other frame continues to the canonical checks unchanged.func CheckDeclaredVersion(raw []byte, maxFrame int) error { if maxFrame < 0 || len(raw) > maxFrame { return ErrFormat } if version, ok := DeclaredVersion(raw); ok && version != 6 { return ErrVersion } return nil}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:
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); }func checkScope(core *wirepb.Core, profile Profile, boundary Boundary, version uint64) error { family, op := Body(core) if profile.Version != version || core.GetVersion() != version || boundary.VersionFloor > version || !slices.Contains(profile.Families, family) || !slices.Contains(profile.Atomicities, core.GetAtomicity()) || (version == 5 && (op.GetOpcode() < 1 || op.GetOpcode() > 2 || (family != 4 && family != 6))) { return ErrVersion }The reference authority applies its configured operation set right after canonical decoding:
/// 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:
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, }}// ReferenceAccepts reports whether the reference configuration executes the// operation of a decoded Core. It accepts 25 of the 31 operations of version// 6 and refuses Write, Timeout, CorrelatedResponse, Capabilities, Liveness// and InstallEpoch.func ReferenceAccepts(core *wirepb.Core) bool { family, op := Body(core) opcode := op.GetOpcode() switch family { case 1, 3: return opcode >= 1 && opcode <= 6 case 2: return opcode >= 1 && opcode <= 3 case 4: return opcode >= 1 && opcode <= 8 case 5, 7: return opcode == 1 default: return 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:
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))}// Receive admits a version 6 frame and binds its transport key. The attest// callback checks the admitted Core before its operands are decoded.func (store *TransportBindings) Receive(raw []byte, profile Profile, boundary Boundary, attest func(*wirepb.Core) bool) (Arrival, *CanonicalPacket, error) { return store.ReceiveAccepting(raw, profile, boundary, nil, attest)}// ReceiveAccepting is Receive with the operation check that a configuration// runs in front of the admission gate. An operation that accepts refuses// receives ErrVersion before any signature or source check. A nil accepts// admits every operation.func (store *TransportBindings) ReceiveAccepting(raw []byte, profile Profile, boundary Boundary, accepts func(*wirepb.Core) bool, attest func(*wirepb.Core) bool) (Arrival, *CanonicalPacket, error) { if profile.Version != 6 { return First, nil, ErrVersion } if err := CheckDeclaredVersion(raw, boundary.MaxFrame); err != nil { return First, nil, err } packet, err := DecodePacket(raw, boundary.MaxFrame) if err != nil { return First, nil, err } // Decode and bind the signed Core without exposing it to either callback. signedCore := packet.Core() if accepts != nil && !accepts(packet.Core()) { return First, nil, ErrVersion } if err = AdmitPacket(packet, profile, boundary); err != nil { return First, nil, err } if !attest(packet.Core()) { return First, nil, ErrSource } decodedOperands, err := DecodeOperands(signedCore, boundary.MaxNesting) if err != nil { return First, nil, err } packet.decodedOperands = &decodedOperands key, err := Key(signedCore) if err != nil { return First, nil, err } arrival, err := store.bind(key, packet.exact) if err != nil { return First, nil, err } return arrival, packet, nil}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:
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)} sent, ttl := core.GetIssuedSeconds(), max(core.GetTtlSeconds(), boundary.AssetTTLFloor) if ttl > math.MaxUint64-sent || boundary.FutureSkew > math.MaxUint64-boundary.Now || sent > boundary.Now+boundary.FutureSkew { return ErrTTL } if sent+ttl < boundary.Now { return ErrExpired } return nil}The persistent authority applies its optional ceiling on the signed TTL:
/// 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:
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), }}func (store *TransportBindings) bind(key TransportKey, exact []byte) (Arrival, error) { if prior, ok := store.entries[key]; ok { if !bytes.Equal(prior, exact) { return First, ErrReplay } return Identical, nil } if store.entries == nil { store.entries = make(map[TransportKey][]byte) } store.entries[key] = bytes.Clone(exact) return First, nil}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:
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:
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:
/// 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:
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:
/// 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:
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:
#[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],}type Boundary struct { Deployment, Source, Target, Principal, KeyID, Epoch, RouteScope uint64 Now, AssetTTLFloor, FutureSkew, VersionFloor uint64 MaxFrame, MaxNesting int CriticalAllowed bool PublicKey [32]byte}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.
Related Pages#
Edition history#
- 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.
