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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ Protocol specification: [draft-litzki-sovp-04](https://datatracker.ietf.org/doc/

## [Unreleased]

### Changed
- `sovp/core.py` — Draft 04 host binding is enforced through `expected_host`; v2.0 freshness remains signed and expiry is always validated. Generated DNS key references use the URL hostname, excluding ports and normalizing trailing dots and case.
- `sovp/resolver.py` — DNS TXT parsing now accepts only the exact `v=SOVP1; k=<base64>` record shape and requires a valid 32-byte Ed25519 public key. Legacy endpoint fallback remains restricted to HTTP 404.
- `examples/` — live validation, end-to-end verification, and the ARD trustManifest example now identify Draft 04 explicitly and demonstrate v2.0 host binding and the current `contentAddress` shape.
- `workers/sovp-identity/` — the reference worker now serves a publisher-supplied, pre-signed Draft 04 v2.0 document from a Cloudflare secret instead of embedding a stale v1.4 artifact.

### Tests
- Added Draft 04 conformance coverage for freshness clock-skew boundaries, the 600-second issuance window, signed unknown top-level members, unsigned `contentAddress` and `scan` extensions, host normalization, URL ports, and exact DNS TXT syntax.


### Added
- `sovp/document_safety.py` — `parse_unverified_sovp_document()`, a minimal JSON parser enforcing draft-litzki-sovp-04's "Resource Limits for Unverified Documents": rejects a document larger than 64 KiB, nested deeper than 16 levels, or containing a duplicate member name in any object, before the document is trusted with any further processing.

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This is the reference implementation of an open protocol. Contributions that imp
- **Roadmap items** — features listed in the README Roadmap table
- **Documentation** — clarifications, typo fixes, example corrections

If you want to propose a change to the protocol itself (not just this implementation), the right venue is the IETF draft: [draft-litzki-sovp-03](https://litzki-systems.com/sovp).
If you want to propose a change to the protocol itself (not just this implementation), the right venue is the IETF draft: [draft-litzki-sovp-04](https://litzki-systems.com/sovp).

## How to submit a change

Expand Down
38 changes: 22 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# sovp-python

**sovp-python** is the reference implementation of the [Sovereign Validation Protocol (SOVP)](https://litzki-systems.com/sovp) — a pre-ingestion verification protocol that lets LLMs and autonomous agents cryptographically confirm the identity and integrity of a data source before parsing it. It exists because existing mechanisms (DANE, DIDs, TLS) operate at the wrong layer for agentic pipelines: SOVP sits at Layer 0, before the body is read. To get started: clone the repo and run `pip install -e .` — this exposes both a `sovp.core` Python API and a `sovp` CLI with three commands.
**sovp-python** is the reference implementation of the [Sovereign Validation Protocol (SOVP)](https://litzki-systems.com/sovp), a pre-ingestion verification protocol for validating a signed identity document before its contents are ingested. SOVP verifies that the document was signed by the party controlling an Ed25519 public key published for the document host in DNS. It operates at Layer 0, before the document body is ingested. To get started: clone the repo and run `pip install -e .`. This exposes the `sovp.core` Python API and the `sovp` CLI.

> **Protocol specification:** [draft-litzki-sovp](https://datatracker.ietf.org/doc/draft-litzki-sovp/) — IETF Internet-Draft

Expand Down Expand Up @@ -29,18 +29,19 @@ Psi_core = Verify(K_pub, sigma, JCS(M))

---

## Position in the agentic trust stack
## Protocol scope

SOVP is the infrastructure attestation layer inside a broader agentic trust stack. The four layers, from discovery to runtime:
SOVP defines a narrow cryptographic validation boundary at Layer 0. The core verification function is:

| 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 |
`Psi_core = Verify(K_pub, sigma, JCS(M))`

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.
where the public key is resolved from the host's `_sovp` DNS TXT records. For schema v2.0, the signed document also carries freshness data, including `created` and `expiresAt`. The reference resolver validates the document against the expected host and applies the v2.0 freshness rules.

SOVP establishes that the signed document is attributable to the party controlling the DNS-published Ed25519 key. Optional instance binding can associate a signed document with an expected instance identifier. Instance binding does not establish that a process is running as that instance.

SOVP does not establish legal identity, semantic accuracy or truthfulness, security posture, configuration or hardening state, runtime behavior, requester authorization, or permission to perform an action. Those properties require separate evidence and policy mechanisms. SOVP claims should therefore be composed with other systems only through explicit, separately defined evidence and claim semantics.

`contentAddress` provides an unsigned SHA-256 content identity over the same JCS representation used for the signature. It supports content consistency and downstream binding. It does not provide authenticity and must not be used as a substitute for signature verification.

---

Expand Down Expand Up @@ -69,7 +70,7 @@ cd sovp-python
pip install -e .
```

This installs the `sovp` CLI and the `sovp.core` library. Dependencies (`cryptography`, `canonicaljson`) are declared in `pyproject.toml`.
This installs the `sovp` CLI and the `sovp.core` library. Dependencies (`cryptography`, `jcs`, `dnspython`, `requests`) are declared in `pyproject.toml`.

---

Expand All @@ -92,7 +93,12 @@ document = generate_identity_document(

# 3. Verify (Psi_core)
signature = document["integrity_proof"]["signature"]
psi_core = verify_identity(document, signature, public_key_b64)
psi_core = verify_identity(
document,
signature,
public_key_b64,
expected_host="example.com",
)
print("Psi_core =", 1 if psi_core else 0) # → 1

# 4. Tamper detection
Expand Down Expand Up @@ -187,8 +193,8 @@ signed_payload = {
from sovp.core import verify_identity

# Pass the full document or the non-proof subset — both work identically.
# Enable check_timestamp=True to enforce the 600-second validity window
# (draft Section 7.2 SHOULD).
# Enable check_timestamp=True to enforce the 600-second issuance window
# defined by Draft 04 Section 7.2.
psi_core = verify_identity(signed_payload, signature, public_key_b64, check_timestamp=True)

if psi_core:
Expand All @@ -215,7 +221,7 @@ sovp verify --payload test_payload.json --sig <base64-signature> --pubkey <base6
| Canonicalization | JSON Canonicalization Scheme / JCS (RFC 8785) |
| Hashing | Ed25519 pure mode (RFC 8032) — `sign(JCS(M))`, no external pre-hash applied |
| Key distribution | DNS TXT at `_sovp.yourdomain.tld` |
| Replay protection | `freshness.created` timestamp validation (600 s window, `check_timestamp=True`), cryptographically bound to the signature since schema v2.0; nonce deduplication not yet implemented — see Roadmap |
| Freshness | Signed `freshness.created` and `freshness.expiresAt` validation; the reference resolver applies the 600 s issuance window when timestamp checking is enabled |

---

Expand Down Expand Up @@ -310,7 +316,7 @@ Result: VERIFIED — identity and integrity confirmed.
| RFC conformance test vectors | Implemented — see `tests/test_vectors.py` |
| Live validation (`validate_live.py`) | Implemented |
| AgenTrust Marketplace integration | Implemented — see [sovp-agentrust-bridge](https://github.com/litzki-systems/sovp-agentrust-bridge) |
| Reference identity-endpoint deployment (Cloudflare Worker) | Implemented — see `workers/sovp-identity`, live at litzki-systems.com |
| Reference identity-endpoint deployment (Cloudflare Worker) | Implemented — see `workers/sovp-identity`; deployment requires a fresh Draft 04 document and matching DNS key |
| Replay protection — nonce deduplication | Planned |
| `SOVPIdentity` / `SOVPSigner` / `SOVPValidator` class API | Planned |
| IETF Internet-Draft | [draft-litzki-sovp](https://datatracker.ietf.org/doc/draft-litzki-sovp/) — active |
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Version | Supported |
|---|---|
| 1.0.x | Yes |
| 1.1.x | Yes |

## Reporting a Vulnerability

Expand All @@ -23,7 +23,7 @@ the reporter.

This policy covers:
- The `sovp` Python package (`sovp.core`, `sovp.resolver`, `sovp.cli`)
- The SOVP protocol specification (`draft-litzki-sovp-03`)
- The SOVP protocol specification (`draft-litzki-sovp-04`)

This policy does not cover third-party dependencies. Please report
vulnerabilities in `cryptography`, `dnspython`, or `requests` to their
Expand Down
78 changes: 32 additions & 46 deletions examples/ard_trust_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,25 @@
# 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.
# Shows how a SOVP v2.0 identity document can be referenced from an
# ai-catalog.json trustManifest entry.
#
# Run: python examples/ard_trust_manifest.py

import hashlib
import json
import sys
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from sovp.core import generate_keypair, generate_identity_document, verify_identity
import jcs

try:
from canonicaljson import encode_canonical_json
except ImportError:
print("Install canonicaljson: pip install canonicaljson", file=sys.stderr)
sys.exit(1)
from sovp.core import generate_keypair, generate_identity_document, verify_identity

print("=== SOVP ARD trustManifest Example ===\n")

# ── Step 1: Produce a signed sovp-identity.json ────────────────────────────────
# Step 1: Produce a signed schema v2.0 sovp-identity.json document.
private_key_b64, public_key_b64 = generate_keypair()

document = generate_identity_document(
Expand All @@ -33,66 +29,56 @@
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}"}
digest_hex = document["contentAddress"]["digest"]

print("sovp-identity.json (with contentAddress):")
print("sovp-identity.json:")
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.
# Step 2: Build an ai-catalog.json reference.
catalog_entry = {
"schemaVersion": "ard/1.0",
"type": "trustManifest",
"uri": "https://publisher.example/.well-known/sovp-identity.json",
"contentAddress": {
"digest": f"sha256:{digest_hex}"
"alg": "sha256",
"digest": digest_hex,
},
"sovp": {
"draftVersion": "draft-litzki-sovp-03",
"draftVersion": "draft-litzki-sovp-04",
"entityUid": document["entity"]["uid"],
"canonicalUrl": document["entity"]["canonical_url"],
"publicKeyRef": document["integrity_proof"]["public_key_ref"]
}
"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"]
# Step 3: The consuming agent recomputes the unsigned content identity.
fetched_doc = document

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()
recomputed_fields = {
key: value
for key, value in fetched_doc.items()
if key not in ("integrity_proof", "contentAddress", "scan")
}
recomputed_bytes = jcs.canonicalize(recomputed_fields)
recomputed_digest = hashlib.sha256(recomputed_bytes).hexdigest()

digest_ok = recomputed_digest == catalog_entry["contentAddress"]["digest"]
print(f"contentAddress match : {'PASS' if digest_ok else 'FAIL'}")

# Step 4: Verify the signed document and its host binding.
signature = fetched_doc["integrity_proof"]["signature"]
psi_core = verify_identity(fetched_doc, signature, public_key_b64)
psi_core = verify_identity(
fetched_doc,
signature,
public_key_b64,
expected_host="publisher.example",
)
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 ===")
assert digest_ok and psi_core, "Draft 04 verification chain failed."
print("\n=== Done — trustManifest reference verified ===")
4 changes: 2 additions & 2 deletions examples/end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@
# ── Step 3: Verify — Psi_core resonance check ─────────────────────────────────
signature = document["integrity_proof"]["signature"]

psi_core = verify_identity(document, signature, public_key_b64)
psi_core = verify_identity(document, signature, public_key_b64, expected_host="example.com")
print(f"Psi_core = {'1 ✓ Verified — ingestion may proceed.' if psi_core else '0 ✗ Blocked.'}")
assert psi_core, "Verification should pass for an unmodified document."

# ── Step 4: Tamper detection ───────────────────────────────────────────────────
tampered = json.loads(json.dumps(document))
tampered["entity"]["canonical_url"] = "https://attacker.com"

psi_tampered = verify_identity(tampered, signature, public_key_b64)
psi_tampered = verify_identity(tampered, signature, public_key_b64, expected_host="example.com")
print(f"Psi_core = {'1' if psi_tampered else '0 ✗ Tamper detected — ingestion blocked.'}")
assert not psi_tampered, "Verification should fail after tampering."

Expand Down
6 changes: 3 additions & 3 deletions examples/validate_live.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""
SOVP Live Validation Example
Demonstrates full pipeline: DNS TXT resolution + HTTP fetch + Ed25519 verify
against a production SOVP-certified domain.
Demonstrates the Draft 04 pipeline: DNS TXT resolution, HTTP retrieval,
host binding, freshness validation, and Ed25519 verification.

Protocol specification: draft-litzki-sovp-03
Protocol specification: draft-litzki-sovp-04
https://datatracker.ietf.org/doc/draft-litzki-sovp/
"""

Expand Down
6 changes: 4 additions & 2 deletions sovp/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,10 @@ def generate_identity_document(
Returns:
dict: The complete signed sovp-identity.json document.
"""
domain = urlparse(canonical_url).netloc
public_key_ref = f"dns:txt:_sovp.{domain}"
hostname = urlparse(canonical_url).hostname
if not hostname:
raise ValueError("canonical_url must contain a valid host")
public_key_ref = f"dns:txt:_sovp.{hostname.rstrip('.').lower()}"
created = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
resolved_nonce = nonce if nonce is not None else str(uuid.uuid4())
resolved_expires_at = expires_at
Expand Down
Loading
Loading