diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c2a70..fab3c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.2.0] - 2026-03-08 -Protocol-first restructure, TypeScript implementation, finalized URI scheme, and full CapsuleType conformance. +Protocol-first restructure, TypeScript implementation, finalized URI scheme, full CapsuleType conformance, Security Considerations in spec, cryptographic chain verification, and 11-framework compliance directory. ### Changed @@ -24,19 +24,30 @@ Protocol-first restructure, TypeScript implementation, finalized URI scheme, and - Protocol documentation at `docs/` (language-agnostic) - Python-specific docs at `reference/python/docs/` - No `pyproject.toml` at repo root — the repo is a protocol, not a package +- **Compliance restructured into per-framework directory** — `docs/compliance.md` replaced by `docs/compliance/` with individual documents per framework and a README index. ### Added -- **`capsule://` URI scheme (Active)** — content-addressable references to Capsule records via their SHA3-256 hash. Spec at `spec/uri-scheme.md`, finalized from Draft to Active. Supports hash references (`capsule://sha3_`), chain references (`capsule://chain/42`), ID references, and fragment syntax into the 6 sections. Includes URI conformance vectors at `conformance/uri-fixtures.json`. -- **TypeScript reference implementation** — full CPS-conformant implementation at `reference/typescript/`: Capsule model with factories, canonical JSON serializer (CPS Section 2 with float-path handling), SHA3-256 hashing, Ed25519 seal/verify, and chain verification. Passes all 16 golden fixtures. 101 tests, 100% coverage (v8). Uses `@noble/hashes` ^2.0.1, `@noble/ed25519` ^3.0.0, vitest ^4.0.0, TypeScript ^5.9.0. Node.js >= 20.19.0. -- **Implementor's Guide** (`docs/implementors-guide.md`) — step-by-step instructions for building a conformant CPS implementation in any language, with language-specific pitfalls for TypeScript, Go, and Rust. -- **Why Capsules** (`docs/why-capsules.md`) — the case for cryptographic AI memory, aimed at decision-makers and architects. -- **URI scheme security considerations** — `spec/uri-scheme.md` includes: URI injection validation, resolution trust model, denial-of-service mitigations, fragment path traversal safety, no ambient authority principle. +- **Security Considerations in CPS spec** (`spec/README.md` Section 7) — documents what CPS provides (integrity, authenticity, non-repudiation, ordering, quantum resistance) and what it does not (confidentiality, truthfulness, availability, identity binding). Covers signer key compromise, chain truncation, verification levels, replay, and timestamp trust. +- **Cryptographic chain verification** — `chain.verify(verify_content=True)` recomputes SHA3-256 from content and compares to stored hash. `chain.verify(seal=seal_instance)` also verifies Ed25519 signatures. Both Python and TypeScript implementations. Default structural-only behavior is unchanged (backward compatible). +- **11-framework compliance directory** (`docs/compliance/`) — per-framework regulatory mappings: NIST SP 800-53, NIST AI RMF, EU AI Act, SOC 2, ISO 27001, HIPAA, GDPR, PCI DSS, FedRAMP, FINRA, CMMC. Each document maps protocol-level capabilities to specific controls and lists complementary controls outside the protocol's scope. +- **NIST RFI submission archive** (`nist-submission/`) — exact artifacts submitted to NIST (Docket NIST-2025-0035), SHA-256 checksums, and README with normative/informative classification. +- **`capsule://` URI scheme (Active)** — content-addressable references to Capsule records via their SHA3-256 hash. Spec at `spec/uri-scheme.md`, finalized from Draft to Active. Supports hash references (`capsule://sha3_`), chain references (`capsule://chain/42`), ID references, and fragment syntax into the 6 sections. - **URI conformance vectors** (`conformance/uri-fixtures.json`) — 10 valid and 11 invalid URI parsing test vectors for cross-language URI parser verification. +- **TypeScript reference implementation** — full CPS-conformant implementation at `reference/typescript/`: Capsule model with factories, canonical JSON serializer (CPS Section 2 with float-path handling), SHA3-256 hashing, Ed25519 seal/verify, and chain verification with `verifyContent` option. Passes all 16 golden fixtures. 101 tests, 100% coverage (v8). Uses `@noble/hashes` ^2.0.1, `@noble/ed25519` ^3.0.0, vitest ^4.0.0, TypeScript ^5.9.0. Node.js >= 20.19.0. +- **TypeScript release workflow** (`.github/workflows/typescript-release.yaml`) — npm publish with provenance on version tags, gated by conformance tests. - **`vault` golden fixture** — conformance suite now covers all 8 CapsuleTypes (16 total fixtures, up from 15). The `vault_secret` fixture tests secret rotation with policy-based authority. -- **Protocol structure tests** (`reference/python/tests/test_protocol_structure.py`) — guards the protocol-first layout, spec completeness, conformance suite integrity, TypeScript type alignment with spec, markdown link resolution, CI configuration, and root-level file requirements. +- **Implementor's Guide** (`docs/implementors-guide.md`) — step-by-step instructions for building a conformant CPS implementation in any language, with URI parsing section and language-specific pitfalls. +- **Why Capsules** (`docs/why-capsules.md`) — the case for cryptographic AI memory. +- **Protocol structure tests** — guards the protocol-first layout, spec completeness (including Security Considerations), conformance suite integrity, URI vectors, compliance directory, TypeScript alignment, markdown links, CI configuration, and root-level files. - **Dependabot for TypeScript** — npm dependency updates for `reference/typescript/`. +### Security + +- `chain.verify()` now supports cryptographic verification (`verify_content=True`, `seal=`) in addition to structural-only checks. Structural verification alone trusts stored hash values; cryptographic verification recomputes from content. +- Hash computation in chain verification uses the canonical `compute_hash()` function (Python) and `computeHash(toDict())` (TypeScript) to prevent divergence from the sealing path. +- Spec Section 7 explicitly documents non-goals: no confidentiality, no content truthfulness, no availability guarantees, no identity binding. + ### Updated - **Python dependencies** — pytest >=9.0.0, pytest-asyncio >=1.0.0, ruff >=0.15.0, mypy >=1.19.0, sqlalchemy >=2.0.48, asyncpg >=0.31.0, liboqs-python >=0.14.1. diff --git a/docs/architecture.md b/docs/architecture.md index 89c9eac..3cd452d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -248,12 +248,21 @@ Capsule #0 Capsule #1 Capsule #2 ### Chain Verification -`chain.verify()` walks every Capsule in sequence order and checks: +`chain.verify()` supports two verification levels: + +**Structural** (default, fast): walks every Capsule in sequence order and checks: 1. Sequence numbers are consecutive: 0, 1, 2, ... 2. Each Capsule's `previous_hash` matches the previous Capsule's `hash` 3. The genesis Capsule (sequence 0) has `previous_hash = None` +**Cryptographic** (`verify_content=True`): everything above, plus: + +4. Recomputes SHA3-256 from content and compares to stored hash +5. Optionally verifies Ed25519 signatures (when `seal=` is provided) + +Structural verification trusts stored hash values. Cryptographic verification catches storage-level tampering where an attacker modifies content without the signing key. See [CPS Section 7.5](../spec/README.md) for the security rationale. + If any check fails, the result includes the Capsule ID where the chain broke and the number of Capsules verified before the break. ### Multi-Tenant Chains diff --git a/docs/implementors-guide.md b/docs/implementors-guide.md index b7c2daa..c2b86a1 100644 --- a/docs/implementors-guide.md +++ b/docs/implementors-guide.md @@ -116,6 +116,10 @@ Seal fields (`hash`, `signature`, `signature_pq`, `signed_at`, `signed_by`) are ## Step 6: Chain Verification +Implementations SHOULD support two verification levels (see [CPS Section 7.5](../spec/README.md)): + +**Structural** (fast): + ``` 1. Load all Capsules in sequence order 2. Verify sequence numbers are consecutive: 0, 1, 2, ... @@ -123,6 +127,17 @@ Seal fields (`hash`, `signature`, `signature_pq`, `signed_at`, `signed_by`) are 4. For each subsequent Capsule, verify previous_hash = hash of previous Capsule ``` +**Cryptographic** (thorough): + +``` +All structural checks, plus: +5. For each Capsule, recompute SHA3-256 from content via to_dict() + canonicalize() +6. Compare recomputed hash to stored hash (detects storage-level tampering) +7. Optionally verify Ed25519 signature on each Capsule +``` + +Structural verification trusts stored hash values. Cryptographic verification catches content tampering where the attacker does not have the signing key. + --- ## Step 7: Conformance Testing diff --git a/reference/python/src/qp_capsule/chain.py b/reference/python/src/qp_capsule/chain.py index fc46618..a06cde6 100644 --- a/reference/python/src/qp_capsule/chain.py +++ b/reference/python/src/qp_capsule/chain.py @@ -23,6 +23,8 @@ from dataclasses import dataclass from typing import TYPE_CHECKING +from qp_capsule.seal import compute_hash + if TYPE_CHECKING: from qp_capsule.capsule import Capsule from qp_capsule.protocol import CapsuleStorageProtocol @@ -88,21 +90,36 @@ async def add(self, capsule: Capsule, tenant_id: str | None = None) -> Capsule: return capsule - async def verify(self, tenant_id: str | None = None) -> ChainVerificationResult: + async def verify( + self, + tenant_id: str | None = None, + *, + verify_content: bool = False, + seal: Seal | None = None, + ) -> ChainVerificationResult: """ Verify the entire chain integrity. - Checks: + Structural checks (always): 1. Sequence numbers are consecutive (0, 1, 2, ...) 2. Each Capsule's previous_hash matches the previous Capsule's hash 3. First Capsule has previous_hash = None + Cryptographic checks (when verify_content=True or seal is provided): + 4. Recompute SHA3-256 from content and compare to stored hash + 5. Verify Ed25519 signature (requires seal) + Args: tenant_id: If provided, verify only this tenant's chain + verify_content: If True, recompute hashes from content + seal: If provided, also verify Ed25519 signatures (implies verify_content) Returns: ChainVerificationResult with validity and error details """ + if seal is not None: + verify_content = True + capsules = await self.storage.get_all_ordered(tenant_id=tenant_id) if not capsules: @@ -120,7 +137,6 @@ async def verify(self, tenant_id: str | None = None) -> ChainVerificationResult: # Check previous_hash if i == 0: - # First Capsule should have no previous if capsule.previous_hash is not None: return ChainVerificationResult( valid=False, @@ -129,7 +145,6 @@ async def verify(self, tenant_id: str | None = None) -> ChainVerificationResult: capsules_verified=0, ) else: - # Subsequent Capsules should link to previous expected_prev = capsules[i - 1].hash if capsule.previous_hash != expected_prev: return ChainVerificationResult( @@ -139,6 +154,24 @@ async def verify(self, tenant_id: str | None = None) -> ChainVerificationResult: capsules_verified=i, ) + if verify_content: + computed = compute_hash(capsule.to_dict()) + if computed != capsule.hash: + return ChainVerificationResult( + valid=False, + error=f"Content hash mismatch at sequence {i}", + broken_at=str(capsule.id), + capsules_verified=i, + ) + + if seal is not None and not seal.verify(capsule): + return ChainVerificationResult( + valid=False, + error=f"Signature verification failed at sequence {i}", + broken_at=str(capsule.id), + capsules_verified=i, + ) + return ChainVerificationResult( valid=True, capsules_verified=len(capsules), diff --git a/reference/python/tests/test_chain.py b/reference/python/tests/test_chain.py index 0b06ca0..ecad53b 100644 --- a/reference/python/tests/test_chain.py +++ b/reference/python/tests/test_chain.py @@ -4,15 +4,35 @@ Tests chain integrity and verification. """ +import json import tempfile from pathlib import Path import pytest +from sqlalchemy import select from qp_capsule.capsule import Capsule, TriggerSection from qp_capsule.chain import CapsuleChain from qp_capsule.seal import Seal -from qp_capsule.storage import CapsuleStorage +from qp_capsule.storage import CapsuleModel, CapsuleStorage + + +async def _tamper_stored_capsule( + storage: CapsuleStorage, sequence: int, field: str, value: str +) -> None: + """Tamper with a capsule's serialized data in the database without updating its hash.""" + await storage._ensure_db() + factory = storage._get_session_factory() + async with factory() as session: + result = await session.execute( + select(CapsuleModel).where(CapsuleModel.sequence == sequence) + ) + model = result.scalar_one() + data = json.loads(model.data) + section, key = field.split(".", 1) + data[section][key] = value + model.data = json.dumps(data) + await session.commit() @pytest.fixture @@ -185,6 +205,187 @@ async def test_sequence_gap_fails(self, chain, seal, storage): assert "gap" in result.error.lower() or "Sequence" in result.error +class TestChainCryptographicVerification: + """Test chain verification with content hash recomputation and signature checks.""" + + @pytest.mark.asyncio + async def test_verify_content_passes_for_valid_chain(self, chain, seal, storage): + """verify_content=True passes when content matches stored hashes.""" + for i in range(3): + capsule = create_capsule(f"capsule_{i}") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + result = await chain.verify(verify_content=True) + + assert result.valid is True + assert result.capsules_verified == 3 + + @pytest.mark.asyncio + async def test_verify_content_detects_tampered_content(self, chain, seal, storage): + """verify_content=True catches content modified after sealing.""" + capsule = create_capsule("original") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + await _tamper_stored_capsule(storage, sequence=0, field="trigger.request", value="tampered") + + result = await chain.verify(verify_content=True) + + assert result.valid is False + assert "hash mismatch" in result.error.lower() + + @pytest.mark.asyncio + async def test_verify_with_seal_passes_for_valid_chain(self, chain, seal, storage): + """Passing seal= verifies signatures on each capsule.""" + for i in range(3): + capsule = create_capsule(f"capsule_{i}") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + result = await chain.verify(seal=seal) + + assert result.valid is True + assert result.capsules_verified == 3 + + @pytest.mark.asyncio + async def test_verify_with_seal_detects_bad_signature(self, chain, seal, storage): + """Passing seal= catches forged signatures.""" + capsule = create_capsule("test") + capsule = await chain.add(capsule) + seal.seal(capsule) + capsule.signature = "00" * 64 + await storage.store(capsule) + + result = await chain.verify(seal=seal) + + assert result.valid is False + assert "Signature verification failed" in result.error + + @pytest.mark.asyncio + async def test_structural_only_misses_content_tampering(self, chain, seal, storage): + """Default (structural-only) verification does NOT catch content tampering.""" + capsule = create_capsule("original") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + await _tamper_stored_capsule(storage, sequence=0, field="trigger.request", value="tampered") + + result = await chain.verify() + + assert result.valid is True, ( + "Structural verification should not catch content tampering " + "(this is why verify_content=True exists)" + ) + + @pytest.mark.asyncio + async def test_seal_implies_verify_content(self, chain, seal, storage): + """Passing seal= implies verify_content=True.""" + capsule = create_capsule("original") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + await _tamper_stored_capsule(storage, sequence=0, field="trigger.request", value="tampered") + + result = await chain.verify(seal=seal) + + assert result.valid is False + assert "hash mismatch" in result.error.lower() + + @pytest.mark.asyncio + async def test_empty_chain_with_verify_content(self, chain): + """Empty chain is valid even with verify_content=True.""" + result = await chain.verify(verify_content=True) + + assert result.valid is True + assert result.capsules_verified == 0 + + @pytest.mark.asyncio + async def test_single_capsule_with_verify_content(self, chain, seal, storage): + """Single capsule chain passes cryptographic verification.""" + capsule = create_capsule("solo") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + result = await chain.verify(verify_content=True) + + assert result.valid is True + assert result.capsules_verified == 1 + + @pytest.mark.asyncio + async def test_tamper_in_middle_of_chain(self, chain, seal, storage): + """Tampering in the middle of the chain is caught by verify_content.""" + for i in range(5): + capsule = create_capsule(f"capsule_{i}") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + await _tamper_stored_capsule( + storage, sequence=2, field="trigger.request", value="tampered_middle" + ) + + result = await chain.verify(verify_content=True) + + assert result.valid is False + assert result.capsules_verified == 2 + assert "hash mismatch" in result.error.lower() + + @pytest.mark.asyncio + async def test_default_verify_is_backward_compatible(self, chain, seal, storage): + """Default verify() with no new params behaves identically to original.""" + for i in range(3): + capsule = create_capsule(f"capsule_{i}") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + result = await chain.verify() + + assert result.valid is True + assert result.capsules_verified == 3 + assert result.error is None + assert result.broken_at is None + + @pytest.mark.asyncio + async def test_verify_content_reports_correct_broken_at(self, chain, seal, storage): + """verify_content reports the ID of the tampered capsule.""" + for i in range(3): + capsule = create_capsule(f"capsule_{i}") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + all_capsules = await storage.get_all_ordered() + tampered_id = str(all_capsules[1].id) + await _tamper_stored_capsule(storage, sequence=1, field="trigger.request", value="tampered") + + result = await chain.verify(verify_content=True) + + assert result.valid is False + assert result.broken_at == tampered_id + + @pytest.mark.asyncio + async def test_verify_content_false_is_structural_only(self, chain, seal, storage): + """Explicitly passing verify_content=False skips hash recomputation.""" + capsule = create_capsule("original") + capsule = await chain.add(capsule) + seal.seal(capsule) + await storage.store(capsule) + + await _tamper_stored_capsule(storage, sequence=0, field="trigger.request", value="tampered") + + result = await chain.verify(verify_content=False) + + assert result.valid is True + + class TestChainOperations: """Test chain utility operations.""" diff --git a/reference/python/tests/test_protocol_structure.py b/reference/python/tests/test_protocol_structure.py index 04cdb91..8a9f5b2 100644 --- a/reference/python/tests/test_protocol_structure.py +++ b/reference/python/tests/test_protocol_structure.py @@ -99,6 +99,36 @@ def test_spec_defines_hash_chain(self): assert "previous_hash" in text assert "sequence" in text + def test_spec_has_security_considerations(self): + """CPS spec must include a Security Considerations section.""" + text = (REPO_ROOT / "spec" / "README.md").read_text() + assert "Security Considerations" in text + + def test_spec_security_covers_key_compromise(self): + text = (REPO_ROOT / "spec" / "README.md").read_text() + assert "Key Compromise" in text or "Signer Key Compromise" in text + + def test_spec_security_covers_chain_truncation(self): + text = (REPO_ROOT / "spec" / "README.md").read_text() + assert "Chain Truncation" in text + + def test_spec_security_covers_verification_levels(self): + text = (REPO_ROOT / "spec" / "README.md").read_text() + assert "Verification Levels" in text or "verify_content" in text + + def test_spec_security_documents_non_goals(self): + """Spec must be explicit about what CPS does NOT provide.""" + text = (REPO_ROOT / "spec" / "README.md").read_text() + assert "Does Not Provide" in text + + def test_spec_security_covers_timestamp_trust(self): + text = (REPO_ROOT / "spec" / "README.md").read_text() + assert "Timestamp" in text and "trust" in text.lower() + + def test_spec_security_covers_replay(self): + text = (REPO_ROOT / "spec" / "README.md").read_text() + assert "Replay" in text + def test_uri_scheme_defines_capsule_protocol(self): text = (REPO_ROOT / "spec" / "uri-scheme.md").read_text() assert "capsule://" in text diff --git a/reference/typescript/src/chain.ts b/reference/typescript/src/chain.ts index 1399418..6ecbac8 100644 --- a/reference/typescript/src/chain.ts +++ b/reference/typescript/src/chain.ts @@ -8,6 +8,8 @@ */ import type { Capsule } from "./capsule.js"; +import { toDict } from "./capsule.js"; +import { computeHash } from "./seal.js"; export interface ChainVerificationResult { valid: boolean; @@ -16,17 +18,31 @@ export interface ChainVerificationResult { capsules_verified: number; } +export interface ChainVerifyOptions { + /** Recompute SHA3-256 from content and compare to stored hash. */ + verifyContent?: boolean; +} + /** * Verify the integrity of a Capsule chain. * - * Checks (per CPS Section 4): + * Structural checks (always): * 1. Sequence numbers are consecutive (0, 1, 2, ...) * 2. Each Capsule's previous_hash matches the prior Capsule's hash * 3. Genesis Capsule (sequence 0) has previous_hash = null * + * Cryptographic check (when verifyContent is true): + * 4. Recompute SHA3-256 from content and compare to stored hash + * * @param capsules Array of Capsules sorted by sequence (ascending) + * @param options Optional verification options */ -export function verifyChain(capsules: Capsule[]): ChainVerificationResult { +export function verifyChain( + capsules: Capsule[], + options?: ChainVerifyOptions, +): ChainVerificationResult { + const verifyContent = options?.verifyContent ?? false; + if (capsules.length === 0) { return { valid: true, error: null, broken_at: null, capsules_verified: 0 }; } @@ -63,6 +79,18 @@ export function verifyChain(capsules: Capsule[]): ChainVerificationResult { }; } } + + if (verifyContent) { + const computed = computeHash(toDict(capsule)); + if (computed !== capsule.hash) { + return { + valid: false, + error: `Content hash mismatch at sequence ${i}: stored hash does not match recomputed hash`, + broken_at: capsule.id, + capsules_verified: i, + }; + } + } } return { diff --git a/spec/README.md b/spec/README.md index 24dec5b..b8396dd 100644 --- a/spec/README.md +++ b/spec/README.md @@ -329,7 +329,69 @@ For a conformant implementation in any language: --- -## 7. URI Scheme +## 7. Security Considerations + +### 7.1 What CPS Provides + +| Property | Mechanism | Strength | +|---|---|---| +| **Integrity** | SHA3-256 content hash | Any modification changes the hash | +| **Authenticity** | Ed25519 signature | Proves which key signed the record | +| **Non-repudiation** | Signature + `signed_by` fingerprint | Third-party verification via `verify_with_key()` | +| **Temporal ordering** | Hash chain (`previous_hash` + `sequence`) | Insertion, deletion, and reordering are detectable | +| **Quantum resistance** | Optional ML-DSA-65 dual signature | FIPS 204, additive to Ed25519 | + +### 7.2 What CPS Does Not Provide + +| Property | Reason | +|---|---| +| **Confidentiality** | Capsule content is plaintext JSON. The protocol provides integrity, not encryption. Field-level encryption is a deployment concern. | +| **Content truthfulness** | The seal proves a record has not been modified after creation. It does not prove the content was accurate when created. A compromised or misaligned agent can record fabricated reasoning that passes all cryptographic checks. | +| **Availability** | The protocol cannot force an application to create Capsules. If the runtime is compromised, it may skip record creation entirely. The chain shows no gap because the record was never created. | +| **Identity binding** | `signed_by` contains a key fingerprint, not an identity. The protocol does not bind keys to agents, organizations, or runtimes. Whoever holds the private key IS the signer. | + +### 7.3 Signer Key Compromise + +If an attacker obtains the Ed25519 private key, they can forge Capsules that are indistinguishable from legitimate ones. Past Capsules remain valid. Mitigations: + +- Restrict key file permissions (reference implementation uses `0600` with `umask(0o077)`) +- Use HSM-bound keys in production to prevent extraction +- Rotate keys periodically; use `verify_with_key()` for old keys +- Enable ML-DSA-65 dual signatures so both algorithms must be compromised + +The protocol does not include key revocation or expiration. These are deployment-layer concerns. + +### 7.4 Chain Truncation + +If an attacker deletes the last N records from storage, the truncated chain still verifies as valid from genesis to the truncation point. The protocol has no "expected chain length" anchor. Mitigations: + +- Monitor chain length externally (compare expected vs. actual) +- Periodically checkpoint chain head hashes to an independent system +- Use append-only storage (e.g., S3 Object Lock, WORM storage) + +### 7.5 Chain Verification Levels + +Chain verification has two levels. Implementations SHOULD support both: + +1. **Structural verification** (fast): Check sequence numbers and `previous_hash` linkage. This trusts stored hash values without recomputing them. +2. **Cryptographic verification** (thorough): Recompute SHA3-256 from content for each record and optionally verify Ed25519 signatures. This detects storage-level content tampering. + +Structural verification alone does not detect an attacker who modifies both content and the stored hash. Cryptographic verification catches this because the signature will not match the recomputed hash (unless the signing key is also compromised). + +### 7.6 Replay + +A sealed Capsule is valid regardless of where it is stored. An attacker who copies a valid chain to a different storage backend creates a valid chain. The protocol does not bind chains to storage locations. Mitigations: + +- Use `tenant_id` scoping to isolate chains +- Verify chain provenance via external metadata (storage origin, deployment context) + +### 7.7 Timestamp Trust + +`trigger.timestamp` and `signed_at` are set by the creating application, not by a trusted time source. An application could backdate or future-date timestamps. The hash chain provides relative ordering (Capsule N was sealed after Capsule N-1), but absolute timestamps are only as trustworthy as the runtime clock. + +--- + +## 8. URI Scheme Capsules are content-addressable via the `capsule://` URI scheme. Every sealed Capsule can be referenced by its SHA3-256 hash: