diff --git a/CHANGELOG.md b/CHANGELOG.md index afb7a82..5d83de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All notable changes to the sovp Python package are documented here. -Protocol specification: [draft-litzki-sovp-02](https://datatracker.ietf.org/doc/draft-litzki-sovp/) +Protocol specification: [draft-litzki-sovp-03](https://datatracker.ietf.org/doc/draft-litzki-sovp/) ## [1.0.3] — 2026-06-09 diff --git a/README.md b/README.md index 1b54a84..be3f0af 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![CI](https://github.com/litzki-systems/sovp-python/actions/workflows/ci.yml/badge.svg)](https://github.com/litzki-systems/sovp-python/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](https://www.python.org/downloads/) -[![IETF Draft](https://img.shields.io/badge/IETF-draft--litzki--sovp--02-lightgrey.svg)](https://datatracker.ietf.org/doc/draft-litzki-sovp/) +[![IETF Draft](https://img.shields.io/badge/IETF-draft--litzki--sovp--03-lightgrey.svg)](https://datatracker.ietf.org/doc/draft-litzki-sovp/) [![Status](https://img.shields.io/badge/Status-Patent_Pending-orange.svg)](https://litzki-systems.com/sovp) --- @@ -29,6 +29,21 @@ Psi_core = Verify(K_pub, sigma, JCS(M)) --- +## Position in the agentic trust stack + +SOVP is the infrastructure attestation layer inside a broader agentic trust stack. The four layers, from discovery to runtime: + +| Layer | Concern | Mechanism | +|---|---|---| +| **Discovery** | Is the source findable and routable? | DNS, service registries, `ai-catalog.json` | +| **Install safety** | Is the artifact what it claims to be before execution? | `contentAddress` digest (SHA-256 over JCS bytes), SOVP `trustManifest` type | +| **Infrastructure trust** | Does the serving entity control the domain and key? | SOVP `sovp-identity.json`, `_sovp` DNS TXT, Ed25519 proof | +| **Runtime governance** | Is the agent permitted to act on this data in this context? | Policy engines, capability tokens, audit logs | + +SOVP operates at **layers 2 and 3**. If you arrived here from [ards-project/ard-spec issue #41](https://github.com/ards-project/ard-spec/issues/41): the `trustManifest` type in an `ai-catalog.json` entry maps to layer 2 — it binds a catalog entry's `contentAddress` digest to an independently verifiable infrastructure attestation, so a consuming agent can confirm the entry was produced by the declared entity before acting on it. + +--- + ## How is this different from DANE or DIDs? | | SOVP | DANE | W3C DIDs | @@ -115,7 +130,7 @@ from sovp.core import sign_identity import json # Non-proof fields only — integrity_proof is always excluded from the signed scope -# (draft-litzki-sovp-02, Section 4 MUST). sign_identity() will strip it automatically if present. +# (draft-litzki-sovp-03, Section 4 MUST). sign_identity() will strip it automatically if present. metadata = { "@context": "https://litzki-systems.com/protocol/v1.4", "@type": "SovereignIdentity", @@ -200,6 +215,9 @@ sovp verify --payload test_payload.json --sig --pubkey " + }, "parameters": { "entropy_threshold": 0.12, "determinism_score": 0.98 @@ -207,6 +225,8 @@ sovp verify --payload test_payload.json --sig --pubkey **`contentAddress` (optional, draft-litzki-sovp-03 Section 4):** `contentAddress.digest` is a SHA-256 hash computed over the JCS-canonical representation of all non-proof, non-`contentAddress` fields. A verifier independently recomputes it as `sha256(JCS(doc_without_proof_and_contentAddress))` and compares the hex string. This lets downstream consumers (e.g. an `ai-catalog.json` entry) bind a catalog record to the exact document bytes without re-running the Ed25519 signature check. **`contentAddress` is excluded from the Ed25519 signed scope** — it is computed after signing, from the same byte range the signature covers. + > **Note:** `parameters` is non-normative and MUST NOT be used for trust > decisions (draft Section 4). It is excluded from the signed scope. @@ -262,6 +282,7 @@ Result: VERIFIED — identity and integrity confirmed. | RFC conformance test vectors | Implemented — see `tests/test_vectors.py` | | `SOVPIdentity` / `SOVPSigner` / `SOVPValidator` class API | Planned | | IETF Internet-Draft | [draft-litzki-sovp-03](https://datatracker.ietf.org/doc/draft-litzki-sovp/) — active (updated 2026-06-09) | +| ARD `trustManifest` type registration | In progress — [ards-project/ard-spec #41](https://github.com/ards-project/ard-spec/issues/41) | | U.S. Provisional Patent | Filed — No. 64/005,737 | --- diff --git a/CHANGELOG-v02.md b/docs/draft-v02-planned-changes.md similarity index 100% rename from CHANGELOG-v02.md rename to docs/draft-v02-planned-changes.md diff --git a/examples/ard_trust_manifest.py b/examples/ard_trust_manifest.py new file mode 100644 index 0000000..bb85836 --- /dev/null +++ b/examples/ard_trust_manifest.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Litzki Systems LLC +# SPDX-License-Identifier: Apache-2.0 +# +# Shows how a SOVP v1 entry looks inside an ai-catalog.json trustManifest. +# This is the pattern being proposed in ards-project/ard-spec issue #41. +# +# Run: python examples/ard_trust_manifest.py + +import hashlib +import json +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from sovp.core import generate_keypair, generate_identity_document, verify_identity + +try: + from canonicaljson import encode_canonical_json +except ImportError: + print("Install canonicaljson: pip install canonicaljson", file=sys.stderr) + sys.exit(1) + +print("=== SOVP ARD trustManifest Example ===\n") + +# ── Step 1: Produce a signed sovp-identity.json ──────────────────────────────── +private_key_b64, public_key_b64 = generate_keypair() + +document = generate_identity_document( + private_key_b64=private_key_b64, + entity_uid="urn:sovp:example-publisher", + canonical_url="https://publisher.example", +) + +# ── Step 2: Compute contentAddress over the non-proof, non-contentAddress fields +# +# The digest covers the same byte range that the Ed25519 signature covers: +# JCS(document minus integrity_proof minus contentAddress). +# This lets a catalog consumer verify content identity without re-running Ed25519. +signed_fields = {k: v for k, v in document.items() + if k not in ("integrity_proof", "contentAddress")} +canonical_bytes = encode_canonical_json(signed_fields) +digest_hex = hashlib.sha256(canonical_bytes).hexdigest() + +document["contentAddress"] = {"digest": f"sha256:{digest_hex}"} + +print("sovp-identity.json (with contentAddress):") +print(json.dumps(document, indent=2)) +print() + +# ── Step 3: Build the ai-catalog.json entry (trustManifest type) ─────────────── +# +# This is the structure an ARD-compliant catalog producer emits. +# The `contentAddress` here matches the one inside the SOVP document, +# giving downstream agents a single hash to pin before fetching the full +# identity document. +catalog_entry = { + "schemaVersion": "ard/1.0", + "type": "trustManifest", + "uri": "https://publisher.example/.well-known/sovp-identity.json", + "contentAddress": { + "digest": f"sha256:{digest_hex}" + }, + "sovp": { + "draftVersion": "draft-litzki-sovp-03", + "entityUid": document["entity"]["uid"], + "canonicalUrl": document["entity"]["canonical_url"], + "publicKeyRef": document["integrity_proof"]["public_key_ref"] + } +} + +print("ai-catalog.json entry (trustManifest):") +print(json.dumps(catalog_entry, indent=2)) +print() + +# ── Step 4: Consuming agent verifies the chain ───────────────────────────────── +# +# 1. Fetch the SOVP document from catalog_entry["uri"]. +# 2. Recompute the digest and compare to catalog_entry["contentAddress"]["digest"]. +# 3. Run SOVP Psi_core check to confirm infrastructure trust. + +fetched_doc = document # in production: HTTP GET catalog_entry["uri"] + +recomputed_fields = {k: v for k, v in fetched_doc.items() + if k not in ("integrity_proof", "contentAddress")} +recomputed_bytes = encode_canonical_json(recomputed_fields) +recomputed_digest = "sha256:" + hashlib.sha256(recomputed_bytes).hexdigest() + +digest_ok = recomputed_digest == catalog_entry["contentAddress"]["digest"] +print(f"contentAddress match : {'PASS' if digest_ok else 'FAIL'}") + +signature = fetched_doc["integrity_proof"]["signature"] +psi_core = verify_identity(fetched_doc, signature, public_key_b64) +print(f"Psi_core : {'1 VERIFIED' if psi_core else '0 BLOCKED'}") + +assert digest_ok and psi_core, "Chain verification failed." +print("\n=== Done — full trustManifest chain verified ===")