| Internet-Draft | SOVP | October 2026 |
| Litzki | Expires 10 April 2027 | [Page] |
This document specifies the Sovereign Validation Protocol (SOVP), a protocol for checking, before ingestion, that a signed identity document was produced by the party that controls the Ed25519 key published in DNS for a host. A consumer retrieves a JSON document from a well-known location on the host, canonicalizes it, and verifies its signature against the key published in a DNS TXT record. The protocol defines the signed scope, the retrieval and key resolution rules, freshness checks, resource limits for unverified input, and an optional extension that binds a document to a deployment instance. It does not establish legal identity, the accuracy of content, or the configuration of the system that serves the document.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 10 April 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
Autonomous agents and other automated consumers ingest data from sources they cannot inspect. SOVP lets a source publish a signed identity document together with a DNS-published public key, so that a consumer can check, at the point where data enters its system, that the document was produced by the party that controls the key published under the source's host name.¶
This document defines the signed document, its canonicalization and signature check, the rules for retrieving the document and resolving the key, freshness checks, resource limits for unverified input, and an optional extension that binds a document to a deployment instance. It does not define how a consumer learns which host to check for a given request. It does not establish legal identity, content accuracy, or the hardening state of a system (see Section 3).¶
This document is Experimental. The experiment is to gather implementation and deployment experience with host-anchored identity documents and with the optional instance extension in Section 8. The extension is a candidate for removal or revision if that experience shows it is not useful.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
SOVP attributes a signed document to the holder of the key published for a host, and is designed to prove no more than that. It establishes that the document was produced by whoever controlled the private key corresponding to a K_pub published in DNS for the retrieval host (see Section 9.5 in Section 9 for the precise scope, including what it does not establish about legal or real-world identity). SOVP does not validate the semantic accuracy or the truthfulness of the content provided by the entity. It does not establish the security posture, configuration, or hardening of the host that serves the document, and it does not authenticate the sender of a request that names a host. It does not replace downstream fact-checking or qualitative analysis.¶
The core of the validation is the signature verification function, denoted as Psi_core. It returns a binary result indicating whether the digital signature over the declared entity identity's output is valid. To ensure global interoperability, the identity metadata (M), the signed scope defined in Section 5, MUST be processed using the JSON Canonicalization Scheme (JCS) as defined in [RFC8785].¶
The validation follows the formal function of the Edwards-curve Digital Signature Algorithm, Ed25519, in its pure mode per [RFC8032], including the range check on the signature component S in Section 5.1.7 of that document. Implementations MUST NOT apply any external hash function to JCS(M) before passing it to Verify; Ed25519 pure mode consumes the canonicalized payload directly.¶
Psi_core = Verify(K_pub, sigma, JCS(M))¶
Where:¶
The implementation relies on a signed JSON object retrieved over HTTPS (the "https" scheme of [RFC9110]) from a well-known location on the host, per [RFC8615]. The document is JSON [RFC8259]; a verifier need not perform JSON-LD processing. The primary retrieval path is /.well-known/sovp-identity.json; if that path returns HTTP status 404, a Validating Agent or Gateway MUST fall back to /sovp-identity.json at the host's root, as defined normatively in Section 6.1. This object serves as the primary data carrier for the sovereign identity.¶
Schema (illustrative values; a complete, verifiable document is given in Appendix A):¶
{
"@context": "https://litzki-systems.com/protocol/v2.0",
"@type": "SovereignIdentity",
"entity": {
"uid": "urn:sovp:example-entity",
"canonical_url": "https://example.org",
"verification_method": "Ed25519"
},
"freshness": {
"created": "2026-06-01T12:00:00Z",
"nonce": "example-nonce-0001",
"expiresAt": "2026-12-01T00:00:00Z"
},
"integrity_proof": {
"signature": "<standard Base64 of the 64-octet signature>",
"public_key_ref": "dns:txt:_sovp.example.org"
},
"contentAddress": {
"alg": "sha256",
"digest": "<hex SHA-256 digest>"
}
}
¶
| Member | Requirement | Meaning |
|---|---|---|
| @context | REQUIRED | Opaque schema identifier that is not dereferenced. For this revision: https://litzki-systems.com/protocol/v2.0. It versions the document schema; the v=SOVP1 token of the DNS record versions the key record format, independently. |
| @type | REQUIRED | The string "SovereignIdentity". |
| entity.uid | REQUIRED | Opaque identifier chosen by the signer. This document assigns it no structure and verifiers MUST NOT parse it. The urn:sovp: form used in examples does not denote a registered URN namespace. |
| entity.canonical_url | REQUIRED | URL of the publishing entity. Its host MUST equal the host from which the document was retrieved (see Section 6.2). A verifier MUST NOT use it to select the DNS name to query. |
| entity.verification_method | REQUIRED | The string "Ed25519". |
| entity.owner | OPTIONAL | Self-declared display name (see Section 10). |
| freshness | REQUIRED | Object with created, expiresAt (both REQUIRED) and nonce (OPTIONAL), covered by the signature (see Section 9.4). Timestamps use the RFC 3339 date-time format [RFC3339] in UTC with a literal Z suffix and no fractional seconds. |
| integrity_proof.signature | REQUIRED | Ed25519 signature over JCS(M), encoded as standard Base64 (Section 4 of [RFC4648], with padding) of the 64-octet signature value. |
| integrity_proof.public_key_ref | OPTIONAL | Informational pointer to the DNS name of the key. A verifier MUST NOT use it to select the key. |
| contentAddress | OPTIONAL | See Section 5.1. |
| scan | OPTIONAL | Non-normative vendor extension, outside the signed scope. |
| instance | OPTIONAL | Deployment instance binding (see Section 8). |
The signed scope is determined by an algorithmic reduction of the received document, not by an open-ended textual exclusion. Implementations (both signer and verifier) MUST compute it as follows:¶
This is a fixed three-member exclusion (negative) list, not a positive allow-list of permitted fields: { integrity_proof, contentAddress, scan } are the only top-level keys ever removed before canonicalization. integrity_proof is excluded because a signature cannot cover itself; contentAddress is excluded because it is computed after signing, over this same reduced byte range (see Section 5.1 below); scan is excluded because it is a non-normative vendor extension object carrying scanner output, not part of the identity claim. As of schema v2.0, freshness is part of the signed scope (it is not one of the three excluded keys); under schema v1.4, the equivalent created/nonce/expiresAt fields lived inside integrity_proof and were therefore not cryptographically bound to the signature.¶
A verifier that encounters any top-level field not named integrity_proof, contentAddress, or scan MUST include it in the object it canonicalizes, exactly as received, even if the field name is unfamiliar to that verifier. Only the literal three-member exclusion set applies, not a positive list of permitted field names. A signer that adds a new top-level field outside these three names must therefore expect every verifier to include that field in JCS(M), and a verifier that applies the exclusion rule correctly canonicalizes and verifies such a document without knowing the field in advance.¶
contentAddress, when present, is an object with the fields alg and digest. digest is the SHA-256 hash, encoded as lowercase hexadecimal, of exactly the same byte sequence that forms JCS(M), the signed scope defined in Section 5.2 below, i.e. it is computed by applying the identical three-key exclusion algorithm (removing integrity_proof, contentAddress, and scan, and nothing else) and the identical JCS canonicalization step, then hashing the result. It is computed after signing and is itself unsigned. Its purpose is to let an external consumer (for example, a catalog or index that references this document by its content hash) pin and verify the exact content of the identity claim without re-running Ed25519 verification; such a consumer recomputes the digest over the received document, using the same exclusion algorithm, and compares it to the referenced value.¶
contentAddress is unauthenticated. It is not covered by the Ed25519 signature (in this design it is computed after signing and is not part of the signed scope) and an attacker who can modify or replace the served document can freely remove, alter, or fabricate a contentAddress object without invalidating Psi_core, because contentAddress is one of the three keys stripped before signature verification. contentAddress therefore provides content-identity (consistency/indexing) verification only. It MUST NOT be treated as evidence of authenticity, integrity against tampering, or non-repudiation; those guarantees are established exclusively by a successful Psi_core verification of integrity_proof.signature. A Validating Agent or Sovereign Gateway MUST NOT base any trust decision, including partial or conditional acceptance of a document, on the presence, absence, or matching of contentAddress.¶
Implementations MUST canonicalize and sign only the fields inside the signed scope defined above; canonicalizing the full document including integrity_proof would make signing self-referential and is invalid. The exclusion set is exactly { integrity_proof, contentAddress, scan }; no other field is ever excluded on the basis of being an "extension" or being unrecognized. Verifiers MUST apply the identical three-key exclusion set before recomputing JCS(M) against the received signature; a verifier that excludes a field beyond this set, or fails to exclude one of these three, will report some or all genuinely valid documents as invalid.¶
SOVP defines two operational modes:¶
| Mode | Actor | Description |
|---|---|---|
| Mode A | Validating Agent (Client) | The agent verifies the document of a named host locally before committing data to memory. |
| Mode B | Sovereign Gateway (Server) | An infrastructure-level gateway verifies, on behalf of a protected cluster, the document of the host that a request names or claims to originate from. |
In both modes the verifier evaluates a host and not a requester. SOVP documents are public, so presenting or knowing a document proves nothing about the sender of a request. How a gateway determines the host to check is outside the scope of this document.¶
Execution sequence:¶
Retrieval path fallback: the verifier falls back from /.well-known/sovp-identity.json to /sovp-identity.json if and only if the well-known path returned HTTP status 404 (Not Found). Any other outcome on the well-known path (a 5xx server error, a connection timeout, a TLS handshake failure, or a connection reset) MUST cause the verifier to fail the verification for that request outright; it MUST NOT fall back to the root path in these cases. This restriction exists because an attacker who can selectively disrupt one retrieval path (for example, by causing 5xx responses or timeouts on /.well-known/ while serving a forged document at the root path) would otherwise be able to force a verifier onto a fallback path of the attacker's choosing.¶
If the host does not support SOVP, provides no sovp-identity.json at either retrieval path (the well-known path returns 404 and the root path returns no document), the outcome is evidence_missing. A Sovereign Gateway then acts as follows, as it does for every other outcome except verified and accepted:¶
A verifier applies the following steps in this order and returns the outcome of the first step that fails. Steps marked (B) apply to a verifier that claims instance binding conformance (see Section 8); other verifiers skip them. The retrieval host is the host component of the URL from which the document was retrieved, in lowercase A-label form [RFC5890] without port or trailing dot. A verifier MUST NOT verify a document retrieved from a host given as an IP literal.¶
| Outcome | Meaning |
|---|---|
| verified | Signed by the current holder of a key published for the retrieval host, names that host, and is fresh. This is attribution only. |
| accepted | As verified, and in addition the key is authorized through an external path and the document is bound to the expected instance. |
| evidence_missing | No document was obtained. |
| document_invalid | Limits exceeded, not parseable, unsupported version, type or method, or malformed or inconsistent freshness members. |
| authorization_undetermined | No authorization path is available or configured. |
| host_mismatch | The document names a different host than the one it was retrieved from. |
| key_unresolved | The key set could not be resolved or is empty. |
| invalid_signature | Psi_core failed under every key of the set. |
| anchor_untrusted | An authorization path exists and does not authorize the verifying key. |
| stale | The issuance window or the expiry check failed. |
| instance_missing | The document has no instance member. |
| instance_mismatch | The document describes a different instance or descriptor than the one under evaluation. |
verified means that the document was produced by the current holder of a key published for the retrieval host. It does not mean that the holder is authorized to attest anything about the host or about an instance (see Section 8.2). The fixed order makes each outcome deterministic for a given input, which allows independent implementations to be compared test case by test case (see Appendix A).¶
A Sovereign Gateway that rejects a request at Layer 0 MUST respond with status code 403 (Forbidden) Section 15.5.4 of [RFC9110]. This applies to every outcome other than verified and accepted. Status code 401 is not used: Section 15.5.2 of [RFC9110] requires a 401 response to carry a WWW-Authenticate challenge, and this document defines no HTTP authentication scheme. Status code 422 is not used either, because [RFC9110] ties it to a well-formed request that is semantically erroneous at the application layer, which does not describe a Layer 0 credential failure. A gateway MAY include a problem details object [RFC9457] that distinguishes the outcomes; this document defines no problem types.¶
The public key is published as a TXT resource record at the node name _sovp.<domain> (see Section 11.1), where <domain> is defined below. The record value MUST be formatted as:¶
v=SOVP1; k=<base64>¶
k is the 32-byte raw Ed25519 public key, encoded using standard Base64 (not Base64url) as defined in Section 4 of [RFC4648], with no line breaks. The literal v=SOVP1 prefix identifies the record as a SOVP version-1 key record and MUST be matched exactly (case-sensitive) by a conformant verifier; the semicolon-separated k= parameter carries the key value.¶
Multiple _sovp TXT records: a zone MAY publish more than one _sovp TXT record to support key rotation. A verifier collects the records whose complete value begins with v=SOVP1 and parses each as specified above. A record that cannot be parsed, or whose k value is not the strict Base64 encoding of exactly 32 octets, is ignored. The remaining keys form an unordered set, and the order in which a resolver returns records has no meaning. A verifier MUST consider at least 4 keys and MAY consider more; a zone SHOULD NOT publish more than 4 matching records, because keys beyond a verifier's limit are ignored in an unspecified order. If DNS resolution fails (including NXDOMAIN, SERVFAIL, a timeout, and a DNSSEC validation failure) or the set is empty, verification MUST fail with the outcome key_unresolved.¶
Multiple character-strings within a single TXT RR: [RFC1035] allows a single TXT RDATA to contain more than one <character-string> (each limited to 255 octets). A verifier MUST concatenate all character-strings of a record, in order and without separators, before parsing the record. The complete v=SOVP1; k=<base64> record is well under 255 octets (a Base64-encoded 32-byte key is 44 characters), so a signer SHOULD publish it as a single character-string.¶
DNS name derivation: the DNS name to query is formed by prepending the label _sovp. to the retrieval host, which is the host component of the URL from which the document was retrieved (see Section 6.2). The resolver's normal answer to the TXT query is used, including the result of following a CNAME at that name. No subdomain tolerance applies: a document retrieved from foo.example.com MUST be verified against _sovp.foo.example.com, and a verifier MUST NOT fall back to querying _sovp.example.com or any other ancestor domain if the exact-match query yields no record. This exact-match rule prevents an attacker who controls a subdomain from having their key implicitly trusted via a parent domain's DNS record, or vice versa.¶
A SOVP document as defined above names a host and a key. It does not state which deployment instance the claim refers to. This section defines an OPTIONAL top-level member, instance, that carries a signed label for one deployment instance, and the additional steps of Section 6.2 that a verifier claiming instance binding conformance applies. Documents without an instance member are verified as specified in the preceding sections; a verifier claiming instance binding conformance returns instance_missing for them.¶
The label is an assertion by the key holder. It does not prove that the process serving the document is that instance, or that the instance exists or is still running. It is meaningful where a host serves a single instance, or where the verifier selects the host to retrieve from using its own record of the instance.¶
The extension does not change the schema major version. instance is not one of the three keys excluded from the signed scope (see Section 5), so it is covered by the Ed25519 signature without any change to the exclusion rule. A verifier that does not implement this section still canonicalizes and verifies such a document correctly, but derives no instance label from it.¶
"instance": {
"id": "0192f5c4-7e1a-7c3b-9d2e-5a4b6c7d8e9f",
"descriptor_digest": {
"alg": "sha256",
"digest": "<lowercase hex SHA-256 digest>"
}
}
¶
The verifier obtains the expected instance identifier, and the descriptor if it holds one, from its own context, for example from the manifest or deployment record it is evaluating. It MUST NOT take an expected value from the document under test. The outcome is instance_mismatch if instance.id differs from the expected identifier, or if the verifier holds a descriptor and the document either lacks descriptor_digest or carries a digest that differs from the one computed over that descriptor.¶
A SOVP identity document, with or without the instance member, is a signed claim about who controls a key for a host and, with this extension, which deployment instance the claim refers to. Its signed scope contains no measurement of the execution environment. Scanner output carried in the scan member lies outside the signed scope (see Section 5) and receives no assurance from Psi_core. Assessment results such as verdicts or scores require a separate signed document, issued under a key that the verifier resolves independently (see Section 8.2); the format of such a document is out of scope here.¶
A SOVP document makes no claim about permitted actions, runtime behavior, or the content of an Agent Manifest. A consumer that composes SOVP documents with Agent Manifest or TRACE records MUST treat each as a separate claim, MUST join them only through the declared instance identifier, and MUST NOT infer the validity of one layer from the validity of another.¶
This section follows the guidelines of [RFC3552].¶
SOVP considers an attacker who can observe and modify network traffic to a verifier, who can publish content on hosts and DNS zones the attacker controls, and who can copy any public SOVP document. The protocol detects modification of a document after signing, replacement of a document by a party that holds no key published for the retrieval host, and presentation of a document under a host other than the one it names. It does not protect against compromise of the signing key, of the zone that publishes the key, or of the host that serves the document.¶
Compared with relying on the TLS session alone, a signature checked against a DNS-published key can be re-verified from a stored copy and does not depend on the TLS terminator or a content delivery network. Related designs are DKIM [RFC6376], which publishes signing keys in TXT records under an underscored label, DANE [RFC6698], which binds keys to TLS endpoints through DNS, and HTTP Message Signatures [RFC9421], which sign individual messages. SOVP signs a standing document and not a message.¶
A SOVP document is public. A party that copies it can present it, and verification of that document says nothing about who sent a request. A Sovereign Gateway therefore authenticates a host's document and not the sender. Deployments that need to tie a request to a host need another mechanism, for example HTTP Message Signatures [RFC9421] made with a key that is bound to the host. This document defines no such binding.¶
To minimize the window of vulnerability during a key compromise, SOVP records in DNS SHOULD use a low Time-To-Live (TTL), with a recommended value of 300 seconds. Revocation is achieved by updating or removing the _sovp TXT record. Because a signature is accepted if it verifies under any key in the published set (see Section 7), a compromised key remains acceptable to a verifier until its record has expired from that verifier's resolver caches, and a rotation is complete only after the record of the retired key has been removed. A verifier that caches documents or keys extends this window by the cache lifetime and SHOULD NOT cache beyond the DNS TTL of the key record.¶
Freshness uses the freshness object (created, nonce, expiresAt), which is part of the Ed25519-signed scope (see Section 5.2). Because freshness is covered by the signature, none of its fields can be altered, including being pushed further into the future, without invalidating Psi_core. A verifier applies the following checks, in order, to every document whose signature has already passed Psi_core verification:¶
The issuance window and the expiry check serve different purposes. The window bounds how long ago the document was issued and is evaluated against created only. A signer that serves a pre-signed document therefore needs to re-sign it at an interval shorter than W for the document to pass a verifier that uses the default window. This keeps the signing key on a system that is online at that interval, and operators should weigh that exposure; a longer W by local policy trades freshness for key isolation. The expiry check bounds how long a document remains valid at all.¶
Replay of a document within W is not prevented: a verifier has no challenge to bind the document to a request, and a copy of a fresh document is as fresh as the original. nonce is intended to support deduplication of high-frequency requests, but a verifier MUST NOT treat the presence of a nonce alone as a deduplication guarantee, because replay detection requires the verifier to remember nonces it has seen, which this document does not specify.¶
For documents whose @context declares a schema version earlier than 2.0, the equivalent created and expiresAt fields were carried in integrity_proof, which lies outside the signed scope; any change to them is undetectable by Psi_core. A verifier that supports such documents MAY apply the checks above as a best-effort heuristic and MUST NOT treat the result as cryptographically guaranteed.¶
SOVP attributes the signed metadata to the party that holds the private key matching a K_pub published for the retrieval host when the verifier resolves it. This is attribution to the current key holder and is not non-repudiation in a legal or historical sense: DNS records can be changed or removed, and a verifier that did not record the key at the time cannot later show who held it. Attribution does not establish the signer's legal or real-world identity (see Section 3): SOVP makes no claim about who controls a domain, only that the party that published K_pub produced the signature. SOVP itself defines no reputation system and makes no claim about whether, or by what mechanism, a domain's signing history could be used by a third party to identify or block that domain in the future.¶
Because the trust anchor is a DNS TXT record, protection against DNS spoofing is critical. DNSSEC [RFC4033] is RECOMMENDED for the zone that hosts the _sovp record. A verifier SHOULD resolve the record through a validating resolver and SHOULD treat a DNSSEC validation failure as a resolution failure. Without DNSSEC, the integrity of the resolved key depends entirely on the DNS resolution path (cache integrity of intermediate resolvers, absence of on-path spoofing, and the transport used to reach the resolver), a path that SOVP does not protect and cannot monitor. Operators who need a trust anchor stronger than that resolution path need DNSSEC; SOVP offers no alternative mechanism.¶
A verifier parses the document before Psi_core is known, so the document is attacker-controlled input at that point. Before canonicalization, a verifier MUST reject a document, with the outcome document_invalid, that is larger than 65536 octets, that contains JSON values nested deeper than 16 levels (the top-level object is at level 1), or that contains a duplicate member name in any object. The size limit applies to the octets of the document after any HTTP content decoding. Duplicate member names are rejected because JSON parsers differ in which value they keep (see [RFC8259]), so a document could be read differently by a verifier and by a consumer of the same bytes. A verifier MUST NOT canonicalize or otherwise process a document before these checks have passed.¶
Retrieving the document and resolving the key consume a verifier's resources on behalf of whoever caused the request. A verifier SHOULD bound the time spent on each retrieval and each DNS query, and SHOULD limit the rate of retrievals per source. A verifier that derives the host to retrieve from untrusted input MUST NOT retrieve from addresses that are not meant to be publicly reachable (for example loopback, link-local, and private ranges), and MUST check the address after name resolution, to avoid server-side request forgery and DNS rebinding.¶
A verifier SHOULD NOT follow HTTP redirects when retrieving the document. A verifier that follows them MUST still derive the DNS name from the host of the original request URL (see Section 7) and MUST NOT follow a redirect to a URL that does not use the https scheme.¶
Each retrieval and each DNS query tells the publisher of the document and the operators of the resolvers that a verifier is interested in a host. A gateway that verifies on behalf of many requesters can reveal request patterns this way. This document defines no mitigation.¶
The DNS TXT record that anchors K_pub lies under the control of the entity that serves the document. Without the external authorization step in Section 8.2, a verifier learns that the document and the key come from one party and learns nothing about whether that party may attest for the instance. An attacker who controls a host can sign any instance identifier for it. Verifiers MUST therefore take the expected instance identifier from their own context, as required in Section 8.1, and MUST NOT treat a matching instance.id alone as evidence of authenticity.¶
Because instance is inside the signed scope, a party that does not hold the signing key cannot retarget a signed document to a different instance without invalidating Psi_core. This protects against retargeting after the fact and does not protect against an issuer that signs a false claim.¶
The information in this section reflects the status of known implementations as of the writing of this document per the requirements in [RFC7942]. The description of implementations in this section is intended to assist the IETF in its decision processes and is not intended to (and MUST NOT be construed to) favor or disfavor any particular implementation. This section will be removed before publication as an RFC.¶
The reference implementations named in this section (sovp-python, sovp-engine, and the Cloudflare Worker deployment) are developed and operated by Litzki Systems LLC, the organization publishing this document; they are not independent third-party implementations. The following capabilities are implemented in the reference implementations (sovp-python's sovp/core.py, sovp-engine's lib/sovereign-identity.mjs) as of schema v2.0:¶
Test coverage: sovp-engine's test/verifyIdentityDocument.test.js covers the production verifyIdentityDocument() path with 12 tests: genuine v2.0 acceptance, per-field tamper rejection (freshness.expiresAt, freshness.created, entity.owner, entity.canonical_url, freshness.nonce, wrong public key), downgrade resistance (freshness stripped, with and without a forged v1.4 @context), upgrade resistance (a genuine v1.4 document with @context and a fabricated freshness object forged to v2.0), and legacy v1.4 fallback correctness (including the known-and-accepted legacy expiresAt-forgery gap). test/canonicalizationDrift.test.js additionally verifies byte-for-byte JCS ([RFC8785]) canonicalization agreement across all three separate implementations maintained by Litzki Systems LLC (sovp-engine's npm canonicalize, sovp-python's jcs package, and the Cloudflare Worker's hand-rolled jcs()) for the real SovereignIdentity document shape, including non-ASCII field values and unsorted source key order.¶
The reference implementation sovp-python is published at https://github.com/litzki-systems/sovp-python under the Apache License 2.0; the other implementations are maintained by the same organization. Contact: ietf@litzki-systems.com. sovp-python implements the resource limits of Section 9.8 (sovp/document_safety.py) and the key set of Section 7 (up to 4 keys, any of which may verify).¶
Known limitations: nonce-reuse detection (deduplication of freshness.nonce across requests) is not yet implemented in either reference implementation (see Section 9.4). In sovp-python, the issuance window is applied only when the caller requests it (check_timestamp), verify_identity does not check expiresAt, and generate_identity_document emits expiresAt only when the caller supplies it, whereas this document requires expiresAt to be present and checked. The host binding of Section 6.2 (step 3) is not implemented in sovp-python.¶
Deployment instance binding (Section 8), including the authorization and outcome steps of Section 6.2 that are marked (B), is specified in this revision and is not yet implemented in any reference implementation. Appendix A gives test vectors for each outcome. The vector in A.1 is taken from the sovp-python test suite. The vectors in A.2 and A.3 were generated for this revision with the same published test key and checked with a separate script that follows the verification order of this document, not with a reference implementation. Until a reference implementation passes them, no implementation claims instance binding conformance.¶
Per [RFC8552], IANA is requested to add the following entry to the Section 11.1 registry:¶
| RR Type | _NODE NAME | Reference |
|---|---|---|
| TXT | _sovp | This document |
All values in this appendix use the test key below and the host example.org. The test private key is published for testing only and MUST NOT be used for any other purpose. Ed25519 signatures in pure mode are deterministic, so an implementation can reproduce every signature from the seed. Long lines are folded as described in [RFC8792] (a single backslash at the end of a line, with the continuation at the start of the next line).¶
Test private key (32-octet seed, standard Base64): /Tp1tRJ/wlUUHYDsO7C1rR0XAsAg4hHmLC0xQKkdmHQ= Public key as published in DNS at _sovp.example.org: v=SOVP1; k=Roc8V4AnfbRjW35aagwwuHJhc7hPQZl7ooncY+HdM+s=¶
Document A.1:¶
NOTE: '\' line wrapping per RFC 8792
{
"@context": "https://litzki-systems.com/protocol/v2.0",
"@type": "SovereignIdentity",
"entity": {
"uid": "urn:sovp:test-entity-2",
"canonical_url": "https://example.org",
"verification_method": "Ed25519"
},
"freshness": {
"created": "2026-06-01T12:00:00Z",
"nonce": "fixed-test-nonce-v2-0001",
"expiresAt": "2026-12-01T00:00:00Z"
},
"contentAddress": {
"alg": "sha256",
"digest": "d00c6db5124803dd67495b0551dc0249994fd6f818509724202\
6e28a4d30b460"
},
"integrity_proof": {
"signature": "Th2wr122ikRicemrC5bITXHri5glqNqfkPosCjLBjXLIdO2U\
StZm5j7SmJ0j1xuEYAPOc9WtJW4blsceuCVCBg==",
"public_key_ref": "dns:txt:_sovp.example.org"
}
}
¶
JCS(M), the canonicalized signed scope (312 octets), whose SHA-256 digest equals contentAddress.digest above:¶
NOTE: '\' line wrapping per RFC 8792
{"@context":"https://litzki-systems.com/protocol/v2.0","@type":"\
SovereignIdentity","entity":{"canonical_url":"https://example.or\
g","uid":"urn:sovp:test-entity-2","verification_method":"Ed25519\
"},"freshness":{"created":"2026-06-01T12:00:00Z","expiresAt":"20\
26-12-01T00:00:00Z","nonce":"fixed-test-nonce-v2-0001"}}
¶
The same key and entity with an instance member. The descriptor is the following JSON text, which is already in JCS form, and descriptor_digest.digest is the SHA-256 digest of its UTF-8 octets:¶
NOTE: '\' line wrapping per RFC 8792
{"instanceId":"0192f5c4-7e1a-7c3b-9d2e-5a4b6c7d8e9f","url":"http\
s://example.org/agent"}
¶
Document A.2:¶
NOTE: '\' line wrapping per RFC 8792
{
"@context": "https://litzki-systems.com/protocol/v2.0",
"@type": "SovereignIdentity",
"entity": {
"uid": "urn:sovp:test-entity-2",
"canonical_url": "https://example.org",
"verification_method": "Ed25519"
},
"freshness": {
"created": "2026-06-01T12:00:00Z",
"nonce": "fixed-test-nonce-v2-0001",
"expiresAt": "2026-12-01T00:00:00Z"
},
"instance": {
"id": "0192f5c4-7e1a-7c3b-9d2e-5a4b6c7d8e9f",
"descriptor_digest": {
"alg": "sha256",
"digest": "d9714591fa2a1ef5b07331bd3437d4f5a79f9321b26009293\
6decf8fbab3848d"
}
},
"contentAddress": {
"alg": "sha256",
"digest": "7ddd289e0e3dd81df253a9fdbeb18760aa94ae87286c6eca676\
eeb11a7619140"
},
"integrity_proof": {
"signature": "+ey/Rr3G/Qeu+VSsyA8fPI66qSy+V0T6YBxJsoxtcEK77YoH\
wH2cLd6mZnNROnCYMHgoTpakAIBDoC9mbFY/Dg==",
"public_key_ref": "dns:txt:_sovp.example.org"
}
}
¶
JCS(M) (482 octets):¶
NOTE: '\' line wrapping per RFC 8792
{"@context":"https://litzki-systems.com/protocol/v2.0","@type":"\
SovereignIdentity","entity":{"canonical_url":"https://example.or\
g","uid":"urn:sovp:test-entity-2","verification_method":"Ed25519\
"},"freshness":{"created":"2026-06-01T12:00:00Z","expiresAt":"20\
26-12-01T00:00:00Z","nonce":"fixed-test-nonce-v2-0001"},"instanc\
e":{"descriptor_digest":{"alg":"sha256","digest":"d9714591fa2a1e\
f5b07331bd3437d4f5a79f9321b260092936decf8fbab3848d"},"id":"0192f\
5c4-7e1a-7c3b-9d2e-5a4b6c7d8e9f"}}
¶
Unless a row states otherwise, the verifier context is: retrieval host = example.org; DNS publishes the key of A.1 (K) only; the authorization path authorizes K for (example.org, urn:sovp:test-entity-2); now = 2026-06-01T12:05:00Z; expected instance identifier = 0192f5c4-7e1a-7c3b-9d2e-5a4b6c7d8e9f; expected descriptor digest = d9714591fa2a1ef5b07331bd3437d4f5a79f9321b260092936decf8fbab3848d; the issuance window W is 600 seconds with N = 60 seconds. K' denotes the key RYVkYPO0CymHmHNYfjw3a9baEyWH40fnEzrgY/rMA5M=, which does not correspond to the test key. The verifier claims instance binding conformance.¶
| ID | Input | Expected outcome |
|---|---|---|
| V1 | A.2 document | accepted |
| V2 | No document obtained | evidence_missing |
| V3 | A.1 document (no instance member) | instance_missing |
| V4 | A.2 document; authorized set is {K'} | anchor_untrusted |
| V5 | A.2 document; no authorization path configured | authorization_undetermined |
| V6 | A.2 document with the last character of instance.id changed to 0, signature unchanged | invalid_signature |
| V7 | A.2 document; now = 2026-06-01T13:00:00Z | stale |
| V8 | A.2 document; now = 2026-06-01T11:58:00Z (created lies more than N seconds in the future) | stale |
| V9 | A.2 document; expected instance identifier = 0192f5c4-7e1a-7c3b-9d2e-ffffffffffff | instance_mismatch |
| V10 | A.2 document; expected descriptor digest is the SHA-256 digest of the ASCII string "other descriptor" | instance_mismatch |
| V11 | A.2 document retrieved from host attacker.example, whose DNS publishes K and whose authorized set for (attacker.example, urn:sovp:test-entity-2) is {K} | host_mismatch |
| V12 | A.2 document; DNS publishes {K', K}; authorized set is {K'} | anchor_untrusted |
| V13 | A.2 document; DNS publishes {K', K}; authorized set is {K} | accepted |
| V14 | A.2 document; DNS publishes no valid v=SOVP1 record | key_unresolved |
| V15 | A.2 document text in which the member nonce appears twice in freshness | document_invalid |
| V16 | A.2 document text padded with spaces to 65537 octets | document_invalid |