Skip to main content

sovp-python

sovp-python is the reference implementation of the Sovereign Validation Protocol (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, run pip install sovp. This exposes the sovp.core and sovp.resolver Python APIs and the sovp CLI. For development, clone the repo and run pip install -e . instead.

Protocol specification: draft-litzki-sovp — IETF Internet-Draft

PyPI CI License Python IETF Draft Status


What SOVP does

SOVP answers one question deterministically: does this data originate from the declared entity, and has it been tampered with?

It does not validate semantic accuracy or truthfulness. It validates identity and integrity only.

Psi_core = Verify(K_pub, sigma, JCS(M))

Psi_core = 1 — source verified, proceed with ingestion.
Psi_core = 0 — verification failed, ingestion blocked.

Scope of this reference implementation: This library provides Ed25519 signing and verification primitives for SOVP identity documents, plus automatic DNS TXT key resolution and HTTP retrieval of sovp-identity.json via sovp.resolver (resolve_dns_pubkeys(), resolve_dns_pubkey(), fetch_identity_document(), validate_domain()). Mode B gateway behavior — an active, caching relay rather than a one-shot check — is implementation-defined and not included in this package.


Protocol scope

SOVP defines a narrow cryptographic validation boundary at Layer 0. The core verification function is:

Psi_core = Verify(K_pub, sigma, JCS(M))

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.


How is this different from DANE or DIDs?

SOVP DANE W3C DIDs
Target Agentic / LLM ingestion pipelines TLS certificate validation Decentralized identity
Layer Layer 0 (pre-ingestion) Transport layer Application layer
Trigger Before body parsing TLS handshake On-demand resolution
DNS anchor _sovp TXT record _dane TLSA record DID document

Installation

pip install sovp

For development, clone the repo instead:

git clone https://github.com/litzki-systems/sovp-python.git
cd sovp-python
pip install -e .

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


End-to-end example

The fastest way to see SOVP in action — generate a keypair, sign an identity document, verify it, and watch tamper detection fire:

from sovp.core import generate_keypair, generate_identity_document, verify_identity

# 1. Generate keys — publish public_key_b64 in your DNS TXT record
private_key_b64, public_key_b64 = generate_keypair()

# 2. Build and sign — serve this JSON at /.well-known/sovp-identity.json
document = generate_identity_document(
    private_key_b64=private_key_b64,
    entity_uid="urn:sovp:example-entity",
    canonical_url="https://example.com",
)

# 3. Verify (Psi_core)
signature = document["integrity_proof"]["signature"]
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
document["entity"]["canonical_url"] = "https://attacker.com"
psi_core = verify_identity(document, signature, public_key_b64)
print("Psi_core =", 1 if psi_core else 0)   # → 0  (blocked)

A runnable version with annotated output is in examples/end_to_end.py:

python examples/end_to_end.py

Usage

Python API

Generate a key pair

from sovp.core import generate_keypair

private_key_b64, public_key_b64 = generate_keypair()
print(public_key_b64)   # publish this in your DNS TXT record
print(private_key_b64)  # keep this secret

Sign an identity payload

from sovp.core import generate_identity_document
import json

# Schema v2.0 (current default): created/nonce/expiresAt live in a signed
# "freshness" object, not in the unsigned integrity_proof. Use this helper
# rather than assembling the document by hand — see "Low-level signing
# primitive" below for what it does internally.
signed_payload = generate_identity_document(
    private_key_b64,
    entity_uid="urn:sovp:your-entity-id",
    canonical_url="https://yourdomain.com",
)

with open("sovp-identity.json", "w") as f:
    json.dump(signed_payload, f, indent=2)

Low-level signing primitive

generate_identity_document() above is built on sign_identity(), the raw Ed25519-over-JCS primitive with no schema opinion of its own — shown here for anyone assembling a document shape by hand:

from sovp.core import sign_identity

# Non-proof fields only — integrity_proof is always excluded from the signed scope
# (draft Section 4 MUST). sign_identity() will strip it automatically if present.
metadata = {
    "@context": "https://litzki-systems.com/protocol/v2.0",
    "@type": "SovereignIdentity",
    "entity": {
        "uid": "urn:sovp:your-entity-id",
        "canonical_url": "https://yourdomain.com",
        "verification_method": "Ed25519"
    },
    "freshness": {
        "created": "2026-01-01T00:00:00Z",
        "expiresAt": "2026-04-01T00:00:00Z"
    }
}

signature = sign_identity(private_key_b64, metadata)

signed_payload = {
    **metadata,
    "integrity_proof": {
        "signature": signature,
        "public_key_ref": "dns:txt:_sovp.yourdomain.tld"
    }
}

sign_identity() always strips integrity_proof before canonicalizing, so callers may pass the full document or only the non-proof fields — the result is identical. The signature cannot cover itself.

Verify (Psi_core)

from sovp.core import verify_identity

# Pass the full document or the non-proof subset — both work identically.
# check_timestamp=True enforces the issuance window of Draft 04 Section 9.4
# (default W = 600 s) against freshness.created. Leave it off for a statically
# published .well-known document: freshness.expiresAt is checked either way.
psi_core = verify_identity(signed_payload, signature, public_key_b64)

if psi_core:
    print("Psi_core = 1: Verified.")
else:
    print("Psi_core = 0: Ingestion blocked.")

CLI

sovp generate-keypair
sovp sign   --payload test_payload.json --privkey <base64-private-key>
sovp verify --payload test_payload.json --sig <base64-signature> --pubkey <base64-public-key>

Technical foundation

Primitive Specification
Signatures Ed25519 (RFC 8032)
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
Freshness Signed freshness.created and freshness.expiresAt, RFC 3339 UTC with Z and no fractional seconds; expiry checked without allowance; the 600 s issuance window (60 s skew toward the future only) applies when timestamp checking is enabled

The sovp-identity.json schema

{
  "@context": "https://litzki-systems.com/protocol/v2.0",
  "@type": "SovereignIdentity",
  "entity": {
    "uid": "urn:sovp:your-entity-id",
    "canonical_url": "https://yourdomain.com",
    "verification_method": "Ed25519"
  },
  "freshness": {
    "created": "2026-03-19T10:00:00Z",
    "nonce": "optional-unique-string",
    "expiresAt": "2026-06-17T10:00:00Z"
  },
  "integrity_proof": {
    "signature": "<Ed25519 signature, base64>",
    "public_key_ref": "dns:txt:_sovp.yourdomain.tld"
  },
  "contentAddress": {
    "alg": "sha256",
    "digest": "<hex-encoded SHA-256 over JCS-canonical bytes of the non-proof fields>"
  }
}

freshness (schema v2.0): created, nonce, and expiresAt live in this object, which IS part of the signed scope — unlike schema v1.4, where these three fields sat inside integrity_proof and were therefore forgeable without invalidating the signature (integrity_proof itself is excluded from the signed scope). A v1.4 document's expiresAt in particular could be pushed arbitrarily into the future by anyone able to modify the served file, without breaking the signature. verify_identity() reads freshness.created first and falls back to the unsigned integrity_proof.created only for documents that predate this change.

contentAddress (optional, draft 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.

Serve this file at https://yourdomain.com/.well-known/sovp-identity.json.


DNS setup

_sovp.yourdomain.tld  IN  TXT  "v=SOVP1; k=<your-Ed25519-public-key-base64>"

Recommended TTL: 300 seconds (per draft Section 9.3). DNSSEC recommended for the _sovp zone.

Automatic DNS resolution is implemented (added in 1.0.2, shipped in the installable wheel since 1.0.4). sovp.resolver.resolve_dns_pubkeys() reads all v=SOVP1 TXT records at _sovp.<host> (up to four, so a key can be rotated by publishing old and new key side by side), and validate_domain() uses that key set; resolve_dns_pubkey() returns the first matching key for callers that need one. Records that do not match the exact v=SOVP1; k=<base64> shape or do not decode to a 32-byte Ed25519 key are ignored. See "Live validation example" below.

The sovp.core functions (verify_identity(), verify_identity_detail()) do no network I/O by design: they take the public key as an argument. Use sovp.resolver when the key should come from DNS.


Live validation example

To validate a live production domain over the installed API (requires DNS + network access, nothing to clone):

from sovp.resolver import validate_domain

result = validate_domain("litzki-systems.com")
print(result["psi_core"], result["outcome"], result["document"]["entity"]["uid"])

Output:

1 verified urn:sovp:litzki-systems-llc

validate_domain() runs the checks of draft Section 6.2 in its order: retrieval of /.well-known/sovp-identity.json (fallback to the root path only on HTTP 404, redirects not followed, size, depth and duplicate-member limits), supported @context, @type and verification_method, host binding, resolution of the _sovp DNS key set, Ed25519 verification against each published candidate key, and the v2.0 freshness rules. A document from an IP literal is refused before any retrieval.

outcome gives the result under the outcome names of draft Section 6.2, Table 3. reason names the same result in more detail and is kept for backward compatibility:

reason outcome (Draft 04)
ok verified
document_invalid, freshness_missing, freshness_invalid document_invalid
host_mismatch host_mismatch
no_key key_unresolved
signature invalid_signature
created_in_future, expired, issuance_window stale

core.DRAFT_OUTCOMES holds the same mapping. Retrieval failures (evidence_missing) and DNS failures (key_unresolved) raise SOVPResolverError.

Issuance window. validate_domain() does not apply the issuance window of draft Section 9.4 by default. A statically published .well-known document is re-signed on its validity cycle, not per request, so it cannot satisfy a 600-second window; Section 9.4 permits a longer W by local policy. Pass check_timestamp=True when you verify a document that was produced in response to a challenge.

A longer annotated walkthrough, including key generation and tamper detection, is in examples/validate_live.py and examples/end_to_end.py in the repository. Those files are not part of the PyPI distribution — clone the repo to run them.


Roadmap

Feature Status
sovp.core primitives (generate_keypair, sign_identity, verify_identity) Implemented
sovp.core document builder (generate_identity_document) Implemented
CLI (generate-keypair, sign, verify) Implemented
Replay protection — timestamp validation (check_timestamp=True) Implemented
contentAddress digest (SHA-256 over JCS bytes) Implemented
Automatic DNS key resolution (_sovp TXT, key-set rotation) and HTTP document retrieval Implemented — see sovp.resolver
RFC conformance test vectors Implemented — see tests/test_vectors.py
Live validation (validate_domain()) Implemented — see sovp.resolver
AgenTrust Marketplace integration Implemented — see sovp-agentrust-bridge
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 (object-oriented wrapper over the existing functions) Planned
IETF Internet-Draft draft-litzki-sovp — active
ARD trustManifest type registration Proposed — open issue ards-project/ard-spec #41
U.S. Provisional Patent Filed — No. 64/005,737

Contributing

See CONTRIBUTING.md.


License

Apache 2.0 — see LICENSE.

The SOVP protocol specification is intellectual property of Litzki Systems LLC. Patent pending.


Litzki Systems LLC · St. Petersburg, FL · info@litzki-systems.com

Metadata

Release files for sovp 1.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sovp 1.1.2
File Size Uploaded Provenance
sovp-1.1.2.tar.gz 39.8 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sovp 1.1.2
File Interpreter ABI Platform
sovp-1.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 63.9 kB

Release files / sovp-1.1.2.tar.gz

Download URL sovp-1.1.2.tar.gz
Size 39.8 kB
Tags Source
SHA-256 checksum
How to use checksums
b9c5d62bec6aa8a518ecb72a64b52f8265ceeaaebfd9685f0024958af9ea2589
BLAKE2b-256 checksum
How to use checksums
7ca371102f676fa848297300c67cc173db693cea208db1948e99bfa94126b525
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / sovp-1.1.2-py3-none-any.whl

Download URL sovp-1.1.2-py3-none-any.whl
Size 24.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ce1e862b043a2f1cfb7a89f66b4fc40ea758f87e94556271413645aa07e724a6
BLAKE2b-256 checksum
How to use checksums
1840ce2c2935da89fbeb8bc6724f59b418fb2d157a45ae2e39242b76204062f6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Security for sovp 1.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page