Exqub Protocol Specification v1.0
Copyright (c) 2026 VSPRY INTERNATIONAL LIMITED (ABN 59 631 026 027). All
rights reserved.
Exqub is a trading name of VSPRY INTERNATIONAL LIMITED.
This Exqub Protocol Specification is licensed under the Creative Commons Attribution 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
0. Licensing
- This Exqub Protocol Specification is freely implementable by anyone under Creative Commons Attribution 4.0 International (CC BY 4.0).
- The SDKs, application crates, and tooling are Apache 2.0 with no restrictions.
- The core cryptographic engine is dual-licensed — free for integrators under Apache 2.0, commercial only if you're building competing credential infrastructure under Exqub Commercial Licence (ECL).
NORMATIVE SPECIFICATION — V1.0 includes standard credentials (0x01), delegation credentials (0x02), chain linking (via attribute conventions), and content attestation (0x04)
1. Conventions
This document uses RFC 2119 / RFC 8174 key words (MUST, MUST NOT, SHALL, SHOULD, MAY) when in capitals. All sections are normative unless marked otherwise. This specification is authoritative for Exqub v1.0; in case of conflict with other documents, this specification takes precedence.
All multi-byte integers MUST be encoded as unsigned big-endian (network byte order) unless otherwise stated. This applies to length prefixes, timestamps, counters, CBOR integers, tree indices, and all other protocol integer values.
2. Scope
2.1 Included
- ML-DSA-65 signatures only (FIPS 204, quantum-resistant)
- SHA3-256 hashing only (all protocol operations)
- Single canonical signing format
- Attribute Merkle tree with selective disclosure
- Sparse Merkle Tree (SMT) revocation registry
- Device co-signatures (ML-DSA-65)
- Proximity attestation (optional)
- Deterministic CBOR encoding (RFC 8949 §4.2)
- Bounded types for no_std core engine (heap optional for application layer)
- Delegation credentials with scoped authority and sub-delegation (credential_type 0x02)
- Chain-linked credentials for tamper-evident ordered logs (via reserved attribute conventions)
- Content attestation credentials for document provenance (credential_type 0x04)
2.2 Excluded (Explicit Non-Goals)
- Dual-root modes, alternate roots
- Alternative hash functions (BLAKE3) or signature algorithms (SLH-DSA, Ed25519)
- Dynamic crypto suite selection
- SD-JWT/OIDC bridges
- EVM smart contract variations
- Unbounded/heap-allocated containers in core engine
- Multi-party computation
- Attribute modification without reissuance
2.3 Future Considerations (Non-Normative)
The following capabilities are out of scope for V1.0 and may be addressed in future versions:
- Threshold credentials (k-of-n multi-party issuance)
- Conditional disclosure (predicate proofs, e.g., age > 18 without revealing age)
- Blind signatures
- Formal verification proofs (Lean 4)
- Language tags / internationalization metadata
3. Constants
3.1 Cryptographic Sizes
| Constant | Value | Source |
|---|---|---|
| ML-DSA-65 Public Key | 1952 bytes | FIPS 204 |
| ML-DSA-65 Private Key | 4032 bytes | FIPS 204 |
| ML-DSA-65 Signature | 3309 bytes | FIPS 204 |
| SHA3-256 Output | 32 bytes | FIPS 202 |
| Salt Size | 32 bytes | |
| Nonce Size | 32 bytes | |
| Domain Separator Size | 16 bytes |
3.2 Limits
| Constant | Value | Notes |
|---|---|---|
| MAX_ATTRIBUTES | 64 | Maximum attributes per credential |
| MAX_TREE_DEPTH | 8 | Safety bound: ceil(log2(64)) + 2. Actual proof depth = log2(next_power_of_2(attr_count)) |
| MAX_SMT_PROOF_DEPTH | 256 | Supports 2^256 credentials |
| MAX_CBOR_DEPTH | 16 | DoS protection |
| MAX_CBOR_MAP_ENTRIES | 128 | |
| MAX_CBOR_ARRAY_LENGTH | 256 | |
| MAX_CBOR_BYTE_STRING | 16384 bytes | |
| MAX_CBOR_TEXT_STRING | 1024 bytes | |
| MAX_ATTRIBUTE_KEY_LENGTH | 64 bytes | UTF-8 |
| MAX_STRING_LENGTH | 1024 bytes | UTF-8 |
| MAX_CREDENTIAL_SIZE | 16384 bytes | |
| MAX_PRESENTATION_SIZE | 32768 bytes | |
| CORE_ENGINE_STACK | 4096 bytes | Core crypto operations only (see note) |
| MAX_DELEGATION_DEPTH | 5 | Hard cap; prevents deep chains expensive to verify (absolute max is 10) |
| MAX_SCOPE_ACTIONS | 32 | Bounded for no_std heapless compatibility |
| MAX_SCOPE_RESOURCES | 64 | Bounded for no_std heapless compatibility |
| MAX_CHAIN_LENGTH | 1,000,000 | Advisory ceiling — verification is O(n); operational limits SHOULD be lower. Not enforced by the core engine; enforced at the application layer |
3.3 Timing
| Constant | Value | | ------------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | DEFAULT_CLOCK_SKEW | 300 seconds | | HIGH_SECURITY_CLOCK_SKEW | 60 seconds | | MAX_CLOCK_SKEW | 600 seconds | | MAX_CREDENTIAL_LIFETIME | 31536000 seconds (365 days) | | MIN_REPLAY_CACHE_TTL | 900 seconds | | MAX_REPLAY_CACHE_TTL | 86400 seconds | | MAX_SMT_ROOT_AGE | 604800 seconds (7 days) | | MIN_DELEGATION_LIFETIME | 60 seconds | Prevents nonsensical sub-second delegations | | MAX_DELEGATION_LIFETIME | 86,400 seconds (24h) | For ephemeral sub-delegations; root delegations MAY use MAX_CREDENTIAL_LIFETIME (365 days). Advisory for root; SHOULD enforce for sub-delegations |
3.4 Status Values
| Value | Meaning |
|---|---|
| 0x00 | STATUS_VALID |
| 0x01 | STATUS_REVOKED |
| 0x02 | STATUS_SUSPENDED |
3.5 Credential Type Values
| Value | Meaning | Notes |
|---|---|---|
| 0x01 | Standard credential | Original V1 credential type |
| 0x02 | Delegation credential | Scoped authority with sub-delegation support |
| 0x03 | Reserved | Held for future use |
| 0x04 | Content attestation credential | Document provenance and integrity |
Design decision: Chain linking does NOT have its own credential_type. Chain
linking attributes (chain_id, chain_seq, chain_prev) can appear on ANY
credential type (0x01, 0x02, 0x04). Chains are detected by attribute presence,
not by credential_type. Type 0x03 is reserved for future use.
Exhaustion guard: credential_type is a u8 (255 usable values, 0x00
unused). Values 0x05–0xFF are reserved for future assignment and MUST be
rejected by V1 parsers. Adding a new credential type requires a spec revision.
3.6 Protocol Version
PROTOCOL_VERSION = 0x01
3.7 Relationships
- MAX_TREE_DEPTH = ceil(log2(MAX_ATTRIBUTES)) + 2
- Tree Size = next_power_of_2(attr_count), where attr_count ≤ MAX_ATTRIBUTES
- Proof Path Length = log2(Tree Size) — this is the ACTUAL depth used in proofs, NOT MAX_TREE_DEPTH
- Verifiers MUST use the actual mathematical depth, not the safety bound
3.8 Performance Boundaries (Non-Normative)
Note: These are quality targets, not requirements. Actual performance depends on hardware capabilities.
| Operation | Target Time | Notes |
|---|---|---|
| ML-DSA-65 Sign | < 50ms | Hardware-dependent |
| ML-DSA-65 Verify | < 100ms | Hardware-dependent |
| Full Presentation Verify | < 200ms | Complete verification flow |
| Merkle Proof Verify | < 1ms | Hashing operations only |
| SMT Proof Verify | < 5ms | Hashing operations only |
Note on CORE_ENGINE_STACK: The 4096-byte limit applies to individual cryptographic operations (hashing, proof verification, etc.) in no_std environments. Full presentation parsing including ML-DSA signatures (3309 bytes each) requires additional stack space. Implementations MUST ensure deterministic memory usage appropriate for their target environment.
4. Domain Separators
All domain separators are exactly 16 bytes, defined as compile-time byte literals.
| Name | Bytes (ASCII) | Used In |
|---|---|---|
| EXQUB_ISSUER_V1 | EXQUB_ISSUER_V1_ |
Issuer ID derivation |
| EXQUB_CRED_ID_V1 | EXQUB_CRED_ID_V1 |
Credential ID generation |
| EXQUB_SIG_V1 | EXQUB_SIG_V1____ |
Credential signature input |
| EXQUB_ATTR_LEAF_V1 | EXQUB_ATTR_LEAF_ |
Attribute leaf hash |
| EXQUB_ATTR_NODE_V1 | EXQUB_ATTR_NODE_ |
Attribute tree node hash |
| EXQUB_ATTR_PAD_V1 | EXQUB_ATTR_PAD__ |
Padding leaf hash |
| EXQUB_SMT_EMPTY_V1 | EXQUB_SMT_EMPTY_ |
SMT empty subtree |
| EXQUB_SMT_NODE_V1 | EXQUB_SMT_NODE__ |
SMT internal node hash |
| EXQUB_SMT_LEAF_V1 | EXQUB_SMT_LEAF__ |
SMT leaf hash |
| EXQUB_DEV_BIND_V1 | EXQUB_DEV_BIND__ |
Device co-signature binding |
| EXQUB_DEV_KEY_V1 | EXQUB_DEV_KEY_V1 |
Device key hash |
| EXQUB_PROX_PROOF_V1 | EXQUB_PROX_PROOF |
Proximity attestation |
| EXQUB_PRES_HASH_V1 | EXQUB_PRES_HASH_ |
Presentation hash |
| EXQUB_HOLDER_V1 | EXQUB_HOLDER_V1_ |
Holder ID derivation |
| EXQUB_REV_SNAP_V1 | EXQUB_REV_SNAP__ |
Revocation snapshot signature |
| EXQUB_REPLAY_KEY_V1 | EXQUB_REPLAY_KEY |
Replay cache key |
| EXQUB_DELEG_V1 | EXQUB_DELEG_V1__ |
Delegation credential sig_input |
| EXQUB_SCOPE_V1 | EXQUB_SCOPE_V1__ |
Scope constraint hash |
| EXQUB_ACTION_V1 | EXQUB_ACTION_V1_ |
Action request hash |
| EXQUB_SUBDEL_V1 | EXQUB_SUBDEL_V1_ |
Sub-delegation sig_input |
| EXQUB_CHAIN_V1 | EXQUB_CHAIN_V1__ |
Chain ID derivation |
Requirements: Implementations MUST define these as raw byte constants (e.g.,
b"EXQUB_ISSUER_V1_"). MUST NOT construct at runtime, concatenate from parts,
or null-terminate. Implementations MUST include compile-time tests verifying all
21 separators are exactly 16 bytes and pairwise unique.
5. Cryptographic Requirements
5.1 ML-DSA-65 Requirements
Algorithm: ML-DSA-65 (FIPS 204, Category 3, NIST Security Level 3)
Signing Modes:
- Issuer backend: MUST use deterministic signing (empty context, no randomization, no pre-hashing)
- Device co-signature: SHOULD use randomized signing (countermeasure against side-channel attacks on TEE hardware)
Common Parameters: Empty context string. No pre-hashing — sign the 32-byte sig_input directly.
Error Handling: Invalid key format → MUST return error, MUST NOT attempt recovery. Signing failure → MUST return error, MUST NOT retry. Verification failure → MUST return false, MUST NOT accept partial matches.
5.2 Cryptographic Library Requirements
Compliance: MUST use FIPS 204 compliant implementation. Side-channel resistance required for device operations.
RNG Requirements: Implementations MUST use a cryptographically secure random number generator provided by the platform. Entropy source MUST provide at least 256 bits of entropy. Salt generation MUST use fresh randomness for each attribute.
Key Generation: MUST occur in secure hardware when available (HSM, TEE, Secure Enclave). Private keys MUST NOT be exported from secure storage. Key material MUST be zeroized after use.
Library Validation: Implementations SHOULD use validated cryptographic libraries (FIPS 140-3 or equivalent). MUST pass all NIST test vectors for ML-DSA-65 and SHA3-256.
6. Data Structures
6.1 CredentialV1
CredentialV1 {
version: u8, // MUST be 0x01
credential_type: u8, // MUST be 0x01 (Standard credential)
credential_id: [u8; 32], // See §7.2
issuer_id: [u8; 32], // See §7.1
holder_id: [u8; 32], // See §7.4
issued_at: u64, // Unix timestamp
expires_at: u64, // Unix timestamp
attr_count: u32, // Number of actual attributes (≤64)
attr_root: [u8; 32], // Attribute Merkle tree root
}
SignedCredential {
credential: CredentialV1,
signature: [u8; 3309], // ML-DSA-65 over sig_input (§7.3)
}
Boundary Conditions: attr_count = 0 is prohibited. issued_at MUST be < expires_at. credential_type MUST be 0x01 for standard credentials (see §3.5 for all valid types).
6.2 PresentationV1
PresentationV1 {
credential: SignedCredential,
nonce_v: [u8; 32], // Verifier challenge nonce
verifier_id: [u8; 32], // Verifier identifier
presentation_timestamp: u64, // Unix timestamp
disclosed_attributes: BoundedVec<DisclosedAttribute, 64>,
smt_proof: SmtInclusionProof,
device_signature: DeviceSignature,
proximity_attestation: Option<ProximityProofData>, // Optional
}
Disclosure Boundaries: May disclose 0 attributes (proof of possession only) up to attr_count. Disclosing all attributes is valid but defeats selective disclosure purpose.
6.3 DisclosedAttribute
DisclosedAttribute {
leaf_index: u32, // Position in sorted attribute tree (0-based)
key: BoundedString<64>, // Attribute key
value: BoundedString<1024>, // Attribute value (UTF-8)
salt: [u8; 32], // Per-attribute random salt
merkle_proof: BoundedVec<MerkleProofNode, 8>, // Sibling hashes, leaf to root
}
MerkleProofNode {
sibling_hash: [u8; 32],
}
Direction at each level is inferred from leaf_index: even index = left child, odd index = right child. No explicit direction field.
6.4 SmtInclusionProof
SmtInclusionProof {
smt_root: [u8; 32],
siblings: BoundedVec<SmtSibling, 256>, // Sparse — only non-empty siblings
sibling_count: u8,
leaf_status: u8, // STATUS_VALID, STATUS_REVOKED, STATUS_SUSPENDED
}
SmtSibling {
depth: u8, // 0–255
sibling_hash: [u8; 32],
}
Siblings MUST be sorted strictly ascending by depth. No duplicate depths. sibling_count MUST equal the array length. Implementations MUST reject proofs violating these constraints before any cryptographic operations.
6.5 DeviceSignature
DeviceSignature {
device_public_key: [u8; 1952], // ML-DSA-65 public key
signature: [u8; 3309], // ML-DSA-65 signature over device_sig_input (§7.7)
}
No timestamp field — the presentation_timestamp serves this purpose.
6.6 ProximityProofData
ProximityProofData {
proof_hash: [u8; 32],
proximity_timestamp: u64,
proximity_nonce: [u8; 32],
observer_device_pubkey_hash: [u8; 32],
}
6.7 RevocationSnapshotV1
RevocationSnapshotV1 {
issuer_id: [u8; 32],
epoch: u64, // Monotonic counter — prevents rollback
smt_root: [u8; 32],
issued_at: u64,
signature: [u8; 3309], // ML-DSA-65 over snapshot_sig_input (§7.8)
}
6.8 DelegationCredentialV1
DelegationCredentialV1 {
// Base fields (same field names as CredentialV1 §6.1; separate struct — see implementation note)
version: u8, // MUST be 0x01
credential_type: u8, // MUST be 0x02 (Delegation)
credential_id: [u8; 32],
issuer_id: [u8; 32],
holder_id: [u8; 32], // The delegated agent's identity
issued_at: u64, // Unix timestamp
expires_at: u64, // Unix timestamp
attr_count: u32, // Number of agent attestation attributes (may be 0)
attr_root: [u8; 32], // Attribute Merkle tree root (§8)
// Delegation-specific fields
delegator_credential_id: [u8; 32], // Parent credential ID (all zeros for root delegation)
delegation_depth: u8, // 0 = root; max MAX_DELEGATION_DEPTH
max_delegation_depth: u8, // Ceiling for sub-delegation (≤ MAX_DELEGATION_DEPTH)
scope_hash: [u8; 32], // SHA3-256 of canonical CBOR of ScopeConstraints (§7.16)
}
SignedDelegationCredential {
credential: DelegationCredentialV1,
signature: [u8; 3309], // ML-DSA-65 over deleg_sig_input (§7.15)
}
Boundary Conditions:
delegation_depthMUST be ≤max_delegation_depthMUST be ≤ MAX_DELEGATION_DEPTH (5)delegator_credential_idMUST be all zeros iffdelegation_depth == 0delegator_credential_idMUST NOT be all zeros iffdelegation_depth > 0issued_atMUST be <expires_atattr_countMAY be 0 (no agent attestation attributes required)
Implementation note: DelegationCredentialV1 is a separate struct, not a
wrapper around CredentialV1. The CBOR wire format places all fields (base +
delegation-specific) in a single canonical map with interleaved keys per §12.5
canonical ordering. A wrapper struct would complicate this serialisation. Common
logic is extracted into helper functions.
6.9 ScopeConstraints
ScopeConstraints {
actions: ActionVec, // Permitted action identifiers (heapless::Vec<AttributeKey, 32> / Vec<String>)
resource_patterns: ResourceVec, // Permitted resource patterns (heapless::Vec<ResourcePattern, 64> / Vec<String>)
max_value: Option<u64>, // Per-action monetary limit (e.g., cents)
max_daily_value: Option<u64>, // Aggregate daily monetary limit
max_actions_per_hour: Option<u32>, // Rate limit
time_window: Option<TimeWindow>, // Permitted time range
required_attestations: AttestationVec, // Required agent attestation keys (heapless::Vec<AttributeKey, 16> / Vec<String>)
}
TimeWindow {
start_hour: u8, // 0–23 UTC
end_hour: u8, // 0–23 UTC
days_of_week: u8, // Bitmask: bit 0 = Monday, bit 6 = Sunday
}
Optional field encoding: Option::None fields MUST be omitted from the CBOR
map entirely (not encoded as CBOR null). This matches §12.4. The scope_hash is
computed over the canonical CBOR of present fields only.
Array ordering: actions and resource_patterns arrays MUST be sorted
lexicographically (UTF-8 byte order) before encoding. This ensures scope_hash
is deterministic regardless of insertion order.
Scope attenuation: A child ScopeConstraints S' is a valid attenuation of
parent S iff S' ⊆ S:
actions: S'.actions ⊆ S.actions (exact string match after NFC normalisation)resource_patterns: S'.resource_patterns ⊆ S.resource_patterns (exact string match — patterns are opaque strings, not globs; application-layer interpretation is outside the protocol)max_value— Monotonic narrowing rule:- Parent None, child None → valid (both unlimited)
- Parent None, child Some(y) → valid (narrowing)
- Parent Some(x), child Some(y) where y ≤ x → valid (narrowing or equal)
- Parent Some(x), child None → INVALID (would widen scope)
- Parent Some(x), child Some(y) where y > x → INVALID (would widen scope)
max_daily_value: Same monotonic narrowing rule asmax_valuemax_actions_per_hour: Same monotonic narrowing rule asmax_valuetime_window: S'.time_window ⊆ S.time_window (start_hour ≥, end_hour ≤, days_of_week is bitwise subset). Parent None means unrestricted; child None when parent is Some is INVALIDrequired_attestations: S'.required_attestations ⊇ S.required_attestations (child may add requirements, never remove)
Rationale for monotonic narrowing: Once a limit is set in a parent
delegation, all children must preserve or tighten it. If a parent sets
max_value = Some(5000) and a child omits it (None = "no limit"), the child
would have broader authority than the parent — this is a scope widening attack.
6.10 ActionRequest
ActionRequest {
action: AttributeKey, // heapless::String<64> / String
resource: ResourcePattern, // heapless::String<256> / String
value: Option<u64>, // Monetary value of the action (None if not applicable)
timestamp: u64, // Unix timestamp of the request
request_nonce: [u8; 32], // CSPRNG nonce — prevents action replay
}
New collection type: ResourcePattern = heapless::String<256> / String,
following the existing AttributeKey/AttributeValue pattern in
collections.rs.
6.11 DelegatedActionPresentation
DelegatedActionPresentation {
delegation_chain: DelegationChainVec, // heapless::Vec<SignedDelegationCredential, 6> / Vec<...>
action_request: ActionRequest,
scope_constraints: ScopeConstraints, // The leaf agent's scope (convenience — authoritative is scope_hash)
presentation: PresentationV1, // Standard V1 presentation from the agent (§6.2)
}
New collection type: DelegationChainVec =
heapless::Vec<SignedDelegationCredential, 6> / Vec<...> in collections.rs.
Max 6 = MAX_DELEGATION_DEPTH + 1 (root credential + up to 5 sub-delegation
levels).
scope_constraints trust model: The scope_constraints field is included for
the verifier's convenience (display/logging of permitted scope). The
authoritative scope is the scope_hash in the signed delegation credential.
Verifiers MUST compute compute_scope_hash(presented_scope_constraints) and
compare it (constant-time) to the leaf credential's scope_hash before trusting
the presented constraints (see §13.3 step 5). A presenter who modifies the scope
constraints will fail this hash check.
6.12 Chain Linking Attributes
Chain linking is implemented as reserved attribute keys on any credential type (0x01, 0x02, 0x04). No separate credential_type is used.
| Attribute Key | Format | Description |
|---|---|---|
chain_id |
hex string, 64 chars (SHA3-256 in hex) | Identifies the chain (§7.19) |
chain_seq |
decimal string, "1"-based | Sequence number within the chain |
chain_prev |
hex string, 64 chars | SHA3-256 of previous credential CBOR (§7.20); all zeros for first entry |
Semantics: A credential is a chain member iff it contains all three attributes. These attributes MAY appear on any credential type.
First-entry convention: chain_prev = 64 zeros (hex), chain_seq = "1".
Chain membership detection: Verifiers detect chain membership by attribute presence, not by credential_type.
6.13 Content Attestation Attributes
Content attestation is implemented as reserved attribute keys on a credential
with credential_type = 0x04. The following attribute keys are reserved:
| Attribute Key | Type | Required | Description |
|---|---|---|---|
content_hash |
string, 73 chars (sha3-256: + 64 hex chars) |
REQUIRED | Content hash with algorithm prefix (§7.21) |
content_type |
string (MIME type) | RECOMMENDED | MIME type of the attested content |
creator_id |
string | OPTIONAL | Issuer-assigned creator identifier |
creation_method |
enum string | REQUIRED | How the content was created (see values below) |
model_id |
string | CONDITIONAL | AI model identifier — REQUIRED when creation_method is ai_assisted or ai_generated |
CreationMethod enum: Implementations MUST define a CreationMethod type
with the following variants and their canonical string serialisations:
| Variant | String Value |
|---|---|
| HumanAuthored | "human_authored" |
| AiAssisted | "ai_assisted" |
| AiGenerated | "ai_generated" |
| Automated | "automated" |
| Scanned | "scanned" |
| Transcribed | "transcribed" |
| Composite | "composite" |
Implementations MUST reject unrecognised string values with ErrCreationMethodInvalid (0x8005).
model_id requirement: MUST be present when creation_method is
ai_assisted or ai_generated. SHOULD be absent otherwise.
7. Cryptographic Constructions
All constructions use SHA3-256. Inputs are concatenated in the order shown. All multi-byte integers are big-endian.
7.1 Issuer ID
issuer_id = SHA3-256(EXQUB_ISSUER_V1 || issuer_public_key)
- issuer_public_key: 1952 bytes (ML-DSA-65)
7.2 Credential ID
issuer_id = SHA3-256(EXQUB_ISSUER_V1 || issuer_public_key)
credential_id = SHA3-256(EXQUB_CRED_ID_V1 || issuer_id || counter_u64_BE || issued_at_u64_BE)
- counter: Monotonic, persisted with WAL, never reused. See §7.2.1.
7.2.1 Counter Persistence
The counter MUST be persisted in durable storage. MUST be incremented atomically before each issuance. MUST NEVER be reused after restart. Issuers MUST implement UNIQUE constraints on (issuer_public_key, counter, issued_at).
Counter Protocol: Initialize at 0. Increment atomically with write-ahead logging. On overflow at 2^64-1: fail closed, require new issuer key. Recovery after corruption: fail closed, require new issuer key. No wrap-around permitted.
7.3 Credential Signature Input
sig_input = SHA3-256(
EXQUB_SIG_V1 || // 16 bytes
version || // 1 byte
credential_type || // 1 byte
credential_id || // 32 bytes
issuer_id || // 32 bytes
holder_id || // 32 bytes
issued_at_u64_BE || // 8 bytes
expires_at_u64_BE || // 8 bytes
attr_count_u32_BE || // 4 bytes
attr_root // 32 bytes
)
// Total input: 166 bytes
signature = ML_DSA_65_Sign(issuer_private_key, sig_input)
7.4 Holder ID
Option 1 — Issuer-Assigned (recommended):
// When holder_public_key is empty:
holder_id = SHA3-256(EXQUB_HOLDER_V1 || issuer_id || issuer_nonce)
// When holder_public_key is present:
holder_id = SHA3-256(EXQUB_HOLDER_V1 || issuer_id || holder_public_key)
- issuer_nonce: Exactly 32 bytes, CSPRNG-generated, unique per holder per policy.
Option 2 — Self-Sovereign:
holder_id = SHA3-256(EXQUB_HOLDER_V1 || len(pk)_u32_BE || public_key_bytes)
Holder ID MUST be globally unique within an issuer's scope. Different issuers MUST use different holder IDs for the same holder. Deployment profiles MUST declare correlation resistance policy (HIGH PRIVACY: different holder IDs per credential; STANDARD: same holder ID within an issuer).
7.5 Attribute Leaf Hash
leaf_hash = SHA3-256(
EXQUB_ATTR_LEAF_V1 || // 16 bytes
len(key)_u16_BE || // 2 bytes
key_bytes || // variable
salt || // 32 bytes
len(value)_u16_BE || // 2 bytes
value_bytes // variable
)
Each attribute MUST have a unique 32-byte CSPRNG salt. Key length prefix is u16 BE (not u32).
7.6 Attribute Node Hash
node_hash = SHA3-256(EXQUB_ATTR_NODE_V1 || left_child || right_child)
7.7 Presentation Hash and Device Signature
Disclosed Keys Hash (keys MUST be sorted lexicographically by raw UTF-8 bytes before hashing):
disclosed_keys_hash = SHA3-256(
len(key_1)_u16_BE || key_1 || len(key_2)_u16_BE || key_2 || ... || len(key_n)_u16_BE || key_n
)
Presentation Hash:
presentation_hash = SHA3-256(
EXQUB_PRES_HASH_V1 || // 16 bytes
nonce_v || // 32 bytes
verifier_id || // 32 bytes
credential_id || // 32 bytes
presentation_timestamp_u64_BE || // 8 bytes
disclosed_count_u32_BE || // 4 bytes
disclosed_keys_hash || // 32 bytes
attr_root || // 32 bytes
smt_root // 32 bytes
)
Device Key Hash:
device_pubkey_hash = SHA3-256(EXQUB_DEV_KEY_V1 || device_public_key)
Device Signature Input:
device_sig_input = SHA3-256(EXQUB_DEV_BIND_V1 || presentation_hash || device_pubkey_hash)
device_signature = ML_DSA_65_Sign(device_private_key, device_sig_input)
The device signs the 32-byte hash, NOT the raw concatenation.
7.8 Revocation Snapshot Signature
snapshot_sig_input = SHA3-256(
EXQUB_REV_SNAP_V1 || issuer_id || epoch_u64_BE || smt_root || issued_at_u64_BE
)
7.9 Proximity Attestation
observer_key_hash = SHA3-256(EXQUB_DEV_KEY_V1 || observer_device_pubkey)
proof_hash = SHA3-256(
EXQUB_PROX_PROOF_V1 || // 16 bytes
credential_id || // 32 bytes
observer_key_hash || // 32 bytes
proximity_timestamp_u64_BE || // 8 bytes
proximity_nonce // 32 bytes
)
7.10 Padding Leaf
padding_leaf = SHA3-256(EXQUB_ATTR_PAD_V1 || [0u8; 32])
All padding leaves are identical. No salt, no randomness.
7.11 SMT Leaf Hash
smt_leaf_hash = SHA3-256(EXQUB_SMT_LEAF_V1 || credential_id || status_byte)
7.12 SMT Internal Node Hash (Depth-Bound)
smt_node_hash = SHA3-256(EXQUB_SMT_NODE_V1 || depth_byte || left_child || right_child)
depth_byte is a single unsigned byte (0–255).
7.13 SMT Empty Nodes
empty[256] = SHA3-256(EXQUB_SMT_EMPTY_V1)
empty[d] = SHA3-256(EXQUB_SMT_NODE_V1 || d_byte || empty[d+1] || empty[d+1]) for d = 255..0
257 entries (indices 0–256). MUST be stored in .rodata (flash/ROM), NOT on the stack.
Storage Requirements: Precompute at compile-time when possible. For no_std: compute on-demand with O(256-depth) iteration. For std: initialize once, cache in static memory. Total size: 8.2KB (257 × 32 bytes). Alignment: 32-byte boundary for cache efficiency.
7.14 SMT Leaf Position
path_index = SHA3-256(credential_id)
The path_index is a 256-bit big-endian value. Bit extraction:
bit = (path_index[depth / 8] >> (7 - (depth % 8))) & 1. Bit 0 = MSB of byte 0.
Bit 255 = LSB of byte 31.
7.15 Delegation Credential Signature Input
deleg_sig_input = SHA3-256(
EXQUB_DELEG_V1 || // 16 bytes
version || // 1 byte (0x01)
credential_type || // 1 byte (0x02)
credential_id || // 32 bytes
issuer_id || // 32 bytes
holder_id || // 32 bytes
issued_at_u64_BE || // 8 bytes
expires_at_u64_BE || // 8 bytes
attr_count_u32_BE || // 4 bytes
attr_root || // 32 bytes
delegator_credential_id || // 32 bytes
delegation_depth || // 1 byte
max_delegation_depth || // 1 byte
scope_hash // 32 bytes
)
// Total preimage: 232 bytes
signature = ML_DSA_65_Sign(issuer_private_key, deleg_sig_input)
7.16 Scope Hash
scope_hash = SHA3-256(EXQUB_SCOPE_V1 || canonical_cbor(scope_constraints))
Where canonical_cbor(scope_constraints) follows §12.5 canonical ordering:
None fields omitted, arrays sorted lexicographically before encoding.
7.17 Action Request Hash
action_request_hash = SHA3-256(
EXQUB_ACTION_V1 || // 16 bytes
action_len_u16_BE || // 2 bytes
action_utf8 || // variable
resource_len_u16_BE || // 2 bytes
resource_utf8 || // variable
value_u64_BE || // 8 bytes (0 if None)
timestamp_u64_BE || // 8 bytes
request_nonce // 32 bytes
)
Length prefixes use u16 BE (2 bytes), matching the convention from §7.5 attribute leaf hashes.
7.18 Sub-Delegation Signature Input
subdel_sig_input = SHA3-256(
EXQUB_SUBDEL_V1 || // 16 bytes
parent_credential_id || // 32 bytes
child_credential_id || // 32 bytes
child_holder_id || // 32 bytes
child_scope_hash || // 32 bytes
child_issued_at_u64_BE || // 8 bytes
child_expires_at_u64_BE || // 8 bytes
child_delegation_depth // 1 byte
)
// Total preimage: 161 bytes
Sub-delegation is signed by the delegator's device key (randomised ML-DSA-65), not the issuer key. This is an intentional, explicit act — the delegator knowingly grants authority to a sub-agent. See §18 for the device key linkability tradeoff.
7.19 Chain ID Derivation
chain_id = SHA3-256(
EXQUB_CHAIN_V1 || // 16 bytes
issuer_id || // 32 bytes
chain_name_len_u16_BE || // 2 bytes (u16 BE — consistent with §7.5 convention)
chain_name_utf8 // 1–256 bytes (NFC normalised)
)
The resulting 32-byte hash is encoded as lowercase hex (64 chars) when stored as
the chain_id attribute value.
7.20 Chain Previous Hash
chain_prev = SHA3-256(previous_credential_cbor_bytes)
Input is the complete canonical CBOR bytes of the previous credential as issued
(including signature). No domain separator — the CBOR bytes are self-describing.
The resulting 32-byte hash is encoded as lowercase hex (64 chars) when stored as
the chain_prev attribute value. For the first entry, chain_prev attribute
value is 64 zero characters.
7.21 Content Hash
content_hash_value = SHA3-256(raw_content_bytes)
content_hash_attribute = "sha3-256:" || hex_lowercase(content_hash_value)
No domain separator on content hash — the sha3-256: prefix serves this purpose
and enables future algorithm agility. Total attribute length: 73 characters
(9-char prefix + 64-char hex). Implementations MUST reject unknown prefixes
(error 0x8002).
8. Attribute Merkle Tree
8.1 Construction
- Validate: attr_count == salts.length, attr_count ≤ MAX_ATTRIBUTES, no duplicate keys
- Sort attributes lexicographically by key (UTF-8 bytewise comparison, stable sort)
- Compute leaf hashes using §7.5, with each attribute's original salt
- Pad to next_power_of_2(attr_count) using padding leaves (§7.10)
- Build bottom-up: pair leaves, compute parent via §7.6
- Root = final single hash
8.2 Verification
Given a DisclosedAttribute with leaf_index, key, value, salt, and merkle_proof against expected_root and attr_count:
- Reject if leaf_index ≥ attr_count (padding leaf disclosure)
- Compute tree_size = next_power_of_2(attr_count)
- Compute tree_depth = trailing_zeros(tree_size) — the ACTUAL depth, not MAX_TREE_DEPTH
- Reject if merkle_proof.length ≠ tree_depth
- Compute leaf_hash via §7.5
- Walk proof bottom-up: at each level, if current_index is even, current is left child; if odd, current is right child. Compute parent via §7.6. Divide index by 2.
- Compare final hash to expected_root using constant-time comparison
8.3 Sorting
Attributes are sorted by key using UTF-8 bytewise lexicographic comparison. Keys MUST be unique. Sort MUST be stable and deterministic.
9. Sparse Merkle Tree (SMT)
9.1 Structure
The SMT is a 256-level binary tree. Each credential maps to a leaf position via §7.14. Leaf values are §7.11 (leaf hash binding credential_id to status). Internal nodes use §7.12 (depth-bound hashing). Empty subtrees use precomputed values from §7.13.
9.2 Proof Verification
Given leaf_hash, path_index (32 bytes), and sorted sparse siblings:
- Validate: siblings.length ≤ 256, strictly ascending depths, no duplicates
- Set current_hash = leaf_hash, sibling_cursor = last index (deepest sibling)
- For level = 256 down to 1:
- parent_depth = level - 1
- If sibling at cursor has depth == parent_depth: use it, decrement cursor
- Else: use PRECOMPUTED_EMPTY_NODES[parent_depth]
- Extract bit from path_index at parent_depth (§7.14)
- bit == 0: left = current_hash, right = sibling_hash
- bit == 1: left = sibling_hash, right = current_hash
- current_hash = smt_node_hash(parent_depth, left, right) via §7.12
- Assert all siblings consumed (cursor == -1). Reject if not.
- Return current_hash (computed root)
This algorithm uses O(1) stack space. MUST NOT allocate a 256-element lookup table.
9.3 Membership Semantics
A credential is valid if and only if it has an explicit SMT entry with STATUS_VALID (0x00). Non-membership proofs (empty leaf) MUST be rejected. STATUS_REVOKED → reject. STATUS_SUSPENDED → reject or require step-up authentication per deployment policy. Unknown status values → reject.
9.4 Revocation Snapshots
Verifiers accept snapshots by: (1) verifying ML-DSA-65 signature, (2) checking epoch > last accepted epoch (monotonicity), (3) evaluating freshness against MAX_SMT_ROOT_AGE. If root age exceeds threshold, the core engine returns STATUS_STALE_ROOT as a warning — the application layer decides whether to fail closed or accept with audit flag.
Verifiers MUST persistently store (issuer_id, epoch, root) tuples. State MUST survive restarts. Storage failure → fail closed.
Snapshot Protocol: Distribution is out-of-band (HTTPS, IPFS, etc.). Maximum snapshot size: 16KB. Fallback when unavailable: use last known good with STATUS_STALE_ROOT warning. Snapshot MUST NOT contain credential data, only root and signature. Each epoch MUST have exactly one snapshot (error corrections require incrementing epoch).
Transport Security: When distributed via HTTPS, TLS 1.3+ SHOULD be used.
10. Device Co-Signatures
Algorithm: ML-DSA-65. Signing input computed via §7.7 (device_sig_input).
Key Lifecycle: Generated in TEE using CSPRNG. Private key MUST NOT leave TEE. No rotation in v1.0. Recovery requires credential re-issuance. Key hash is 32 bytes for efficiency.
Device Loss Recovery: No key recovery protocol. Lost device requires credential reissuance with new device binding. Old credential SHOULD be revoked via SMT update.
11. Text Validation
Core engine (no_std): MUST reject null bytes (0x00) in text fields. MUST validate UTF-8. MUST NOT perform NFC normalization or combining character detection.
Issuers (std): MUST apply NFC normalization (Unicode 15.0) to all attribute keys and values before hashing. MUST verify idempotency: NFC(NFC(x)) == NFC(x). MUST reject null bytes. MUST enforce length limits (key ≤ 64 bytes, value ≤ 1024 bytes). MUST strip RTL markers before normalization.
Attribute key format: ^[a-zA-Z][a-zA-Z0-9_-]{0,63}$
Attribute Constraints: Keys MUST be unique within a credential. Empty strings prohibited for both keys and values. Maximum 64 attributes per credential (enforced at issuance). Binary data NOT supported in v1.0 — UTF-8 text only. Attribute modification requires full credential reissuance.
Internationalization: UTF-8 only, no BOM. Homograph attacks prevented via key format restrictions. Display names NOT supported — protocol names only. Language tags NOT supported in v1.0.
12. CBOR Encoding
12.1 Canonical Rules
All CBOR MUST conform to RFC 8949 §4.2 deterministic encoding:
- Definite-length encodings only (reject 0x9F, 0xBF, 0x7F, 0x5F)
- Shortest integer representation (reject redundant bytes)
- Map keys sorted by encoded byte order (shorter first, then lexicographic)
- Map key uniqueness (reject duplicates)
- No semantic tags (reject Major Type 6)
- No undefined values (reject 0xF7)
- No floating-point values (reject Major Type 7 values 25, 26, 27)
- Text strings: valid UTF-8, no null bytes
12.2 Streaming Parser
The parser MUST enforce MAX_CBOR_DEPTH during parsing. MUST validate canonical encoding rules during the initial parse (not as a separate pass). MUST operate within the 4KB stack budget. MUST reject trailing bytes after the top-level item.
12.3 Size Limits
During parsing, enforce: byte strings ≤ MAX_CBOR_BYTE_STRING, text strings ≤ MAX_CBOR_TEXT_STRING, arrays ≤ MAX_CBOR_ARRAY_LENGTH, maps ≤ MAX_CBOR_MAP_ENTRIES, total input ≤ MAX_PRESENTATION_SIZE.
12.4 Optional Fields
When an Option
Field Presence Rules: All fields in CredentialV1, SignedCredential, SmtInclusionProof, SmtSibling, DeviceSignature are required. In PresentationV1: proximity_attestation is optional (omit key if absent). Zero values MUST be encoded (not omitted). Unknown fields MUST cause parse failure — no forward compatibility. Arrays under capacity encode actual length, not capacity.
12.5 Wire Format Keys
CBOR map keys are text strings. Canonical key ordering follows RFC 8949 §4.2 (sorted by encoded byte length, then lexicographic).
CredentialV1: "attr_count", "attr_root", "credential_id",
"credential_type", "expires_at", "holder_id", "issued_at",
"issuer_id", "version"
SignedCredential: "credential", "signature"
DisclosedAttribute: "key", "leaf_index", "merkle_proof", "salt",
"value"
SmtInclusionProof: "leaf_status", "siblings", "smt_root"
SmtSibling: "depth", "sibling_hash"
DeviceSignature: "device_public_key", "signature"
PresentationV1: "credential", "device_signature",
"disclosed_attributes", "nonce_v", "presentation_timestamp",
"proximity_attestation" (if present), "smt_proof", "verifier_id"
ProximityProofData: "observer_device_pubkey_hash", "proof_hash",
"proximity_nonce", "proximity_timestamp"
DelegationCredentialV1 (canonical order by CBOR encoded key length, then
lexicographic): "version", "attr_root", "holder_id", "issued_at",
"issuer_id", "attr_count", "expires_at", "scope_hash",
"credential_id", "credential_type", "delegation_depth",
"max_delegation_depth", "delegator_credential_id"
Key encoding byte counts: "version" (7→8),
"attr_root"/"holder_id"/"issued_at"/"issuer_id" (9→10),
"attr_count"/"expires_at"/"scope_hash" (10→11), "credential_id" (13→14),
"credential_type" (15→16), "delegation_depth" (16→17),
"max_delegation_depth" (20→21), "delegator_credential_id" (23→24).
Must verify: These byte counts are a sanity check only. Pinning tests (Task
2.7.11) MUST be generated from actual CBOR encoder output, not from manual
counting. The encoder is authoritative — the H7 bug (proximity_attestation vs
presentation_timestamp ordering) proves this step is load-bearing.
SignedDelegationCredential: "signature", "credential"
ScopeConstraints (optional fields omitted when None): "actions",
"max_value", "time_window", "max_daily_value", "resource_patterns",
"max_actions_per_hour", "required_attestations"
Key encoding byte counts: "actions" (7→8), "max_value" (9→10),
"time_window" (11→12), "max_daily_value" (15→16), "resource_patterns"
(17→18), "max_actions_per_hour" (20→21), "required_attestations" (22→23).
ActionRequest (canonical order): "value", "action", "resource",
"timestamp", "request_nonce"
Key encoding byte counts: "value" (5→6), "action" (6→7), "resource" (8→9),
"timestamp" (9→10), "request_nonce" (13→14).
DelegatedActionPresentation (canonical order): "presentation",
"action_request", "delegation_chain", "scope_constraints"
Key encoding byte counts: "presentation" (12→13), "action_request" (14→15),
"delegation_chain" (16→17), "scope_constraints" (17→18).
TimeWindow: "end_hour", "start_hour", "days_of_week"
13. Verification Procedure
Verifiers MUST execute checks in this exact order (10-step pipeline). MUST NOT proceed if any step fails. MUST NOT reorder steps.
Verification Invariants: No partial verification results — atomic pass/fail only. Memory cleanup required on any failure. Cryptographic comparisons MUST use constant-time operations. The verification steps for a single presentation MUST be executed in sequential order. Multiple independent presentations MAY be verified in parallel. Audit logging SHOULD be performed for all failures.
Step 1 — Parse and Validate CBOR (cheap): Parse presentation_bytes with canonical CBOR validation (§12). Reject if non-canonical, oversized, or malformed.
Step 2 — Version and Type Check (cheap): Reject if version ≠ 0x01 (error 0x1001). Reject if credential_type ∉ {0x01, 0x02, 0x04} (error 0x1005). For delegation presentations, dispatch to §13.3 after completing steps 3–10.
Step 3 — Freshness and Nonce (cheap): Reject if |presentation_timestamp - current_time| > max_clock_skew. Reject if nonce_v ≠ expected verifier nonce (constant-time comparison).
Step 4 — Work Bounding (cheap): Reject if disclosed_attributes count > MAX_ATTRIBUTES (64). Reject if SMT sibling count > MAX_SMT_PROOF_DEPTH.
Step 5 — SMT Membership (hashing only): Verify SMT inclusion proof via §9.2. Compare computed root to expected root (constant-time). Reject non-membership. Reject if leaf_status ≠ STATUS_VALID.
Step 6 — Credential Signature (expensive — ML-DSA-65): Compute sig_input via §7.3. Verify ML-DSA-65 signature against issuer_public_key.
Step 7 — Credential Validity Window: Reject if issued_at ≥ expires_at. Reject if current_time < issued_at - max_clock_skew. Reject if current_time > expires_at + max_clock_skew.
Step 8 — Merkle Proofs (moderate): For each disclosed attribute, verify via §8.2 against credential's attr_root. Reject any padding leaf disclosure (leaf_index ≥ attr_count).
Step 9 — Device Signature (expensive — ML-DSA-65): Compute device_pubkey_hash via §7.7. Compute presentation_hash via §7.7. Compute device_sig_input via §7.7. Verify ML-DSA-65 signature against device_public_key.
Step 10 — Policy (variable): Check required attributes per policy. Verify proximity attestation if required (§13.1). Apply deployment-specific constraints.
13.1 Proximity Attestation Verification
If present: verify proof freshness (|proximity_timestamp - current_time| ≤ max_proximity_age). Verify temporal correlation (|presentation_timestamp - proximity_timestamp| ≤ 60 seconds). Verify observer device trust (match proof_hash against expected hash computed via §7.9 using trusted observer keys, checking both current and previous nonce for rotation window). If policy requires proximity and attestation is absent, reject.
Proximity Protocol: Observers register out-of-band with verifier. Implementations SHOULD support at least 256 trusted observers. Nonce rotation every 300 seconds with 60-second overlap window. Proximity proof expires after 60 seconds. Observer device keys use same ML-DSA-65 as credentials.
13.2 Failure Recovery
After Failed Verification: No automatic retry. Log failure with error code. Clear all temporary state. No information leakage about failure reason to untrusted parties.
Rate Limiting: Application layer MAY implement rate limiting. No protocol-level rate limiting requirements.
13.3 Delegation Chain Verification
Verifiers MUST execute these steps in order for a DelegatedActionPresentation.
MUST NOT reorder.
Step 1 — Chain structure: Reject if delegation_chain is empty (error
0x600C). Reject if length > MAX_DELEGATION_DEPTH + 1 (error 0x600D).
Step 2 — Per-link depth check: For each link i, verify
delegation_depth == i and delegation_depth ≤ max_delegation_depth (error
0x6001, 0x6002).
Step 3 — Per-link temporal attenuation: For each adjacent pair (parent,
child), verify child.expires_at ≤ parent.expires_at (error 0x6009).
Step 4 — Per-link chain continuity: For each adjacent pair (parent, child),
verify child.delegator_credential_id == parent.credential_id (error 0x6008).
Verify root has delegator_credential_id all zeros (error 0x6003).
Step 5 — Scope hash verification: Compute
compute_scope_hash(presented_scope_constraints) via §7.16 and compare
(constant-time) to the leaf credential's scope_hash. Reject on mismatch (error
0x600E). This step prevents scope inflation attacks — a presenter cannot claim
broader scope than what was signed into the credential.
Step 6 — Per-link scope attenuation: Verify each child scope ⊆ parent scope using the monotonic narrowing rules in §6.9. Reject on violation (error 0x6006).
Step 7 — Per-link ML-DSA-65 signature verification (expensive — deferred to last per-link step): Verify each delegation credential's signature via §7.15 against the issuer's public key (error 0x600A).
Step 8 — Action against scope: Verify the action_request.action and
action_request.resource are permitted by the leaf credential's scope (error
0x6005).
Step 9 — Agent presentation verification: Invoke the 10-step core
verification pipeline (§13, steps 1–10) on the leaf agent's PresentationV1.
DoS resistance: Steps 1–4 (structural, O(n)) execute before steps 5–6 (scope computation) before step 7 (ML-DSA-65). This ordering ensures cheap checks gate expensive cryptographic operations.
Return type on success: DelegationChainVerificationResult
| Field | Type | Description |
|---|---|---|
chain_depth |
u8 | Depth of the verified chain (0 = root only) |
root_credential_id |
[u8; 32] | credential_id of the root delegation |
leaf_credential_id |
[u8; 32] | credential_id of the leaf (agent) delegation |
leaf_scope_hash |
[u8; 32] | scope_hash of the leaf delegation (authoritative scope) |
13.4 Chain Linking Verification
Verifiers receiving a sequence of chain-linked credentials MUST verify integrity in this order:
Step 1 — Attribute presence: Reject any credential missing chain_id,
chain_seq, or chain_prev (error 0x7006).
Step 2 — Chain ID consistency: Verify all credentials share the same
chain_id (error 0x7001).
Step 3 — Issuer consistency: Verify all credentials share the same
issuer_id (error 0x7007).
Step 4 — Sequence numbering: Verify chain_seq values form a gapless
ascending sequence starting at "1" (error 0x7002, 0x7005).
Step 5 — First-entry sentinel: Verify the first entry has chain_prev = 64
zero characters (error 0x7004).
Step 6 — Hash chain integrity: For each entry i > 1, compute SHA3-256 of
credential i-1's canonical CBOR bytes and compare to chain_prev of entry i
(constant-time comparison; error 0x7003).
Step 7 — Per-credential verification: Invoke the core 10-step pipeline (§13, steps 1–10) for each credential. In strict mode, a revoked entry (0x7008) fails the entire chain. In audit mode, revoked entries are flagged but the chain is accepted for historical review.
DoS resistance: Steps 1–5 (structural, O(n)) execute before step 6 (SHA3-256, O(n)) before step 7 (ML-DSA-65 × n, expensive).
ChainVerificationMode: Callers MUST specify one of:
Strict— any revoked entry returns ErrChainRevokedEntry (0x7008) and aborts verificationAudit— revoked entries are flagged in the result but verification continues; for historical/regulatory review
Return type on success: ChainVerificationResult
| Field | Type | Description |
|---|---|---|
verified_entry_count |
usize | Number of credentials verified |
chain_id |
[u8; 32] | The chain identifier (binary form) |
revoked_indices |
Vec<usize> | Indices of revoked entries (Audit mode only; empty in Strict mode) |
13.5 Content Attestation Verification
Step 1 — Credential type check: Verify credential_type == 0x04 (error
0x1005).
Step 2 — Core verification: Invoke the 10-step core pipeline (§13, steps 1–10) on the attestation credential's presentation.
Step 3 — Required attributes: Verify content_hash and creation_method
are among the disclosed attributes (error 0x8004, 0x8005).
Step 4 — Hash prefix validation: Verify content_hash value begins with
sha3-256: (error 0x8002). Verify the hex portion is exactly 64 lowercase hex
characters (error 0x8003).
Step 5 — model_id requirement: If creation_method is ai_assisted or
ai_generated, verify model_id is disclosed (error 0x8006).
Step 6 — Content hash verification: Compute SHA3-256 of the content bytes
and compare (constant-time) to the hex-decoded hash from the content_hash
attribute (error 0x8001).
Return type on success: ContentAttestationResult
| Field | Type | Description |
|---|---|---|
content_hash |
[u8; 32] | The verified SHA3-256 hash of the content |
creation_method |
CreationMethod | The verified creation method (§6.13) |
credential_id |
[u8; 32] | The attesting credential's ID |
14. Replay Prevention
Core engine (no_std, stateless): Performs timestamp bounds check and nonce match only. Does NOT maintain state.
Application layer (stateful): MUST track used presentation hashes (§7.7) as replay cache keys. The presentation_hash already includes nonce_v — no additional hashing needed. Minimum retention: 900 seconds. Maximum: 86400 seconds. Maximum entries: 100,000 with LRU eviction. MUST NOT silently evict unexpired entries.
15. Error Codes
| Range | Category |
|---|---|
| 0x1000–0x1FFF | Payload & Parsing |
| 0x2000–0x2FFF | Temporal & Replay |
| 0x3000–0x3FFF | Cryptographic & SMT |
| 0x4000–0x4FFF | Merkle Tree |
| 0x5000–0x5FFF | Policy & Attribute |
| 0x6000–0x6FFF | Delegation |
| 0x7000–0x7FFF | Chain Linking |
| 0x8000–0x8FFF | Content Attestation |
| Code | Name | Description |
|---|---|---|
| 0x1001 | ERR_UNSUPPORTED_VERSION | Version ≠ 0x01 |
| 0x1002 | ERR_CBOR_NON_CANONICAL | CBOR encoding violates canonical rules |
| 0x1003 | ERR_PARSING_LIMIT_EXCEEDED | Payload exceeds memory/stack limits |
| 0x1004 | ERR_MISSING_LEAF_INDEX | DisclosedAttribute missing leaf_index |
| 0x1005 | ERR_UNSUPPORTED_CREDENTIAL_TYPE | Credential type ∉ {0x01, 0x02, 0x04} |
| 0x2001 | ERR_PRESENTATION_EXPIRED | Presentation timestamp outside window |
| 0x2002 | ERR_CREDENTIAL_EXPIRED | Credential expires_at has passed |
| 0x2003 | ERR_CREDENTIAL_NOT_YET_VALID | Credential issued_at is in the future |
| 0x2004 | ERR_NONCE_REPLAYED | Presentation hash in replay cache |
| 0x2005 | ERR_PROXIMITY_STALE | Proximity attestation too old |
| 0x2006 | ERR_PROXIMITY_TEMPORAL_FAIL | Proximity/presentation timestamp mismatch |
| 0x2007 | STATUS_STALE_ROOT | SMT root age exceeds threshold (warning) |
| 0x3001 | ERR_INVALID_SIGNATURE | ML-DSA-65 verification failed |
| 0x3002 | ERR_SMT_DEPTH_VIOLATION | SMT proof exceeds bounds |
| 0x3003 | ERR_SMT_INVALID_ORDERING | SMT siblings not ascending or contain duplicates |
| 0x3004 | ERR_SMT_STATUS_REVOKED | Credential status is not valid |
| 0x3005 | ERR_DEVICE_KEY_MISMATCH | device_pubkey_hash mismatch |
| 0x3006 | ERR_SMT_PROOF_INVALID | SMT proof verification failed |
| 0x4001 | ERR_MERKLE_ROOT_MISMATCH | Computed root ≠ credential attr_root |
| 0x4002 | ERR_MERKLE_PROOF_INVALID | Merkle proof path verification failed |
| 0x4003 | ERR_PADDING_LEAF_DISCLOSED | Disclosed attribute references padding position |
| 0x5001 | ERR_MISSING_REQUIRED_ATTR | Required attribute not disclosed |
| 0x5002 | ERR_POLICY_VIOLATION | Presentation violates verifier policy |
| 0x5003 | ERR_UNTRUSTED_OBSERVER | Proximity observer not in trusted list |
| 0x6001 | ErrDelegationDepthExceeded | delegation_depth > MAX_DELEGATION_DEPTH |
| 0x6002 | ErrDelegationDepthMismatch | delegation_depth > max_delegation_depth |
| 0x6003 | ErrDelegationRootNotZero | Root delegation (depth=0) has non-zero delegator_credential_id |
| 0x6004 | ErrDelegationNonRootZero | Non-root delegation (depth>0) has zero delegator_credential_id |
| 0x6005 | ErrScopeViolation | Action not permitted by leaf scope constraints |
| 0x6006 | ErrScopeAttenuationFailed | Child scope is not a subset of parent scope |
| 0x6007 | ErrDelegationExpired | Delegation credential has expired |
| 0x6008 | ErrDelegationChainBroken | child.delegator_credential_id ≠ parent.credential_id |
| 0x6009 | ErrDelegationTemporalViolation | child.expires_at > parent.expires_at |
| 0x600A | ErrDelegationSignatureInvalid | ML-DSA-65 signature on delegation credential failed |
| 0x600B | ErrSubdelegationSignatureInvalid | Sub-delegation signature (device key) verification failed |
| 0x600C | ErrDelegationChainEmpty | Empty delegation chain in DelegatedActionPresentation |
| 0x600D | ErrDelegationChainTooLong | Chain length > MAX_DELEGATION_DEPTH + 1 |
| 0x600E | ErrDelegationScopeHashMismatch | Computed scope_hash ≠ leaf credential scope_hash |
| 0x600F | ErrDelegationParentRevoked | Parent delegation credential is revoked |
| 0x7001 | ErrChainIdMismatch | chain_id attribute does not match across entries |
| 0x7002 | ErrChainSeqGap | chain_seq values have a gap (non-consecutive) |
| 0x7003 | ErrChainPrevMismatch | chain_prev ≠ SHA3-256 of previous entry's CBOR |
| 0x7004 | ErrChainFirstEntryPrev | First entry chain_prev is not all zeros |
| 0x7005 | ErrChainFirstEntrySeq | First entry chain_seq is not "1" |
| 0x7006 | ErrChainMissingAttribute | Required chain attribute missing (chain_id, chain_seq, or chain_prev) |
| 0x7007 | ErrChainIssuerMismatch | Entries in chain have different issuer_id values |
| 0x7008 | ErrChainRevokedEntry | Chain entry is revoked (strict verification mode) |
| 0x8001 | ErrContentHashMismatch | Computed content hash ≠ attested content_hash |
| 0x8002 | ErrContentHashPrefixInvalid | Unrecognised hash algorithm prefix (only sha3-256: accepted) |
| 0x8003 | ErrContentHashLengthInvalid | Hex portion of content_hash is not exactly 64 characters |
| 0x8004 | ErrContentHashMissing | content_hash attribute not present in disclosed attributes |
| 0x8005 | ErrCreationMethodInvalid | Unrecognised creation_method value |
| 0x8006 | ErrModelIdRequired | model_id must be disclosed when creation_method is ai_assisted or ai_generated |
Implementations MUST return these exact hex codes. MUST NOT panic. All functions MUST return Result types.
Error Handling Protocol: Errors MUST bubble up immediately — no recovery attempts. Cryptographic errors use constant-time handling to prevent timing attacks. Stack unwinding in no_std: return error through Result chain. No error state persistence between operations. Partial results prohibited — atomic success or failure only.
16. Test Vectors
Implementations MUST produce identical outputs for these vectors.
16.1 Domain Separator Hex
EXQUB_ISSUER_V1 = [45,58,51,55,42,5f,49,53,53,55,45,52,5f,56,31,5f]
EXQUB_CRED_ID_V1 = [45,58,51,55,42,5f,43,52,45,44,5f,49,44,5f,56,31]
EXQUB_SIG_V1 = [45,58,51,55,42,5f,53,49,47,5f,56,31,5f,5f,5f,5f]
EXQUB_ATTR_LEAF_V1 = [45,58,51,55,42,5f,41,54,54,52,5f,4c,45,41,46,5f]
EXQUB_ATTR_NODE_V1 = [45,58,51,55,42,5f,41,54,54,52,5f,4e,4f,44,45,5f]
EXQUB_ATTR_PAD_V1 = [45,58,51,55,42,5f,41,54,54,52,5f,50,41,44,5f,5f]
EXQUB_SMT_EMPTY_V1 = [45,58,51,55,42,5f,53,4d,54,5f,45,4d,50,54,59,5f]
EXQUB_SMT_NODE_V1 = [45,58,51,55,42,5f,53,4d,54,5f,4e,4f,44,45,5f,5f]
EXQUB_SMT_LEAF_V1 = [45,58,51,55,42,5f,53,4d,54,5f,4c,45,41,46,5f,5f]
EXQUB_DEV_BIND_V1 = [45,58,51,55,42,5f,44,45,56,5f,42,49,4e,44,5f,5f]
EXQUB_DEV_KEY_V1 = [45,58,51,55,42,5f,44,45,56,5f,4b,45,59,5f,56,31]
EXQUB_PROX_PROOF_V1 = [45,58,51,55,42,5f,50,52,4f,58,5f,50,52,4f,4f,46]
EXQUB_PRES_HASH_V1 = [45,58,51,55,42,5f,50,52,45,53,5f,48,41,53,48,5f]
EXQUB_HOLDER_V1 = [45,58,51,55,42,5f,48,4f,4c,44,45,52,5f,56,31,5f]
EXQUB_REV_SNAP_V1 = [45,58,51,55,42,5f,52,45,56,5f,53,4e,41,50,5f,5f]
EXQUB_REPLAY_KEY_V1 = [45,58,51,55,42,5f,52,45,50,4c,41,59,5f,4b,45,59]
EXQUB_DELEG_V1 = [45,58,51,55,42,5f,44,45,4c,45,47,5f,56,31,5f,5f]
EXQUB_SCOPE_V1 = [45,58,51,55,42,5f,53,43,4f,50,45,5f,56,31,5f,5f]
EXQUB_ACTION_V1 = [45,58,51,55,42,5f,41,43,54,49,4f,4e,5f,56,31,5f]
EXQUB_SUBDEL_V1 = [45,58,51,55,42,5f,53,55,42,44,45,4c,5f,56,31,5f]
EXQUB_CHAIN_V1 = [45,58,51,55,42,5f,43,48,41,49,4e,5f,56,31,5f,5f]
16.2 Attribute Tree
Input: {"age": "25", "country": "US", "name": "Alice Smith"} (pre-sorted)
Salts: salt_age = [0x02; 32], salt_country = [0x03; 32], salt_name = [0x01; 32]
Tree: 3 attributes + 1 padding leaf = tree_size 4, depth 2
Expected age leaf hash:
[38,f3,da,2d,24,d9,c5,bb,48,1d,28,a1,18,e0,e8,cb,2f,08,87,ad,8a,73,3f,8e,75,e1,2e,83,3e,70,39,1d]
Expected country leaf hash:
[10,2b,d9,3b,50,67,03,1d,92,f2,6f,1b,2d,99,b8,32,ad,8d,89,29,25,2a,ca,4a,c9,45,45,b9,0f,a3,9c,da]
Expected name leaf hash:
[12,9c,45,77,a7,61,ea,48,9d,67,32,58,8d,49,b3,d8,a2,1c,ed,fe,9c,7f,ff,f9,e7,a2,12,c0,1c,98,c2,c2]
Expected padding leaf hash:
[b4,4d,07,51,06,ed,f7,cb,a8,8b,6f,19,da,fc,a9,61,f6,87,0c,d3,01,33,2b,2b,3c,4e,e2,39,ea,c5,a4,42]
Expected tree root:
[cf,00,07,42,22,87,6c,35,52,1e,5f,04,00,d8,d9,f3,4b,bf,6f,cb,b8,89,b9,f0,9b,c9,a1,d5,52,1f,3f,05]
16.3 Credential Signature Input
Input: CredentialV1 with version=0x01, credential_type=0x01, credential_id=[0x11;32], issuer_id=[0x55;32], holder_id=[0x99;32], issued_at=1234567890, expires_at=1266103890, attr_count=3, attr_root=tree_root_from_§16.2
Note: Input notation uses [0xNN;32] meaning 32 bytes all set to 0xNN, matching
the notation used in §16.6–§16.12.
Expected sig_input hash (SHA3-256 of 166-byte preimage):
[71,f5,64,e4,09,84,93,32,e6,57,27,6b,b5,7e,21,82,8f,a3,31,d8,65,9a,db,49,48,10,b8,75,ba,38,9e,7a]
Preimage structure (166 bytes):
- Bytes [0..16): EXQUB_SIG_V1____ domain separator
- Byte [16]: version = 0x01
- Byte [17]: credential_type = 0x01
- Bytes [18..50): credential_id = [0x11;32]
- Bytes [50..82): issuer_id = [0x55;32]
- Bytes [82..114): holder_id = [0x99;32]
- Bytes [114..122): issued_at (u64 BE) = 1234567890
- Bytes [122..130): expires_at (u64 BE) = 1266103890
- Bytes [130..134): attr_count (u32 BE) = 3
- Bytes [134..166): attrroot = tree_root_from§16.2
16.4 SMT Leaf
Input: credential_id=[0x11,0x22,0x33,0x44,...], status=STATUS_VALID (0x00)
Expected leaf position (SHA3-256 of credential_id):
[df,ec,3a,48,ea,8c,fd,b1,80,50,30,5a,e4,b7,15,fa,6c,f1,e6,93,0c,2f,22,14,5d,bb,2a,b7,8b,8a,82,d8]
Expected leaf hash:
[37,d9,c2,9a,47,1f,81,0f,0d,d7,56,f1,02,50,32,94,25,d3,6e,56,4e,c0,e5,01,51,4c,87,8c,a0,ca,00,fd]
16.5 Test Requirements
Compliance: All implementations MUST produce bit-identical outputs for the test vectors in this section.
16.6 Delegation Credential Signature Input
Input: DelegationCredentialV1 with version=0x01, credential_type=0x02, credential_id=[0x11;32], issuer_id=[0x55;32], holder_id=[0x99;32], issued_at=1234567890, expires_at=1266103890, attr_count=2, attr_root=[0xAA;32], delegator_credential_id=[0x00;32] (root), delegation_depth=0x00, max_delegation_depth=0x05, scope_hash=[0xBB;32]
Expected deleg_sig_input hash (SHA3-256 of 232-byte preimage):
[e3,8f,d8,fc,6a,90,36,f7,61,5f,76,21,60,96,72,1d,3b,df,87,29,dc,74,4f,39,ab,f4,70,ba,57,56,3b,7f]
Preimage structure (232 bytes):
- Bytes [0..16): EXQUB_DELEG_V1__ domain separator
- Byte [16]: version = 0x01
- Byte [17]: credential_type = 0x02
- Bytes [18..50): credential_id
- Bytes [50..82): issuer_id
- Bytes [82..114): holder_id
- Bytes [114..122): issued_at (u64 BE)
- Bytes [122..130): expires_at (u64 BE)
- Bytes [130..134): attr_count (u32 BE)
- Bytes [134..166): attr_root
- Bytes [166..198): delegator_credential_id
- Byte [198]: delegation_depth
- Byte [199]: max_delegation_depth
- Bytes [200..232): scope_hash
16.7 Scope Hash
Input: ScopeConstraints with actions=["approve"], resource_patterns=["invoices/*"] (all optional fields absent)
Canonical CBOR (48 bytes):
a267616374696f6e738167617070726f7665717265736f757263655f7061747465726e73816a696e766f696365732f2a
Expected scope_hash (SHA3-256 of EXQUB_SCOPE_V1 || canonical_cbor):
[7a,7a,99,62,85,94,72,6a,0b,78,1a,8e,80,c4,14,57,67,15,f0,de,1b,26,cb,2e,99,db,da,82,5b,de,60,44]
16.8 Action Request Hash
Input: action="approve", resource="invoices/INV-2026-001", value=5000, timestamp=1234567890, request_nonce=[0x77;32]
Expected action_request_hash (SHA3-256 of variable-length preimage):
[3d,78,87,17,b5,58,5c,e8,bd,3e,21,fc,a2,8e,c8,47,e3,4e,64,46,5d,92,2a,f3,ec,0c,7c,94,78,f5,cc,a4]
16.9 Sub-Delegation Signature Input
Input: parent_credential_id=[0x11;32], child_credential_id=[0x22;32], child_holder_id=[0x33;32], child_scope_hash=[0x44;32], child_issued_at=1234567890, child_expires_at=1266103890, child_delegation_depth=0x01
Expected subdel_sig_input hash (SHA3-256 of 161-byte preimage):
[cd,3e,fd,76,bd,1d,15,5c,69,59,ac,ad,72,21,1f,7e,0b,59,ca,4e,5a,81,3a,16,01,0b,57,d0,76,18,68,07]
Preimage structure (161 bytes):
- Bytes [0..16): EXQUB_SUBDEL_V1_ domain separator
- Bytes [16..48): parent_credential_id
- Bytes [48..80): child_credential_id
- Bytes [80..112): child_holder_id
- Bytes [112..144): child_scope_hash
- Bytes [144..152): child_issued_at (u64 BE)
- Bytes [152..160): child_expires_at (u64 BE)
- Byte [160]: child_delegation_depth
16.10 Chain ID Derivation
Input: issuer_id=[0x55;32], chain_name="audit-2026" (10 bytes, u16 BE length prefix = 0x000A)
Expected chain_id hash (SHA3-256 of EXQUB_CHAIN_V1 || issuer_id || len_u16_BE || chain_name):
[99,af,f8,98,59,4c,b6,f3,26,49,b4,cb,bc,05,73,00,07,df,78,87,19,55,23,37,65,10,0b,6b,d7,77,b2,f1]
As hex attribute value:
99aff898594cb6f32649b4cbbc05730007df78871955233765100b6bd777b2f1
16.11 Chain Previous Hash
Input: previous_credential_cbor_bytes = [0xCC;64] (mock 64-byte canonical CBOR)
Expected chain_prev hash (SHA3-256 of raw CBOR bytes, no domain separator):
[7f,df,23,68,a2,70,f1,39,aa,f7,89,d7,e6,e2,74,af,32,8f,6c,6e,aa,f6,23,c4,3b,6b,7d,30,17,fa,9e,87]
16.12 Content Hash
Input: raw_content_bytes = Hello, World! (13 bytes, UTF-8)
Expected SHA3-256:
[1a,f1,7a,66,4e,3f,a8,e4,19,b8,ba,05,c2,a1,73,16,9d,f7,61,62,a5,a2,86,e0,c4,05,b4,60,d4,78,f7,ef]
Expected content_hash attribute value (73 chars):
sha3-256:1af17a664e3fa8e419b8ba05c2a173169df76162a5a286e0c405b460d478f7ef
17. Implementation Constraints
- Core engine MUST be no_std compatible with no heap allocation
- Application layer MAY use heap allocation
- All functions MUST return Result types — no panics, no unwrap
- All cryptographic comparisons MUST use constant-time operations
- No floating-point arithmetic anywhere in the protocol
- Integer overflow MUST be checked (overflow-checks = true in release builds)
- GPS coordinates: fixed-point i32
18. Security Considerations
Quantum Resistance: ML-DSA-65 provides NIST Security Level 3 (equivalent to AES-192). Resistant to known quantum attacks via Shor's and Grover's algorithms.
Key Management: Private keys MUST be generated in secure hardware when available. Keys MUST be stored encrypted at rest. Key material MUST be zeroized after use. No key escrow or recovery mechanisms.
Audit Requirements: All verification failures MUST be logged with timestamp, error code, and presentation hash. Logs MUST be tamper-evident. Minimum retention: 90 days. Maximum PII in logs: presentation hash only.
Incident Response: On key compromise: immediate revocation via SMT update. On issuer compromise: revoke all credentials, regenerate issuer key. On protocol vulnerability: freeze issuance, await v2.0.
Delegation Blast Radius: A compromised sub-agent is limited to actions
within its signed scope. Compromise does not grant access beyond the leaf
delegation's scope_constraints. Verifiers enforce scope server-side — a
compromised agent cannot self-elevate.
Delegation Scope Enforcement: Scope is enforced by verifiers, not by
issuers. The scope_hash in the signed delegation credential is the
authoritative record of permitted scope. Presented scope_constraints that do
not hash to scope_hash are rejected.
Sub-Delegation Cascade Revocation: Revoking a parent delegation invalidates all sub-delegations in the chain. Verifiers MUST check revocation status for each link in the chain, not just the leaf.
Sub-Delegation Device Key Linkability: Sub-delegation is signed by the
delegator's device key (§7.18). This cryptographically links the delegator's
device key to the sub-credential — an intentional tradeoff. Linkability is
limited to the issuer and the delegated agent, not exposed to arbitrary
verifiers. The delegator_credential_id already links the chain structurally;
device key signing adds non-repudiation for the delegation act itself.
Randomised vs deterministic signing does not matter here because linkability is
inherent in the delegation chain structure.
19. Protocol Invariants
Determinism: All operations produce identical outputs for identical inputs. No randomness except CSPRNG for salts/nonces.
Isolation: No network operations in core protocol. No filesystem operations during verification. No external dependencies in cryptographic operations.
Timing: Application layer MUST provide trustworthy current_time parameter to core engine. Time comparisons use absolute value of difference.
Memory: Stack allocation bounded at 4KB for no_std. No dynamic allocation in verification path. All buffers statically sized.
Concurrency: No shared mutable state. No threading in core engine. Verification is single-threaded only.
20. Credential Issuance Constraints
Rate Limiting: Implementation-specific maximum issuance rate. No protocol-level rate limit.
Batch Issuance: NOT supported in v1.0. Each credential issued independently.
Modification: No in-place attribute modification. Changes require full credential reissuance with new credential_id.
Holder Binding: MUST occur at issuance time. Cannot rebind credential to different holder.
Attribute Sources: Attributes provided at issuance only. No dynamic attribute resolution.
Validity Period: Maximum 365 days (MAX_CREDENTIAL_LIFETIME). Minimum determined by application.
Issuer Key Rotation: No automated rotation protocol. Manual process: (1) Generate new key pair, (2) Update issuer ID, (3) Begin issuing with new key, (4) Maintain old key for verification only, (5) Revoke old credentials as they expire.
Delegation Credentials: Scope MUST be provided at issuance — zero-scope
delegations are prohibited. delegation_depth MUST be ≤ max_delegation_depth
MUST be ≤ MAX_DELEGATION_DEPTH (5). scope_hash MUST be computed from the
canonical CBOR of the provided ScopeConstraints before signing. Lifetime:
minimum MIN_DELEGATION_LIFETIME (60s), maximum MAX_DELEGATION_LIFETIME (86,400s)
for ephemeral sub-delegations; root delegations MAY use standard
MAX_CREDENTIAL_LIFETIME (365d).
21. Deployment Profiles
21.1 High Security Profile
- Clock skew: 60 seconds (HIGH_SECURITY_CLOCK_SKEW)
- Proximity: required for all presentations
- Failure mode: fail closed on any error
- SMT freshness: 24 hours maximum
- Replay cache: 24 hours retention
- Audit: comprehensive logging with remote backup
21.2 Standard Profile
- Clock skew: 300 seconds (DEFAULT_CLOCK_SKEW)
- Proximity: optional, policy-driven
- Failure mode: fail open with audit flag on recoverable errors
- SMT freshness: 7 days maximum (MAX_SMT_ROOT_AGE)
- Replay cache: 15 minutes retention (MIN_REPLAY_CACHE_TTL)
- Audit: local logging only
21.3 Offline Profile
- Clock skew: 600 seconds (MAX_CLOCK_SKEW)
- Proximity: not supported
- Failure mode: accept with warning on stale SMT roots
- SMT freshness: 30 days acceptable with warning
- Replay cache: best-effort in-memory only
- Audit: optional
22. Version Commitment
This specification defines Exqub V1.0. V1.0 includes: standard credentials (credential_type 0x01), delegation credentials (credential_type 0x02), chain linking (via attribute conventions on any credential type), and content attestation credentials (credential_type 0x04). Credential type 0x03 is reserved for future use.
Versioning Rules: No version negotiation — exact match required (0x01). Unknown versions MUST be rejected. Version field checked first in all structures.
Stability: The normative sections (§1–§16) are stable for V1.0. Non-normative guidance sections (§23–§27) may be updated without a version increment.
Future Considerations: V2.0 MAY add: threshold credentials (k-of-n), conditional disclosure (predicate proofs), blind signatures, additional signature algorithms, formal verification proofs. V2.0 MAY introduce breaking changes. Migration strategy will be defined with the V2.0 specification.
23. Agent Authorization Architecture (Non-Normative)
23.1 Problem Statement
Traditional API key authorization has three failure modes for AI agents: (1) blast radius — a compromised agent key has full API access; (2) auditability — no cryptographic record of what an agent was authorized to do; (3) delegation — no mechanism for agents to spawn sub-agents with narrowed authority. Delegation credentials address all three.
23.2 Threat Model
- Prompt injection: An attacker embeds instructions in external content to hijack an agent's actions. Scope constraints limit the damage — the agent cannot act outside its signed scope even if manipulated.
- Hallucination: An agent requests a resource it is not authorized for. The verifier rejects the action per scope.
- Misconfiguration: A developer grants an agent overly broad authority. The
max_delegation_depthand scope constraints provide explicit audit trails. - Compromised runtime: An agent runtime is fully compromised. Scope constraints limit the blast radius to the signed scope.
23.3 Actors
| Actor | Role |
|---|---|
| Delegator | Holds a standard credential (0x01) and delegates authority to an agent |
| Agent | Holds a delegation credential (0x02) issued by the delegator |
| Service | Verifies delegated action presentations against the issuer's revocation tree |
| Sub-Agent | Holds a delegation credential issued by an agent (sub-delegation) |
23.4 Use Cases
Procurement agent: An employee delegates invoice approval authority up to
$500 to an AI purchasing agent. The agent's scope:
actions=["approve_invoice"], resource_patterns=["invoices/*"],
max_value=50000 (cents).
Clinical agent: A physician delegates read access to patient records to a
diagnostic AI. The scope includes required_attestations=["hipaa_trained"],
enforcing that the agent must present attestation of HIPAA compliance training.
Trading agent: A financial institution delegates order execution to an
algorithmic agent with max_daily_value and time_window constraints.
23.5 Integration Patterns
Framework SDKs (§25) wrap the holder and verifier libraries. The general pattern:
- Delegator calls issuer API to create delegation credential
- Agent receives credential via claim URL (same flow as §7 holder ID)
- Agent constructs
DelegatedActionPresentationusing exqub-holder - Service verifies via exqub-verifier
verify_delegated_action()
24. Agent Attestation Attributes (Non-Normative)
Agent attestation attributes are carried as standard attributes on delegation credentials (credential_type 0x02). They are NOT a separate credential type. These attribute keys are reserved:
| Attribute Key | Type | Required | Description |
|---|---|---|---|
agent_model_id |
string | OPTIONAL | AI model identifier (e.g., "claude-sonnet-4-6") |
agent_model_version |
string | OPTIONAL | Model version string |
agent_model_hash |
hex, 64 chars | OPTIONAL | SHA3-256 of model weights |
agent_runtime |
string | OPTIONAL | Runtime identifier (e.g., "langchain-0.3") |
agent_runtime_hash |
hex, 64 chars | OPTIONAL | SHA3-256 of runtime binary |
safety_alignment_version |
string | OPTIONAL | Safety alignment specification version |
safety_attestation_hash |
hex, 64 chars | OPTIONAL | SHA3-256 of safety evaluation results |
training_data_hash |
hex, 64 chars | OPTIONAL | SHA3-256 of training data manifest |
guardrail_policy_hash |
hex, 64 chars | OPTIONAL | SHA3-256 of guardrail policy document |
Selective disclosure: An agent may present agent_model_id without
revealing training_data_hash — standard attribute Merkle tree selective
disclosure applies.
Attestation freshness: These attributes represent point-in-time facts (what was true at issuance). Model updates require credential reissuance.
Required attestations: A scope constraint's required_attestations field
lists attribute keys the agent MUST disclose in the action presentation.
Example: required_attestations=["safety_alignment_version"] forces disclosure
of the safety spec version before any action is authorized.
25. Framework Integration Guidance (Non-Normative)
25.1 General SDK Pattern
All framework integrations follow this interface:
// Credential-aware tool execution
fn execute_with_credential(
presentation: DelegatedActionPresentation,
action: ActionRequest,
) -> Result<ToolOutput, VerificationError>
The SDK wraps this in framework-specific tool/function call patterns.
25.2 LangChain
Use CredentialAwareTool wrapping standard LangChain tools. The tool verifies
the DelegatedActionPresentation before executing. The credential is threaded
through the RunnableConfig context.
25.3 AutoGen
In multi-agent conversations, each agent holds a delegation credential issued by
the orchestrator. Sub-agents receive sub-delegations with narrowed scope. The
CredentialedAgent base class handles presentation construction automatically.
25.4 CrewAI
Crew roles map to delegation scopes. A "Researcher" role receives a delegation
with actions=["search","read"]; a "Writer" role receives
actions=["write","publish"]. Role credentials are issued at crew
initialization.
25.5 LlamaIndex
Query engines receive delegation credentials scoped to specific index resources.
credential_gated_query() verifies the agent's scope before executing a query
against the index.
25.6 Minimum Supported Versions
| Framework | Minimum Version |
|---|---|
| LangChain | 0.3.x |
| AutoGen | 0.4.x |
| CrewAI | 0.8.x |
| LlamaIndex | 0.11.x |
26. Agent and Audit Deployment Profiles (Non-Normative)
26.1 Agent Deployment Profile
For AI agent authorization scenarios with short-lived credentials and strict enforcement:
- Clock skew: 60 seconds (HIGH_SECURITY_CLOCK_SKEW) — agent credentials are short-lived
- Delegation depth: enforce MAX_DELEGATION_DEPTH (5); prefer ≤ 3 for most deployments
- Scope: require explicit scope constraints; reject zero-scope delegations
- Revocation: check revocation status for every link in the chain on every request
- Credential lifetime: prefer
MAX_DELEGATION_LIFETIME(86,400s) or shorter - Replay cache: required — agents make repeated requests
- Failure mode: fail closed on any error
- Audit: log all delegated action presentations with full chain metadata
26.2 Audit Deployment Profile
For regulatory audit and decision log verification with lenient historical settings:
- Clock skew: 600 seconds (MAX_CLOCK_SKEW) — historical credentials may have loose timestamps
- Revocation mode: audit mode (revoked entries flagged, not rejected) — allows full chain traversal
- SMT freshness: accept stale roots with STATUS_STALE_ROOT warning for historical verification
- Credential lifetime: accept expired credentials for audit purposes (with
explicit
allow_expiredflag) - Replay cache: not applicable for historical verification
- Failure mode: fail open with comprehensive audit flag for recoverable errors
- Chain length: enforce MAX_CHAIN_LENGTH but accept up to operational limit
- Audit: export full chain with selective disclosure for regulatory review
27. Future Considerations (Non-Normative)
The following capabilities are out of scope for V1.0 and are design sketches only. No stability guarantee.
27.1 Threshold Credentials (k-of-n)
A credential requiring k-of-n issuers to sign. Enables multi-party approval
workflows. Would require a new credential_type and threshold signature
construction. Candidate: FROST (Flexible Round-Optimized Schnorr Threshold
Signatures).
27.2 Conditional Disclosure
Zero-knowledge proofs allowing "age > 18" without revealing age. Would require a ZK circuit per predicate and a new proof verification step. Candidate: Groth16 or PLONK on a pairing-friendly curve.
27.3 Vector-Compatible Schemas
Structured attribute schemas enabling semantic search over credentials. A holder could prove membership in a set of credentials matching a vector similarity threshold. Requires standardised schema registry.
27.4 Formal Verification (Lean 4)
Machine-checked proofs of the verification algorithm's correctness properties: (1) soundness (a forged credential is rejected), (2) completeness (a valid credential is accepted), (3) scope monotonicity (child scope cannot exceed parent scope). Candidate toolchain: Lean 4 with Mathlib.