# 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_depth` MUST be ≤ `max_delegation_depth` MUST be ≤
  MAX_DELEGATION_DEPTH (5)
- `delegator_credential_id` MUST be all zeros iff `delegation_depth == 0`
- `delegator_credential_id` MUST NOT be all zeros iff `delegation_depth > 0`
- `issued_at` MUST be < `expires_at`
- `attr_count` MAY 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 as `max_value`
- `max_actions_per_hour`: Same monotonic narrowing rule as `max_value`
- `time_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 INVALID
- `required_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

1. Validate: attr_count == salts.length, attr_count ≤ MAX_ATTRIBUTES, no
   duplicate keys
1. Sort attributes lexicographically by key (UTF-8 bytewise comparison, stable
   sort)
1. Compute leaf hashes using §7.5, with each attribute's original salt
1. Pad to next_power_of_2(attr_count) using padding leaves (§7.10)
1. Build bottom-up: pair leaves, compute parent via §7.6
1. 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:

1. Reject if leaf_index ≥ attr_count (padding leaf disclosure)
1. Compute tree_size = next_power_of_2(attr_count)
1. Compute tree_depth = trailing_zeros(tree_size) — the ACTUAL depth, not
   MAX_TREE_DEPTH
1. Reject if merkle_proof.length ≠ tree_depth
1. Compute leaf_hash via §7.5
1. 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.
1. 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:

1. Validate: siblings.length ≤ 256, strictly ascending depths, no duplicates
1. Set current_hash = leaf_hash, sibling_cursor = last index (deepest sibling)
1. 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
1. Assert all siblings consumed (cursor == -1). Reject if not.
1. 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<T> field is None, the CBOR map key MUST be omitted entirely. MUST
NOT encode CBOR null.

**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
  verification
- `Audit` — 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): attr*root = 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_depth` and 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:

1. Delegator calls issuer API to create delegation credential
1. Agent receives credential via claim URL (same flow as §7 holder ID)
1. Agent constructs `DelegatedActionPresentation` using exqub-holder
1. 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:

```rust
// 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_expired` flag)
- 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.
