Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---
Expand All @@ -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 |
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -200,13 +215,18 @@ sovp verify --payload test_payload.json --sig <base64-signature> --pubkey <base6
"public_key_ref": "dns:txt:_sovp.yourdomain.tld",
"nonce": "optional-unique-string"
},
"contentAddress": {
"digest": "sha256:<hex-encoded SHA-256 over JCS-canonical bytes of the non-proof fields>"
},
"parameters": {
"entropy_threshold": 0.12,
"determinism_score": 0.98
}
}
```

> **`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.

Expand Down Expand Up @@ -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 |

---
Expand Down
File renamed without changes.
98 changes: 98 additions & 0 deletions examples/ard_trust_manifest.py
Original file line number Diff line number Diff line change
@@ -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 ===")