Backpack Protocol v1.0
Profile: PROVARA-1.0_PROFILE_A
Status: Normative reference implementation specification
Date: 2026-02-16
Source of truth: PROTOCOL_PROFILE.txt — immutable after distribution. This document is the human-readable companion.
Normative Precedence
When materials disagree, use this precedence order:
PROTOCOL_PROFILE.txt(frozen normative profile, highest authority)docs/BACKPACK_PROTOCOL_v1.0.md(human-readable protocol companion)docs/OPEN_DECISIONS.md(resolved ambiguities and implementation choices)- Reference implementation code and tests (
SNP_Core/bin,SNP_Core/test)
If a conflict exists between this document and the profile, the profile wins.
Table of Contents
- Purpose
- Concepts and Vocabulary
- Cryptographic Stack
- Canonical JSON
- Event Identity
- Causal Chain
- Event Types
- The Reducer
- Key Management
- Merkle Tree
- Manifest
- Safety Tiers
- Sync Protocol
- Event Permanence
- Directory Structure
- Compliance Requirements
- Reimplementation Guide
- Security Considerations
- Extension Registry Process
- IANA-Style Considerations
1. Purpose
The Backpack Protocol defines a tamper-evident, append-only memory substrate for AI systems, sovereign identity, and long-lived digital institutions. A backpack (vault) is a directory of signed events. Given the same event log, any compliant implementation on any machine must produce byte-identical state.
Design axiom: Truth is not merged. Evidence is merged. Truth is recomputed.
Events are cryptographic evidence — signed, hashed, and causally chained. Beliefs about the world are derived by replaying evidence through a deterministic reducer. No belief is ever directly merged; only the underlying evidence is combined, and conclusions are recomputed fresh each time.
2. Concepts and Vocabulary
| Term | Definition |
|---|---|
| Vault / Backpack | A directory containing a compliant event log, identity files, policies, and manifest |
| Event | An immutable, content-addressed JSON record appended to the event log |
| Actor | The identity that authored an event (identified by actor_key_id) |
| Belief | A derived conclusion about the world — never stored directly, always recomputed from events |
| Namespace | One of four epistemic buckets: canonical, local, contested, archived |
| Reducer | The pure function f(events) → state that deterministically produces beliefs |
| State hash | A SHA-256 digest of reducer output — byte-identical across all compliant implementations |
| Merkle root | A SHA-256 Merkle tree root over all backpack files — seals vault integrity |
| Manifest | A JSON file enumerating every backpack file with its SHA-256 hash and size |
| Fencing token | A signed, content-addressed token that prevents stale writes during sync |
| Key ID | bp1_ + first 16 hex chars of SHA-256(raw_ed25519_public_key_bytes) |
3. Cryptographic Stack
All Profile A implementations MUST use exactly this stack. No substitutions are permitted.
| Function | Algorithm | Specification |
|---|---|---|
| Hashing | SHA-256 | FIPS 180-4 |
| Signing | Ed25519 | RFC 8032 |
| Canonical JSON | JCS subset | RFC 8785 |
SHA-256 Rules
- Output: 64 lowercase hexadecimal characters
- Input: UTF-8 encoded bytes
- Used for: event IDs, file integrity, Merkle nodes, state hash, key ID derivation
Ed25519 Rules
- Key size: 256-bit (32-byte public, 64-byte private with seed)
- Signatures: 64 bytes, Base64-encoded (standard alphabet with padding)
- Signing payload for events:
SHA-256(canonical_bytes(event_without_sig_field)) - The
sigfield MUST be excluded before signing - The
event_idfield MUST be included in the signing payload
Key ID Derivation
key_id = "bp1_" + SHA-256(raw_public_key_bytes)[:16 hex chars]
Example: bp1_27a6549d43046062
4. Canonical JSON
All hashing and signing MUST use RFC 8785 canonical JSON. Two compliant implementations serializing the same logical object MUST produce byte-identical output.
Rules (all MUST apply)
- Object keys MUST be sorted lexicographically by Unicode code point
- No whitespace between tokens
- No trailing commas
- Numbers MUST NOT have leading zeros, positive signs, or trailing decimal zeros
- Strings MUST use minimal escape sequences
- Encoding MUST be UTF-8 without BOM
- Null values MUST be preserved as
"null"— MUST NOT be omitted
Example
Input object (logical):
{"z": 3, "a": 1, "m": {"y": 2, "b": null}}
Canonical output:
{"a":1,"m":{"b":null,"y":2},"z":3}
Reference Implementation
import json
def canonical_bytes(obj):
return json.dumps(
obj,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
Cross-language note: For fractional values (floats), prefer integer encoding or string-wrapped decimals to guarantee byte-exact interoperability across languages.
5. Event Identity
Events are content-addressed. Their ID is derived from their content.
Derivation Rule (MUST)
- Remove
event_idandsigfields from the event object - Compute canonical JSON bytes of the remaining object
event_id = "evt_" + SHA-256(canonical_bytes)[:24 hex chars]
This rule is deterministic: identical event content always produces the same event_id.
Example
def derive_event_id(event: dict) -> str:
hashable = {k: v for k, v in event.items() if k not in ("event_id", "sig")}
digest = hashlib.sha256(canonical_bytes(hashable)).hexdigest()
return f"evt_{digest[:24]}"
6. Causal Chain
Each actor maintains a per-actor linked list of events via prev_event_hash.
Rules (all MUST apply)
- First event by an actor:
prev_event_hashMUST benull - Subsequent events:
prev_event_hashMUST equal theevent_idof that actor's immediately preceding event - Cross-actor:
prev_event_hashMUST NOT reference another actor's events
Integrity Rule
For any event E by actor A, if E.prev_event_hash is not null, there MUST exist an event P where:
P.event_id == E.prev_event_hashP.actor == A
Any gap or forgery breaks the chain and fails compliance.
Fork Detection
A fork occurs when two events by the same actor share the same prev_event_hash. This indicates concurrent offline operation. Forks are preserved in the merged log and surface in the contested/ namespace after reducer replay.
7. Event Types
Machine-readable schema: docs/schemas/provara_event_schema_v1.json
Core Types (no prefix required)
All core event types are reserved. Custom extensions MUST use reverse-domain prefixes (com.example.my_type).
GENESIS
The vault birth certificate. Every backpack has exactly one GENESIS event, written by bootstrap_v0.py.
Required fields:
{
"event_id": "evt_...",
"type": "GENESIS",
"namespace": "canonical",
"actor": "<actor_id>",
"actor_key_id": "<bp1_...>",
"ts_logical": 1,
"prev_event_hash": null,
"timestamp_utc": "<ISO 8601>",
"payload": {
"uid": "<vault_uuid>",
"birth_timestamp": "<ISO 8601>",
"root_key_id": "<bp1_...>",
"protocol_version": "1.0",
"profile": "PROVARA-1.0_PROFILE_A"
},
"sig": "<base64 ed25519>"
}
OBSERVATION
A sensor, measurement, or belief claim with associated confidence.
{
"type": "OBSERVATION",
"namespace": "local",
"payload": {
"subject": "<entity>",
"predicate": "<property>",
"value": "<any JSON value>",
"confidence": 0.9,
"timestamp": "<ISO 8601>"
}
}
The belief key is subject:predicate. The reducer places observations in local/, moving them to contested/ if conflicting high-confidence evidence accumulates.
ASSERTION
Functionally equivalent to OBSERVATION but defaults to lower confidence (0.35). Used for inferred or deduced beliefs rather than direct sensor readings.
ATTESTATION
Promotes a belief to the canonical/ namespace. Requires institutional authority.
{
"type": "ATTESTATION",
"namespace": "canonical",
"payload": {
"subject": "<entity>",
"predicate": "<property>",
"value": "<attested value>",
"target_event_id": "<event_id of source evidence>",
"actor_key_id": "<bp1_...>"
}
}
When a canonical belief already exists for a key, it is moved to archived/ with superseded_by set to the new attestation event ID.
RETRACTION
Removes a belief from active namespaces. Canonical entries are archived as retracted.
{
"type": "RETRACTION",
"payload": {
"subject": "<entity>",
"predicate": "<property>"
}
}
Effect:
- Removes from
local/andcontested/ - If in
canonical/: moves toarchived/withretracted: trueandsuperseded_by: <retraction_event_id>
KEY_REVOCATION
Part of the two-event key rotation protocol. Marks a key as revoked.
{
"type": "KEY_REVOCATION",
"payload": {
"revoked_key_id": "<bp1_...>",
"trust_boundary_event_id": "<last trusted event_id>",
"reason": "compromised",
"revoked_by": "<signing_key_id>"
}
}
MUST be signed by a surviving trusted authority. The revoked key MUST NOT sign its own revocation.
KEY_PROMOTION
Second half of key rotation. Introduces the replacement key.
{
"type": "KEY_PROMOTION",
"payload": {
"new_key_id": "<bp1_...>",
"new_public_key_b64": "<base64>",
"algorithm": "Ed25519",
"roles": ["root"],
"promoted_by": "<signing_key_id>",
"replaces_key_id": "<revoked_key_id>"
}
}
The new key MUST NOT sign its own promotion. A surviving authority signs both rotation events.
REDUCER_EPOCH
Records a reducer version transition for audit purposes.
{
"type": "REDUCER_EPOCH",
"payload": {
"epoch_id": "<epoch_identifier>",
"reducer_hash": "sha256:<hash_of_reducer_code>",
"effective_from_event_id": "<event_id>",
"ontology_versions": {"perception": "v1"}
}
}
8. The Reducer
The reducer is a pure function: f(events) → state. Given the same event sequence, any compliant implementation MUST produce a byte-identical state_hash.
Four-Namespace State Model
| Namespace | Meaning | When Populated |
|---|---|---|
canonical/ |
Institutionally attested truth | Via ATTESTATION event |
local/ |
Node-local observations | Via OBSERVATION (no conflict) |
contested/ |
Conflicting high-confidence evidence | When two conflicting observations both exceed the conflict threshold |
archived/ |
Superseded canonical beliefs | When a canonical entry is overwritten by new ATTESTATION or RETRACTION |
Belief Keys
Beliefs are indexed by subject:predicate. Example: door_01:status.
Conflict Detection
The default conflict confidence threshold is 0.50. An incoming observation triggers conflict if:
- Conflicts with canonical: existing canonical value differs AND incoming confidence ≥ threshold
- Conflicts with local: existing local value differs AND
max(existing_confidence, incoming_confidence)≥ threshold
When conflict is detected, the key moves from local/ to contested/ with full evidence grouping.
Agreeing Evidence
If a new observation agrees with the existing local value:
- If incoming confidence ≤ existing: keep existing, record evidence (no state change)
- If incoming confidence > existing: update local to show higher confidence source
State Hash
state_hash = SHA-256(canonical_bytes(state_without_metadata.state_hash))
The hash covers all four namespaces plus non-hash metadata fields. It excludes metadata.state_hash itself (non-self-referential). An empty reducer (zero events) produces a valid, deterministic state hash.
Reducer Invariants
- Same event sequence → byte-identical
state_hash - Empty log → valid state hash, all namespaces empty,
event_count = 0 - Unknown event types: counted, ignored for state, logged to
_ignored_types - Malformed events (non-dict, missing fields): skipped or handled gracefully — never crash
9. Key Management
Key Registry (identity/keys.json)
{
"keys": [
{
"key_id": "bp1_27a6549d43046062",
"public_key_b64": "<base64>",
"algorithm": "Ed25519",
"roles": ["root", "attestation"],
"status": "active",
"created_at_utc": "2026-02-13T00:00:00Z"
}
],
"revocations": []
}
Key Rotation Protocol
Key rotation is a two-event atomic operation:
- KEY_REVOCATION — signed by a surviving trusted authority (MUST NOT be the key being revoked)
- KEY_PROMOTION — signed by the same surviving authority (MUST NOT be the new key)
Security invariant: A compromised key cannot authorize its own replacement. If an attacker could self-sign a KEY_PROMOTION, they could escalate from key compromise to permanent identity takeover.
Trust hierarchy (for signing rotation):
- Non-compromised root key → signs rotation
- Root compromised, quorum keys survive → quorum signs
- All keys compromised → catastrophic identity death (new genesis required)
Revoked Keys
After revocation, a key's status changes to "revoked". Revoked keys appear in the revocations list and MUST NOT be used to sign new events.
10. Merkle Tree
The Merkle tree seals the integrity of all backpack files.
Construction
- Leaves: For each file entry in the manifest:
leaf_hash = SHA-256(canonical_bytes({"path": "...", "sha256": "...", "size": N})) - Ordering: Leaves MUST be sorted lexicographically by file path
- Padding: If leaf count is odd, the last leaf is duplicated
- Nodes:
node_hash = SHA-256(left_child_bytes || right_child_bytes)— raw byte concatenation (32 + 32 bytes), NOT hex string concatenation - Root: A single 64-character lowercase hex string stored in
merkle_root.txt
Empty Tree
An empty tree (no files) produces: SHA-256(b"").hexdigest()
Reference Implementation
def merkle_root_hex(leaves: list[bytes]) -> str:
if not leaves:
return hashlib.sha256(b"").hexdigest()
level = [hashlib.sha256(leaf).digest() for leaf in leaves]
while len(level) > 1:
next_level = []
for i in range(0, len(level), 2):
left = level[i]
right = level[i + 1] if i + 1 < len(level) else level[i]
next_level.append(hashlib.sha256(left + right).digest())
level = next_level
return level[0].hex()
11. Manifest
manifest.json inventories every file in the backpack with its SHA-256 hash, file size, and relative path.
Format
{
"backpack_spec_version": "1.0",
"manifest_format": "manifest.v0",
"generated_at_utc": "2026-02-13T00:00:00Z",
"merkle_root": "<64 hex chars>",
"files": [
{
"path": "events/events.ndjson",
"sha256": "<64 hex chars>",
"size": 1234
}
]
}
Excluded Files
manifest.json, manifest.sig, and merkle_root.txt are excluded from the file list (they cannot hash themselves).
Manifest Signature
manifest.sig is a detached Ed25519 signature over SHA-256(merkle_root_bytes + canonical_bytes(manifest_header)). It binds the manifest to a specific key and timestamp.
12. Safety Tiers
The safety tier system governs what actions a vault can authorize offline.
| Tier | Risk | Offline Allowed | Gate |
|---|---|---|---|
| L0 | Data-only, reversible | Yes | Local reducer |
| L1 | Low-kinetic | Yes (logged for review) | Reducer + policy |
| L2 | High-kinetic | Lease window only | Multi-sensor + signed policy |
| L3 | Critical / irreversible | No | Human MFA or remote signature |
Merge Ratchet
Safety constraints only tighten automatically on merge (most_restrictive_wins). Loosening requires a signed POLICY_UPDATE by a key with L3 clearance. This is a one-way ratchet — you can always become more cautious, never less cautious without explicit authority.
13. Sync Protocol
The sync protocol is a union merge with causal chain verification.
13.1 Chain Validation Algorithm
To validate causal and cryptographic integrity, a conformant implementation MUST execute the following deterministic procedure:
- Parse all NDJSON lines as UTF-8 JSON objects.
- Build
event_by_id[event_id]and fail on duplicate IDs. - For each event:
- Recompute content-derived ID from the event with
event_idandsigremoved. - Verify recomputed ID equals
event_id.
- Build
actor_events[actor]and sort each actor list by:
timestamp_utc(primary, ascending)event_id(secondary, ascending)
- For each actor chain:
- First event MUST have
prev_event_hash = null. - Each subsequent event MUST set
prev_event_hashto the immediately preceding event ID in that actor-sorted list. prev_event_hashMUST reference an event by the same actor (cross-actor references are invalid).
- For each signed event:
- Resolve public key by
actor_key_id. - Reject missing, unknown, or revoked key IDs.
- Verify Ed25519 signature over canonical JSON bytes of event-without-
sig.
- If all checks pass, the chain is valid; otherwise invalid with a specific error code (see §16.1).
13.2 Algorithm Pseudocode
function verify_vault(events):
event_by_id = {}
actor_events = {}
for e in events:
require_utf8_json(e) # PROVARA_E007
require_required_fields(e) # PROVARA_E004
if e.event_id in event_by_id:
fail(PROVARA_E010, "DUPLICATE_EVENT_ID")
event_by_id[e.event_id] = e
derived = derive_event_id(remove_fields(e, ["event_id", "sig"]))
if derived != e.event_id:
fail(PROVARA_E001, "HASH_MISMATCH")
actor_events[e.actor].append(e)
for actor in actor_events:
chain = sort(actor_events[actor], by=["timestamp_utc", "event_id"])
for i in range(0, len(chain)):
curr = chain[i]
if i == 0:
if curr.prev_event_hash is not null:
fail(PROVARA_E002, "BROKEN_CAUSAL_CHAIN")
else:
prev = chain[i - 1]
if curr.prev_event_hash != prev.event_id:
fail(PROVARA_E002, "BROKEN_CAUSAL_CHAIN")
# explicit same-actor check for referenced prev
linked = event_by_id.get(curr.prev_event_hash)
if linked is null or linked.actor != actor:
fail(PROVARA_E011, "CROSS_ACTOR_REFERENCE")
if has_field(curr, "sig"):
key = key_registry.get(curr.actor_key_id)
if key is null:
fail(PROVARA_E012, "UNKNOWN_KEY_ID")
if key.status == "revoked":
fail(PROVARA_E006, "REVOKED_KEY_USE")
if !verify_ed25519_signature(curr, key.public_key):
fail(PROVARA_E003, "INVALID_SIGNATURE")
return ok()
13.3 Fencing Tokens
Before writing to a backpack, generate a fencing token:
token_hash = SHA-256(latest_event_id + ":" + timestamp + ":" + nonce)
sig = Ed25519_sign(token_hash, active_key)
Validation:
- Recompute
token_hashfrom the fields - Verify signature against key registry
- Verify
latest_event_idstill exists in the event log
A stale token (referencing an event that was superseded by a merge) fails validation, preventing lost-update scenarios.
Delta Export / Import
Delta bundles enable partial sync (only events after a known hash):
Header: {"type": "provara_delta_v1", "since_hash": "...", "event_count": N, "keys": [...]}
Body: One NDJSON line per event
Unknown event types MUST be preserved in the merged log. They MUST NOT affect core reducer state.
14. Event Permanence
Events MUST be permanent. Implementations MUST NOT delete events.
| Data Type | Retention |
|---|---|
| Events | Permanent |
| Checkpoints | Permanent |
| Perception raw data | 30-day default, oldest-first eviction (MAY vary) |
| CAS artifacts | Permanent if referenced; 90-day if unreferenced (MAY vary) |
| State caches | MAY be evicted (regeneratable from events) |
15. Directory Structure
My_Backpack/
├── identity/
│ ├── genesis.json # Birth certificate
│ └── keys.json # Public key registry
├── events/
│ └── events.ndjson # THE source of truth (append-only NDJSON)
├── policies/
│ ├── safety_policy.json # L0-L3 kinetic risk tiers
│ ├── retention_policy.json # Data permanence rules
│ ├── sync_contract.json # Governance + authority ladder
│ └── ontology/
│ └── perception_ontology_v1.json
├── state/ # Regeneratable cache (may be absent)
│ └── current_state.json
├── artifacts/
│ └── cas/ # Content-addressed storage
├── manifest.json # File inventory with SHA-256 hashes
├── manifest.sig # Ed25519 signature over manifest
└── merkle_root.txt # Integrity anchor (single hex string)
Path Safety
All file paths in the manifest MUST:
- Be relative to the backpack root
- Not contain
..or symlinks pointing outside the root - Be lexicographically sorted in the manifest
16. Compliance Requirements
16.1 Error Taxonomy
Standardized error codes for validation failures. Implementations SHOULD use these codes to ensure interoperability and clear auditing.
| Code | Label | Description |
|---|---|---|
PROVARA_E001 |
HASH_MISMATCH |
Event ID does not match computed content hash |
PROVARA_E002 |
BROKEN_CAUSAL_CHAIN |
prev_event_hash does not link to correct previous event |
PROVARA_E003 |
INVALID_SIGNATURE |
Signature verification failed for the given public key |
PROVARA_E004 |
MISSING_FIELD |
A required top-level or payload field is missing |
PROVARA_E005 |
UNAUTHORIZED_SIGNER |
The signing key does not have authority for the event type |
PROVARA_E006 |
REVOKED_KEY_USE |
Event signed by a key that was revoked at the time of signing |
PROVARA_E007 |
MALFORMED_JSON |
Event or file is not valid UTF-8 or standard JSON |
PROVARA_E008 |
MERKLE_ROOT_MISMATCH |
Recomputed Merkle root does not match merkle_root.txt |
PROVARA_E009 |
UNSAFE_PATH |
Manifest entry path is absolute, traverses .., or escapes root |
PROVARA_E010 |
DUPLICATE_EVENT_ID |
Duplicate event_id found in a log that requires uniqueness |
PROVARA_E011 |
CROSS_ACTOR_REFERENCE |
Event references previous hash from a different actor chain |
PROVARA_E012 |
UNKNOWN_KEY_ID |
actor_key_id cannot be resolved in active key registry |
Canonical source for these codes: docs/ERROR_CODES.md
16.2 Minimum Test Coverage
| Category | Tests | Requirement |
|---|---|---|
| Directory structure | 2 | Required folders and files exist |
| Identity schema | 2 | Genesis event and key registry validity |
| Event schema + causal chain | 3 | Event format, uniqueness, causal ordering |
| Manifest integrity + Merkle | 5 | File hashes, Merkle computation, no phantoms, path safety |
| Safety policy structure | 2 | L0-L3 structure and ratchet constraints |
| Sync contract schema | 1 | Governance schema validity |
| Reducer determinism | 1 | Same events → identical state hash |
| Retention permanence | 1 | Events are never deleted |
Run against the reference backpack:
cd SNP_Core/test && PYTHONPATH=../bin python backpack_compliance_v1.py ../examples/reference_backpack -v
17. Reimplementation Guide
To implement Provara v1.0 in another language:
Step 1 — Cryptographic Primitives
Implement:
- SHA-256 (FIPS 180-4)
- Ed25519 sign and verify (RFC 8032)
- RFC 8785 canonical JSON
Step 2 — Validate Against Test Vectors
Validate your implementations against test_vectors/vectors.json. The 7 vectors cover canonical JSON, SHA-256, event ID derivation, key ID derivation, Ed25519 sign/verify, Merkle root, and reducer determinism.
Step 3 — Implement the Reducer
Handle: OBSERVATION, ASSERTION, ATTESTATION, RETRACTION, REDUCER_EPOCH, KEY_REVOCATION, KEY_PROMOTION
For unknown event types: count them, do not modify namespace state, preserve in event log on merge.
Step 4 — Verify Determinism
Run your reducer against the test vector event sequence. Compare your state_hash to the reference value. If they match, your implementation is correct. If they diverge, the canonical JSON or hash computation has a bug.
Step 5 — Pass Compliance Tests
Generate a backpack with your implementation and run the 17 compliance tests. If all pass, your implementation is conformant.
If State Hashes Diverge
Common causes:
- Non-canonical JSON — check key sorting, whitespace, null handling
- Float serialization — JSON float encoding differs across languages; use integers or string-wrapped values for fractional confidence scores
- UTF-8 encoding — ensure no BOM, no alternate encodings
- Missing metadata fields —
state_hashexcludesmetadata.state_hashbut includes all other metadata fields
The Python reference implementation is the canonical source of truth for any ambiguity.
18. Security Considerations
18.1 Threat Model
| Threat | Primary Defense | Residual Risk / Operator Duty |
|---|---|---|
| Payload/data tampering | Event signatures + manifest file hashes + Merkle root verification | Private key compromise can still produce valid malicious events until revocation. |
| History re-ordering | Per-actor causal chain (prev_event_hash) + deterministic sort on replay |
Incorrect timestamp parsing in non-conformant ports can still cause divergence. |
| Silent deletion / truncation | Missing links (prev_event_hash) and manifest mismatch detection |
Deletion of newest tail events can appear as a stale snapshot if no external anchor/checkpoint is compared. |
| Identity takeover | Rotation requires surviving signer; self-promotion is prohibited | If all authorities are compromised, identity continuity is cryptographically lost (new genesis required). |
| Stale-write overwrite | Fencing token includes latest event ID, timestamp, nonce, and signature | Long offline windows increase merge complexity and contested state volume. |
| Fork-and-discard (equivocation) | Fork detection by identical actor + identical prev_event_hash; conflicting branches remain auditable |
Governance must define how forks are adjudicated and whether one branch is quarantined. |
| Time/backdating manipulation | Event ID is content-addressed and signed; chain linkage is authoritative, not wall-clock time | timestamp_utc is still untrusted metadata and can mislead human readers if not cross-checked. |
| Resource-exhaustion (DoS) | Streaming hash/verification, bounded input handling, malformed-line rejection | Operators must enforce max event size, max payload depth, and replay quotas in production deployments. |
| Key ID collision attempt | Key IDs are SHA-256-derived; practical collision resistance at current security levels | Profile migration plan needed if SHA-256 security assumptions materially degrade in future. |
18.2 Mandatory Validation Order
To reduce attack surface and ambiguous error handling, implementations SHOULD validate in this order:
- UTF-8 + JSON parse checks
- Required field checks
- Event ID recomputation checks
- Causal chain linkage checks
- Key resolution/revocation checks
- Signature verification checks
- Manifest and Merkle verification checks
Fail closed at the first critical invariant break and emit the corresponding PROVARA_E### code.
18.3 Replay and Duplication Attacks
Replay of already-seen events is constrained by unique event_id and dedup rules. Implementations MUST reject duplicate IDs in contexts that require uniqueness and MUST NOT permit duplicate events to mutate derived state more than once.
For delta import/sync workflows, unknown event types are preserved, but replayed known events still require full ID/signature/chain validation before acceptance.
18.4 Truncation and Partial Snapshot Risk
Protocol checks can prove integrity of what is present; they cannot prove the local copy is globally complete without comparison to a stronger anchor (signed checkpoint, remote peer state, or externally pinned digest).
Operational guidance:
- Keep signed checkpoints.
- Compare latest known event IDs across peers.
- Treat unexplained chain-head regressions as incident conditions.
18.5 Time Semantics and Backdating
timestamp_utc supports ordering and audit readability but is not a trust root. Chain linkage and signatures are authoritative. Implementations SHOULD prefer deterministic chain order (timestamp_utc, then event_id) and SHOULD log suspicious clock drift for operators.
18.6 Privacy and Confidentiality
Provara provides integrity and authenticity, not confidentiality. Events are plaintext JSON unless encrypted upstream.
Recommendations:
- Encrypt sensitive payloads before event creation.
- Avoid direct storage of PII/secrets in
payload.value. - Separate key-management concerns from vault transport/storage concerns.
18.7 Key Compromise and Rotation Boundaries
Key compromise is handled by KEY_REVOCATION + KEY_PROMOTION signed by surviving authority. Implementations SHOULD surface trust_boundary_event_id prominently in audit output to distinguish trusted pre-compromise history from post-compromise uncertainty.
18.8 Denial-of-Service Considerations
Implementations SHOULD set explicit limits for:
- maximum event byte size
- maximum payload nesting depth
- maximum lines processed per import batch
- maximum replay wall-clock budget before checkpoint fallback
Malformed input MUST be rejected or quarantined deterministically, never silently coerced into valid state transitions.
18.9 Cryptographic Agility
Profile A fixes SHA-256 and Ed25519 for interoperability. Future profiles SHOULD define migration events and dual-signing windows for algorithm transition (including post-quantum profiles) without breaking append-only verification semantics.
19. Extension Registry Process
Custom event type and future core-type promotion workflow is defined in:
docs/EXTENSION_REGISTRY.md
This process is the canonical governance path for extension naming, acceptance, and promotion.
20. IANA-Style Considerations
A standards-track stub (media type, URN, registry concepts) is maintained in:
docs/IANA_CONSIDERATIONS.md
This is informational for future interoperability and does not override v1.0 profile authority.
Derived from PROTOCOL_PROFILE.txt (frozen normative spec). In case of conflict, the profile wins.