Protocol Specification v0.6
Wire Format
An OIP (opens in a new tab) v0.6 frame is a canonical Protocol Buffers Packet containing one Core and signature bytes. Implementations validate the original byte sequence before a generated parser can discard unknown fields or normalize duplicate values. The schema provides field numbers; the canonical rules determine which encodings are valid.
Canonical packet
A packet is the Core bytes and a signature over prefix, length and those exact bytes; Core fields keep one order.
- 1
Two fields
A packet holds field 1, the Core bytes, then field 2, the signature, which is exactly 64 bytes for SINGLE. Nothing else may appear.
- 2
One fixed order
Fields appear once each, in tag order, with the wire type of the schema. Fields 3 to 10 are
deployment,source_domain,target_domain,principal,key_id,authority_epoch,issued_secondsandttl_seconds. - 3
One optional field
Only
secondarymay be absent. Any other missing, repeated or out-of-order field isINVALID_FORMAT. - 4
Exactly one body
The body is one
Operationwhose tag is 15 plus the family, so a query (family 4) arrives as field 19. - 5
Signed as received
The signature covers the prefix, the four-byte big-endian length and the Core bytes exactly as received, never a re-encoding.
- The Core bytes: received in field 1 and signed unchanged
- The signature and the step that makes it
- A field that may be absent
- The Core message
Schematic: cell widths are not byte lengths.
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 selector requires the shortest varints for the Packet tag, Core length, Core field-1 tag and unsigned 64-bit version, and requires the complete declared Core payload. It does not scan past a different first field. If any selector component is missing, truncated, overlong or out of range, the selector declares no version and canonical decoding decides the error. In particular, a truncated Core with a visible version prefix does not select that version. 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 reference implementations read the declared version from the first bytes of the frame and nothing else, so the answer stays the same when a later version adds fields.
/// The version that a received frame declares: the varint of core field 1/// when the frame starts with packet field 1 and the core it carries starts/// with field 1. Nothing else in the frame is read, so the answer does not/// depend on fields that a later version may add. A frame that does not start/// this way declares no version, and the canonical decoder reports it.pub fn declared_version(bytes: &[u8]) -> Option<u64> { let mut input = bytes; if take_varint(&mut input).ok()? != (1 << 3) | 2 { return None; } let size = usize::try_from(take_varint(&mut input).ok()?).ok()?; let mut core = input.get(..size)?; if take_varint(&mut core).ok()? != 1 << 3 { return None; } take_varint(&mut core).ok()}/// 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(()), }}// DeclaredVersion returns the varint of core field 1 when the frame starts// with packet field 1 and the core it carries starts with field 1. Nothing// else in the frame is read, so the answer does not depend on fields that a// later version may add. A frame that does not start this way declares no// version, and the canonical decoder reports it.func DeclaredVersion(raw []byte) (uint64, bool) { key, n := rawVarint(raw) if n < 0 || key != 1<<3|2 { return 0, false } raw = raw[n:] size, n := rawVarint(raw) if n < 0 || size > uint64(len(raw)-n) { return 0, false } core := raw[n : n+int(size)] key, n = rawVarint(core) if n < 0 || key != 1<<3 { return 0, false } version, n := rawVarint(core[n:]) return version, n >= 0}// 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}Canonical signed message#
NormativeBytes, test vectors and proofs
- The v0.6 envelope has a canonical protobuf core and signature bytes. The core binds version and purpose, deployment, source and target domain, authenticated principal, key and authority epoch (opens in a new tab), issued UTC seconds and delivery TTL (opens in a new tab), the full UUID, declared message family (opens in a new tab) and exactly one matching body.
- Keep the seven established message family discriminators.
- Each body is an Operation record with position-based operands determined by family and opcode. Operations (opens in a new tab) gives the position, type and meaning of every operand, and it is generated from the table operations.json (opens in a new tab).
- A request carries its own caller-qualified sequence, session and cut and any exact origin, evidence, role or receipt correlation used by the product consumer. The full transport UUID and durable request and operation identifiers are separate.
- Use ascending known field tags, shortest protobuf varints and lengths, exact wire types, a single encoding for each field, and an exact oneof and presence discipline at every nested record. Varints have at most 10 bytes. Field tags range from 1 to 2^29 - 1, and only wire types 0, 1 and 2 occur.
- Validate original bytes before the generated parser can normalize duplicates, unknown fields or competing alternatives. The canonical parser preserves signature bytes and the original unknown secondary enum number and presence. Unknown primary values are rejected. An unknown secondary number does not authorize any product effect.
- Deployment, source domain, target domain and principal are registered 64-bit handles, and so are the key identifier, authority epoch and route scope. A receiver compares each with its registrations and the negotiated channel. The core carries no address or chain name text.
- Purpose MUST be 1 in this version. A frame with another purpose receives INVALID_SOURCE.
- The transport UUID MUST be 16 bytes with the version 4 bits and the RFC 4122 variant bits: the high four bits of byte 6 are 4, and the high two bits of byte 8 are binary 10.
- Issued seconds and finite appeal deadlines (opens in a new tab) are UTC seconds since the Unix epoch. Delivery TTL, durations and elapsed values are seconds. Amounts are unsigned integers in base units.
- Preserve the absent deadline, the indefinite deadline and the finite deadline as separate forms on the wire (deadline modes (opens in a new tab)). In this version the product gives the absent and the indefinite forms the same meaning: no appeal deadline.
- Strings use validated UTF-8 and canonical ordering by bytes for sets and maps, with sequence order preserved. Transport UUIDs are 16 bytes and unsigned 256-bit integers have one fixed 32-byte big-endian representation. Product bindings use checked range conversion before effects.
- Reject malformed lengths and out-of-profile scope rather than truncating values. The maximum frame size and nesting depth are configuration values of the receiving host. A frame larger than the configured maximum MUST be refused as INVALID_FORMAT. Only complete canonical frames enter the validator.
- The immutable origin signing input is ASCII OIP followed by NUL, ASCII ENVELOPE followed by NUL, four big-endian bytes of core length, and the canonical core. Derive it from the authoritative fields and sign the exact bytes.
- Negotiation uses its own binary signing inputs, and the reference token, policy and source records use the signing inputs in JSON encodings (opens in a new tab). Verify the scheme and signature with the current authenticated source key and scope.
- SINGLE (opens in a new tab) uses Ed25519 (opens in a new tab) (RFC 8032) with a 32-byte public key and a 64-byte signature. A verifier MUST reject a SINGLE signature unless the public key and the first 32 bytes of the signature (R) are canonical point encodings, with the y coordinate below 2^255 - 19, of points that are not of small order, the last 32 bytes (S) encode an integer below the group order, and the equation [S]B = R + [k]A holds without cofactor multiplication.
- THRESHOLD (opens in a new tab) (scheme 2) has no verifier in this version. The reference has no THRESHOLD adapter and refuses every scheme 2 frame as INVALID_THRESHOLD_SIGNATURE, with no fallback. In transport admission (opens in a new tab) this refusal comes after the version, source and time checks. A THRESHOLD adapter MUST enforce exact current distinct authorized signers and a trusted quorum. With no such adapter in the reference, this requirement cannot be tested against it.
Field order on the wire#
The reference decoders check the order of the fields on the received bytes before a generated parser reads them. Field 14 is the only field that may be missing, and the tag of the body must be 15 plus the family.
fn validate_core_bytes(bytes: &[u8]) -> Result<()> { let values = fields(bytes)?; let [ Field::Varint(1, _), Field::Varint(2, _), Field::Fixed64(3), Field::Fixed64(4), Field::Fixed64(5), Field::Fixed64(6), Field::Fixed64(7), Field::Fixed64(8), Field::Fixed64(9), Field::Fixed64(10), Field::Bytes(11, _), Field::Varint(12, family), Field::Varint(13, _), tail @ .., ] = values.as_slice() else { return Err(BAD); }; let tail = if matches!(tail.first(), Some(Field::Varint(14, _))) { &tail[1..] } else { tail }; let [ Field::Varint(15, _), Field::Bytes(tag, op), Field::Varint(23, _), Field::Varint(24, _), Field::Fixed64(25), ] = tail else { return Err(BAD); }; if !(1..=7).contains(family) || u64::from(*tag) != 15 + family { return Err(BAD); } validate_operation_bytes(op)}func verifyCore(data []byte) error { values, err := fields(data) if err != nil || len(values) < 18 { return ErrFormat } for i, kind := range []uint64{0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 0} { if !is(values[i], uint32(i+1), kind) { return ErrFormat } } family := values[11].word if family < 1 || family > 7 { return ErrFormat } tail := values[13:] if is(tail[0], 14, 0) { tail = tail[1:] } if len(tail) != 5 || !is(tail[0], 15, 0) || !is(tail[1], uint32(15+family), 2) || !is(tail[2], 23, 0) || !is(tail[3], 24, 0) || !is(tail[4], 25, 1) { return ErrFormat } return verifyOperation(tail[1].data)}Every varint must be the shortest encoding of its value, so each value has one byte form. Rust refuses a last byte of zero after a continuation byte, and Go compares the length read with the shortest length of the value.
fn take_varint(input: &mut &[u8]) -> Result<u64> { let mut value = 0; for index in 0..10 { let (byte, rest) = input.split_first().ok_or(BAD)?; *input = rest; if index == 9 && *byte > 1 { return Err(BAD); } value |= u64::from(byte & 127) << (index * 7); if byte & 128 == 0 { if index > 0 && *byte == 0 { return Err(BAD); } return Ok(value); } } Err(BAD)}func rawVarint(data []byte) (uint64, int) { value, n := protowire.ConsumeVarint(data) if n < 0 || n != protowire.SizeVarint(value) { return 0, -1 } return value, n}Decoding and re-encoding#
After these checks, each decoder parses the packet with the generated code, encodes the result again and refuses the frame unless the two byte strings are equal.
pub fn decode_packet(bytes: &[u8], max_frame: usize) -> Result<CanonicalPacket> { if bytes.len() > max_frame { return Err(BAD); } let values = fields(bytes)?; let [Field::Bytes(1, core), Field::Bytes(2, _)] = values.as_slice() else { return Err(BAD); }; let checked_core = decode_core(core)?; let packet = Packet::decode(bytes).map_err(|_| BAD)?; if packet.core.as_ref() != Some(&checked_core) || encode_packet(&packet)? != bytes { return Err(BAD); } Ok(CanonicalPacket { packet, core_bytes: core.to_vec(), })}func DecodePacket(raw []byte, maxFrame int) (*CanonicalPacket, error) { if maxFrame < 0 || len(raw) > maxFrame { return nil, ErrFormat } values, err := fields(raw) if err != nil || len(values) != 2 || !is(values[0], 1, 2) || !is(values[1], 2, 2) { return nil, ErrFormat } core, err := DecodeCore(values[0].data) if err != nil { return nil, err } packet := new(wirepb.Packet) if err := proto.Unmarshal(raw, packet); err != nil || !proto.Equal(core, packet.Core) { return nil, ErrFormat } exact, err := EncodePacket(packet) if err != nil || !bytes.Equal(exact, raw) { return nil, ErrFormat } return &CanonicalPacket{packet: packet, exact: bytes.Clone(values[0].data)}, nil}Signing input and signature check#
Decoding stores the bytes of Packet field 1 as they arrived, and the packet builds its signing input from that stored copy.
#[derive(Clone, Debug, PartialEq)]pub struct CanonicalPacket { packet: Packet, core_bytes: Vec<u8>,}impl CanonicalPacket { pub fn core(&self) -> &Core { self.packet.core.as_ref().expect("validated core") } pub fn signature(&self) -> &[u8] { self.packet .signature .as_deref() .expect("validated signature presence") } pub fn core_bytes(&self) -> &[u8] { &self.core_bytes } pub fn encode(&self) -> Result<Vec<u8>> { encode_packet(&self.packet) } pub fn signing_bytes(&self) -> Result<Vec<u8>> { signing_bytes(&self.core_bytes) }}// CanonicalPacket cannot be constructed with caller-supplied typed fields.type CanonicalPacket struct { // Exact typed result from this packet's successful Receive path. decodedOperands *Operands packet *wirepb.Packet exact []byte}func (packet *CanonicalPacket) Core() *wirepb.Core { return proto.Clone(packet.packet.Core).(*wirepb.Core)}func (packet *CanonicalPacket) CoreBytes() []byte { return bytes.Clone(packet.exact) }func (packet *CanonicalPacket) Encode() ([]byte, error) { return EncodePacket(packet.packet) }func (packet *CanonicalPacket) SigningBytes() ([]byte, error) { return SigningBytes(packet.exact) }The signing input is the prefix, the four-byte big-endian Core length and those Core bytes.
pub fn signing_bytes(core: &[u8]) -> Result<Vec<u8>> { let mut out = b"OIP\0ENVELOPE\0".to_vec(); out.extend(u32::try_from(core.len()).map_err(|_| BAD)?.to_be_bytes()); out.extend(core); Ok(out)}func SigningBytes(core []byte) ([]byte, error) { if uint64(len(core)) > math.MaxUint32 { return nil, ErrFormat } out := []byte("OIP\x00ENVELOPE\x00") out = binary.BigEndian.AppendUint32(out, uint32(len(core))) return append(out, core...), nil}Admission first checks the version, the source fields and the issue and expiry times. A frame that passes them and names scheme 2 is refused as INVALID_THRESHOLD_SIGNATURE before any signature check. Any other frame needs a scheme of the negotiated profile and a valid signature over the signing input.
pub fn admit_packet( packet: &CanonicalPacket, profile: &Profile, boundary: &Boundary,) -> Result<()> { check_scope(packet.core(), profile, boundary)?; let scheme = packet .core() .signature_scheme .ok_or(WireError::InvalidFormat)?; if scheme == 2 { return Err(WireError::InvalidThresholdSignature); } if !profile.schemes.contains(&scheme) || !verify( &boundary.public_key, &packet.signing_bytes()?, packet.signature(), ) { return Err(WireError::InvalidSignature); } Ok(())}func AdmitPacket(packet *CanonicalPacket, profile Profile, boundary Boundary) error { core := packet.packet.Core if err := checkScope(core, profile, boundary, 6); err != nil { return err } if core.GetSignatureScheme() == 2 { return ErrThreshold } preimage, err := packet.SigningBytes() if err != nil || !slices.Contains(profile.Schemes, core.GetSignatureScheme()) || !verify(boundary.PublicKey, preimage, packet.packet.Signature) { return ErrSignature } return nil}Both implementations apply the strict SINGLE rules above before they accept a signature.
pub(crate) fn verify(key: &[u8; 32], bytes: &[u8], signature: &[u8]) -> bool { fn canonical_point(point: &[u8]) -> bool { let Some(mut y) = <[u8; 32]>::try_from(point).ok() else { return false; }; y[31] &= 127; let mut prime = [255u8; 32]; prime[0] = 237; prime[31] = 127; y.iter().rev().cmp(prime.iter().rev()).is_lt() } if !canonical_point(key) || !signature.get(..32).is_some_and(canonical_point) { return false; } VerifyingKey::from_bytes(key) .and_then(|key| { let sig = Signature::from_slice(signature)?; key.verify_strict(bytes, &sig) }) .is_ok()}// Reject noncanonical and small-order public encodings before Go's Ed25519// verification. These are public point checks, not a signing implementation.func strictPoint(compressed []byte) bool { if len(compressed) != 32 { return false } var reversed [32]byte for i := range reversed { reversed[31-i] = compressed[i] } reversed[0] &= 127 y := new(big.Int).SetBytes(reversed[:]) if y.Cmp(edwardsPrime) >= 0 { return false } one := big.NewInt(1) for range 3 { yy := new(big.Int).Mod(new(big.Int).Mul(y, y), edwardsPrime) dyy := new(big.Int).Mul(edwardsD, yy) inverse := new(big.Int).ModInverse(new(big.Int).Add(dyy, one), edwardsPrime) if inverse == nil { return false } xx := new(big.Int).Mod(new(big.Int).Mul(new(big.Int).Sub(yy, one), inverse), edwardsPrime) denominator := new(big.Int).Sub(one, new(big.Int).Mul(dyy, xx)) inverse = denominator.ModInverse(denominator, edwardsPrime) if inverse == nil { return false } y.Mod(new(big.Int).Mul(new(big.Int).Add(yy, xx), inverse), edwardsPrime) } return y.Cmp(one) != 0}func verify(key [32]byte, message, signature []byte) bool { return len(signature) == ed25519.SignatureSize && strictPoint(key[:]) && strictPoint(signature[:32]) && ed25519.Verify(ed25519.PublicKey(key[:]), message, signature)}Protocol Buffers schema#
NormativeBytes, test vectors and proofs
This is the schema of the reference edition, shown from the public repository. Field presence is validated by the canonical decoder; optional in the schema is not permission to omit a required protocol value.
syntax = "proto2";package oip.v06;option go_package = "github.com/Oraclizer/oip-spec/go/wirepb";// The canonical OIP v0.6 wire schema. Receivers accept only the canonical// encoding of these messages and reject unknown fields; the encoding rules are// in spec/wire-conformance.md. New fields enter only through a new negotiated// version.message Packet { optional Core core = 1; optional bytes signature = 2;}message Core { optional uint64 version = 1; optional uint64 purpose = 2; optional fixed64 deployment = 3; optional fixed64 source_domain = 4; optional fixed64 target_domain = 5; optional fixed64 principal = 6; optional fixed64 key_id = 7; optional fixed64 authority_epoch = 8; optional fixed64 issued_seconds = 9; optional fixed64 ttl_seconds = 10; optional bytes uuid = 11; optional uint64 family = 12; optional uint64 primary = 13; optional uint64 secondary = 14; optional uint64 signature_scheme = 15; // Exactly one member is present, and its tag is 15 plus the family. oneof body { Operation state_sync = 16; Operation regulatory_action = 17; Operation lock_management = 18; Operation query = 19; Operation ack = 20; Operation heartbeat = 21; Operation governance = 22; } optional uint64 atomicity = 23; optional uint64 priority = 24; optional fixed64 route_scope = 25;}// An operation carries positional operands. The family of the enclosing body// member and the opcode fix the meaning and order of every entry in words and// blobs. The field-meaning table spec/operations.json lists, for each family// and opcode, the positions, enumerations and whether the reference// configuration accepts the operation.message Operation { optional uint64 opcode = 1; repeated fixed64 words = 2 [packed = false]; repeated bytes blobs = 3; // The appeal deadline of a regulatory action (family 2). Operations of // other families reject both members. In this version an absent member and // appeal_deadline_indefinite both mean that no appeal deadline applies. oneof appeal_deadline { // Only the value true is canonical. bool appeal_deadline_indefinite = 4; // Unix time in UTC seconds. fixed64 appeal_deadline_utc_seconds = 5; }}Two groups of bytes in OIP v0.6 are JSON text. The governance (opens in a new tab) blobs endpoint_contexts_json and session_bindings_json are wire bytes of the protocol core (opens in a new tab), and operations (opens in a new tab) gives their shapes. The reference profile also signs or hashes JSON text for its tokens, policy bindings, authorizations and source attestations.
Encoding rules#
NormativeBytes, test vectors and proofs
The JSON text of a value is its compact encoding under the rules below. A producer MUST emit exactly these bytes. A receiver of a governance blob MUST decode it, encode the decoded value again and reject the blob unless the two byte strings are equal.
- The text is UTF-8 with no byte order mark and no whitespace outside strings.
- A record is an object. Its keys are the field names in the declaration order of the record, and each key appears at most once.
- An absent optional value appears as
null. The exception is the reference configuration record and the endpoint grants it contains, which omit some fields when they are empty or absent, so that a configuration without them keeps its bytes. - An unsigned integer is a JSON number in decimal digits with no sign, leading zero, fraction or exponent. Every value up to 2^64 - 1 appears in full. A reader MUST NOT pass such a value through a floating-point type.
- A fixed byte array, such as a 32-byte key or digest, is an array of numbers from 0 to 255 in byte order.
- An enum value without fields is a string holding the variant name. A variant with fields is an object with one key, the variant name, whose value is the object of its fields.
- A set is an array of its elements in ascending order without duplicates. Integers compare by value, tuples compare element by element, and enum values compare by the declaration order of their variants. A tuple is an array of its elements in order. A sequence keeps its order.
- A map with integer keys is an object whose keys are the decimal strings of the keys in ascending numeric order. The dependency map of an endpoint context is instead an array of
[key, value]pairs in ascending key order. - A string escapes the quotation mark, the reverse solidus and the control characters U+0000 to U+001F. The control characters use
\b,\t,\n,\fand\rwhere those exist and\u00followed by two lowercase hexadecimal digits otherwise. No other character is escaped.
Reference signing inputs#
NormativeBytes, test vectors and proofs
Each reference signing input is an ASCII domain string, one NUL byte, and the JSON text of the value. The signer signs these exact bytes with Ed25519. Where the table says digest, the reference binds the SHA-256 digest of the signing input instead of signing it. The reference verifies these signatures with strict Ed25519 verification and rejects small-order public keys.
In the reference, one function appends the JSON text to the domain bytes, and each caller passes its domain with the NUL byte already at the end, as in oip/reference-session/v1\0. Verification refuses small-order public keys.
fn preimage(domain: &[u8], fields: &impl Serialize) -> Result<Vec<u8>, WireError> { let mut bytes = domain.to_vec(); bytes.extend(serde_json::to_vec(fields).map_err(|_| WireError::InvalidFormat)?); Ok(bytes)}fn verifying_key(raw: &[u8; 32]) -> Option<VerifyingKey> { VerifyingKey::from_bytes(raw) .ok() .filter(|key| !key.is_weak())}fn verify_strict(key: &[u8; 32], bytes: &[u8], signature: &[u8]) -> bool { verifying_key(key) .zip(Signature::from_slice(signature).ok()) .is_some_and(|(key, signature)| key.verify_strict(bytes, &signature).is_ok())}| Domain | Value | Use |
|---|---|---|
oip/reference-session/v1 | Session claims: profile, epoch, identity, session, issuer, grants_sha256, issued_seconds, expires_seconds | Session token signed by the configured session issuer |
oip/reference-session-grants/v1 | The configured grants of the session | Digest carried in grants_sha256 |
oip/reference-maintenance/v1 | Maintenance claims: role, profile, epoch, principal, issuer, issued_seconds, expires_seconds | Maintenance token signed by the configured maintenance issuer |
oip/reference-authority-policy/v1 | The authority configuration record | Digest bound into the retained image as the policy fingerprint |
oip/reference-ordered-maintenance/v1 | Maintenance request: profile, principal, cut, selection, action | Signed ordered maintenance action |
oip/reference-successor-ready/v1 | Tuple of recovery profile, current binding, next policy fingerprint and endpoint contexts | Readiness signature of the next key before a handover is selected |
oip/reference-handover-consent/v1 | Tuple of recovery profile, current binding, next binding, role, identity and handover proposal | Consent of the current owner or of the successor |
oip/reference-legal-order/v1 | Tuple of recovery profile, target, stage tag, call identifier, call payload and order | Legal authority signature of an order stage |
oip/reference-enforcement-order/v1 | Tuple of recovery profile, target, call identifier, call payload and order | Enforcement authority signature of an enforcement spend |
oip/reference-claim-recovery/v1 | Tuple of recovery profile, identity, session, session epoch, target, origin, call identifier and call payload | Owner endorsement of a recovery request |
oip/source-terminal-record/v1 | Source statement: profile, origin_epoch, evidence | Source authority signature of its terminal decision |
oip/source-terminal-quorum/v1 | Source statement: profile, origin_epoch, evidence | Vote of a registered quorum signer |
A source certificate is the JSON text of the record with statement and votes, with no domain prefix.
Fixed vectors#
NormativeBytes, test vectors and proofs
These vectors come from running the reference serializer on the values shown. Each JSON text decodes and encodes again to the same bytes.
An endpoint_contexts_json blob with one context: endpoint 2, epoch 9, grants of principal 7 on the normal route for Reserve and Debit, asset 17 depending on assets 17 and 18, and a write permission of principal 7 on asset 17 with value 1. The grants appear in the declaration order of the permission variants. The blob is 142 bytes.
json: [{"endpoint":2,"epoch":9,"grants":[[7,"Normal","Reserve"],[7,"Normal","Debit"]],"dependencies":[[17,[17,18]]],"write_permissions":[[7,17,1]]}] hex: 5b7b22656e64706f696e74223a322c2265706f6368223a392c226772616e7473223a5b5b372c224e6f726d616c222c2252657365727665225d2c5b372c224e6f726d616c222c224465626974225d5d2c22646570656e64656e63696573223a5b5b31372c5b31372c31385d5d5d2c2277726974655f7065726d697373696f6e73223a5b5b372c31372c315d5d7d5dA session_bindings_json blob binding session 5 to principal 7 at authority epoch 9. The blob is 41 bytes.
json: [[5,{"principal":7,"authority_epoch":9}]] hex: 5b5b352c7b227072696e636970616c223a372c22617574686f726974795f65706f6368223a397d5d5dFor the signing inputs below, the input to sign is the domain bytes followed by the JSON text.
Maintenance claims with role ReferenceControl, profile deployment 7, format 1 and authority profile 3, epoch 2, principal 9, an issuer key whose first byte is 1 and last byte is 255 with zeros between, issued 1700000000 and expiring 1700000600. The signing input is 274 bytes.
domain hex: 6f69702f7265666572656e63652d6d61696e74656e616e63652f763100json: {"role":"ReferenceControl","profile":{"deployment":7,"format":1,"authority_profile":3},"epoch":2,"principal":9,"issuer":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255],"issued_seconds":1700000000,"expires_seconds":1700000600} input hex: 6f69702f7265666572656e63652d6d61696e74656e616e63652f7631007b22726f6c65223a225265666572656e6365436f6e74726f6c222c2270726f66696c65223a7b226465706c6f796d656e74223a372c22666f726d6174223a312c22617574686f726974795f70726f66696c65223a337d2c2265706f6368223a322c227072696e636970616c223a392c22697373756572223a5b312c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c3235355d2c226973737565645f7365636f6e6473223a313730303030303030302c22657870697265735f7365636f6e6473223a313730303030303630307dSession claims with the same profile, epoch, issuer and validity window, identity principal 9, domain 4, OCID (opens in a new tab) 11 and RAI (opens in a new tab) 12, session 5, and a grants digest whose second byte is 16 with zeros elsewhere. The signing input is 382 bytes.
domain hex: 6f69702f7265666572656e63652d73657373696f6e2f763100json: {"profile":{"deployment":7,"format":1,"authority_profile":3},"epoch":2,"identity":{"principal":9,"domain":4,"ocid":11,"rai":12},"session":5,"issuer":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255],"grants_sha256":[0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"issued_seconds":1700000000,"expires_seconds":1700000600} input hex: 6f69702f7265666572656e63652d73657373696f6e2f7631007b2270726f66696c65223a7b226465706c6f796d656e74223a372c22666f726d6174223a312c22617574686f726974795f70726f66696c65223a337d2c2265706f6368223a322c226964656e74697479223a7b227072696e636970616c223a392c22646f6d61696e223a342c226f636964223a31312c22726169223a31327d2c2273657373696f6e223a352c22697373756572223a5b312c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c3235355d2c226772616e74735f736861323536223a5b302c31362c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c302c305d2c226973737565645f7365636f6e6473223a313730303030303030302c22657870697265735f7365636f6e6473223a313730303030303630307dRelated Pages#
Edition history#
- 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.
