RFC-ACDP-0001
RFC-ACDP-0001
Agent Context Distribution Protocol (ACDP) — Core
Document: RFC-ACDP-0001 Version: 0.1.0 Status: Community Standards Track (Final) Canonical wire format: JSON over HTTP Required JSON canonicalization: RFC 8785 — JSON Canonicalization Scheme (JCS) Intended status: Stable Core
This is an RFC-style open standard. It is not an IETF RFC.
Abstract
The Agent Context Distribution Protocol (ACDP) lets autonomous AI agents publish, discover, and verify units of contextual information ("contexts") across distributed systems and organizational boundaries.
ACDP introduces one strict invariant:
Once a context body is published, its producer-controlled fields MUST NOT change. The producer-controlled portion of every body MUST be cryptographically signed by its producer, and every lineage MUST be end-to-end verifiable.
The "producer-controlled portion" refers to the fields the producer authors and signs (everything except ctx_id, lineage_id, origin_registry, and created_at, which are registry-assigned at publish time). See §5.7 for the exact exclusion set, and §5.9 for what the producer signature does and does not bind.
ACDP Core does not define discovery semantics, registry policy, retraction rules, attestation schemas, or domain logic. ACDP Core defines structure: the identifier formats (acdp://, lin:), the canonicalization algorithm (JCS), the content-hash and signature semantics, the time format, and the registry hooks the rest of the spec depends on.
1. Status of This Memo
This document is a Final ACDP specification (acdp/0.1.0). It is stable for the 0.1.0 release; subsequent breaking changes require a new RFC and a version bump per VERSIONING.md.
ACDP 0.1.0 is the first published Final version of the protocol; the numbering scheme treats acdp/0.1.0 as the inaugural release. The 0.0.1 identifier was used only for internal pre-release drafts and was never promoted to a Release Candidate or Final status. 0.1.0 is wire-compatible with those drafts — the body format, JCS canonicalization, content-hash, and signature semantics are unchanged.
2. Conventions and Terminology
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
| Term | Definition |
|---|---|
| Agent | A software entity that produces or consumes contexts. Agents have stable identifiers (DIDs, per [DID-CORE]). |
| Context | A unit of agent-produced content described by an ACDP body and tracked by an ACDP registry. |
| Body | The immutable stored object representing a context. Contains producer-controlled fields plus the registry-assigned identity fields (ctx_id, lineage_id, origin_registry, created_at) and the integrity fields (content_hash, signature). All body fields are set at publish time and immutable thereafter. |
| ProducerContent | The signature/hash preimage. The Body with content_hash, signature, and the four registry-assigned identity fields removed (the §5.7 exclusion set). The producer signs ProducerContent; the Body wraps ProducerContent plus the registry-assigned identifiers and the signature. |
| RegistryState | The mutable, registry-derived state returned alongside the Body on retrieval. In v0.1.0 contains only the derived status field. Future ACDP versions add lifecycle events, relationships, and attestations to RegistryState without changing the Body. |
| Registry | A service that accepts, stores, and serves contexts according to this specification. |
| Lineage | A chain of contexts representing successive versions of the same logical work, identified by a stable lineage_id. |
| Producer | An agent that publishes contexts. |
| Consumer | An agent that retrieves and uses contexts. |
The Body contains both producer-controlled and registry-assigned fields; only ProducerContent is covered by the producer signature. See §5.7 for the exact exclusion set and §5.9 for what the producer signature does and does not bind.
3. Scope and Design Goals
ACDP exists to make agent-produced knowledge discoverable, verifiable, and reusable across organizational boundaries. It provides:
- content-addressed, cryptographically-signed bodies;
- a deterministic lineage model for versioning;
- a small set of HTTP-based publish/retrieve operations (RFC-ACDP-0003, RFC-ACDP-0004);
- keyword discovery (RFC-ACDP-0005);
- cross-registry references via the
acdp://URI scheme (RFC-ACDP-0006); - registry capability declaration and a defined error surface (RFC-ACDP-0007);
- a threat model (RFC-ACDP-0008).
ACDP does not define:
- coordination, voting, consensus, or convergent decision-making;
- demand-pull, requests, or fulfillment mechanics;
- payment, settlement, or marketplaces;
- workflow or pipeline declarations;
- reputation algorithms;
- quality scoring by registries;
- audit-grade time anchoring;
- encrypted bodies (use
data_refssplitting and external ACLs); - schema hosting (ACDP only references schemas);
- hard deletion of any kind;
- multi-party or threshold signatures (use
contributors).
See docs/non-goals.md for the full non-goals list and rationale.
4. Architecture
┌────────────────────────────┐ ┌────────────────────────────┐
│ Producer Agent │ │ Consumer Agent │
│ (DID, signing key) │ │ (DID, key resolver) │
└──────────────┬─────────────┘ └──────────────┬─────────────┘
│ POST /contexts (signed body) │
▼ │
┌──────────────────────────┐ │
│ ACDP Registry │ │
│ did:web:reg.example │ ◀──── GET /contexts/{ctx_id}┘
│ /.well-known/acdp.json │ (body + registry_state)
└──────────────────────────┘
│
│ verify producer signature locally,
│ walk derived_from → cross-registry resolves
▼
Other ACDP registriesEach role's responsibilities:
- Producer. Builds a body, computes
content_hashover the JCS-canonicalized body (excluding the fields listed in §5.7 —content_hash,signature, and the registry-assigned identity fields), signs the hash with its DID-bound key, and submits a publish request. - Registry. Recomputes
content_hashover the JCS-canonicalized ProducerContent, then verifies the producer's signature over the recomputed hash. Only after both checks pass does the registry assignctx_id/lineage_id/origin_registry/created_at, validate supersession constraints, persist the body, and derivestatus. See RFC-ACDP-0003 §2.1 for the full ordered step list. - Consumer. Fetches a context, verifies the producer's signature against the producer's DID document, walks
derived_fromreferences via cross-registry resolution.
Verification is stateless and local for the consumer: to check a context, a consumer needs only the producer's public key (resolved from the producer's DID document), the canonicalization algorithm (JCS), and the spec-defined exclusion set for content_hash.
5. Wire Format
5.1 JSON encoding
All ACDP messages on the wire are JSON ([RFC 8259]) objects encoded as UTF-8 ([RFC 3629]). Implementations MUST emit valid UTF-8 and MUST accept any valid UTF-8.
The HTTP Content-Type for ACDP bodies is application/acdp+json. Implementations MAY also accept application/json for compatibility but SHOULD emit application/acdp+json. See registries/media-types.md.
5.2 Canonicalization
Cryptographic hashes over ACDP data structures use JSON Canonicalization Scheme (JCS) [RFC 8785]. Implementations MUST canonicalize using JCS before hashing for any normative cryptographic operation.
Cross-language interoperability of canonicalization is the most common source of ACDP implementation bugs. The conformance fixtures under schemas/conformance/can-*.json define the authoritative test vectors; implementations failing those vectors MUST NOT claim conformance. Numeric serialization is the subtlest part of JCS: can-011-jcs-numeric-vectors.json pins the RFC 8785 §3.2.2.3 (ECMAScript Number-to-String) behavior — the exponential-notation boundaries (magnitude ≥ 1e21 and ≤ 1e-7), the plain-decimal band between them, integer exactness through 2^53, and negative-zero normalization — which a naïve serializer will silently get wrong.
Implementer note — Python json.dumps. Python's stdlib json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False) is JCS-conformant for most input shapes but is not conformant on negative zero: it preserves -0.0 as -0.0 rather than emitting 0 per RFC 8785 §3.2.2.3. Implementations using stdlib will fail can-001-jcs-vector.json's number-formatting vector. Use the jcs package on PyPI (https://pypi.org/project/jcs/), or pre-process input to normalize negative zero before serialization. Similar canonicalization gotchas exist in other languages (e.g., serializers that escape non-ASCII as \uXXXX by default); verify against the conformance fixtures before claiming conformance.
5.3 Time Format
All timestamps in ACDP are RFC 3339 [RFC 3339] date-time strings in UTC with the explicit Z suffix.
The canonical emission form uses millisecond precision:
2026-04-16T10:30:15.123ZTimestamp precision (MUST). Producers MUST truncate every timestamp value in a publish request to millisecond precision before computing content_hash. Because timestamps are part of the JCS-canonicalized body, a timestamp with microsecond or nanosecond precision produces a different canonical string than its millisecond-truncated equivalent — and therefore a different content_hash that no other conformant implementation can reproduce from the same logical timestamp. Library authors MUST apply millisecond truncation automatically in every API surface that accepts a timestamp on the producer path (expires_at, data_period.start, data_period.end, and any data_refs timestamps). Standard-library types in many languages (time.Time in Go, datetime in Python, chrono::DateTime in Rust, Date / Temporal.Instant in JS) default to higher precision and silently emit nanosecond strings unless explicitly truncated; this is the most common source of cross-implementation hash divergence.
Implementations:
- MUST emit timestamps in canonical form when generating new timestamps.
- MUST accept any valid RFC 3339 date-time on input. This includes timestamps with no fractional seconds, microsecond, or nanosecond precision.
- SHOULD normalize accepted timestamps to canonical form on storage.
Producer note. Because timestamps are part of the JCS-canonicalized body, two contexts with timestamps differing only in fractional precision will produce different content_hash values. The canonical millisecond form is mandatory for the producer path; the conformance fixture can-006-timestamp-precision.json pins a nanosecond-precision input and locks in the hash that the exact serialized string produces, making the "hash binds the bytes, not the logical instant" rule explicit.
Registry timestamp emission (MUST). Registries MUST assign created_at using canonical millisecond precision (e.g. 2026-04-16T10:30:15.123Z). Implementations using OS clock APIs that return higher-precision timestamps — time.Now() in Go (nanoseconds), Instant::now() / chrono::Utc::now() in Rust, datetime.datetime.now(tz=UTC) in Python (microseconds), Date.now() returning a millisecond Number but Temporal.Now.instant() returning nanoseconds in JS, Instant.now() in Java (nanoseconds) — MUST truncate to millisecond precision before serializing created_at into both the publish response and the stored body. Truncation MUST round toward the past (floor); rounding to nearest can produce a created_at greater than the registry's wall-clock instant of acceptance, which violates the Time Format invariant. The registry-side requirement is identical to the producer-side rule above; can-007-registry-created-at.json pins it as a separate fixture so registry implementers can verify their emission path independently of any producer payload.
Clock and skew. Registries SHOULD use NTP-synchronized UTC clocks. The registry's clock is authoritative for created_at (registry-assigned) and for the status: expired derivation (RFC-ACDP-0004 §4). Consumers computing expired locally MAY apply a skew tolerance of up to ±60 seconds against expires_at; consumers comparing a registry's created_at against their own local clock SHOULD allow the same tolerance. Producers MUST set expires_at and data_period.{start,end} based on their own UTC clock; small skew between producer and registry is expected and harmless because these fields are signed (not derived).
5.4 Identifier Formats
| Identifier | Form | Spec |
|---|---|---|
ctx_id (context identifier) | acdp://<authority>/<uuid> where <authority> is a DNS hostname identifying the origin registry and <uuid> is a UUID v4 [RFC 9562]. | §5.5 |
lineage_id | lin:<algorithm>:<digest>. v0.1.0 form: lin:sha256:<64-lowercase-hex>. | §5.6 |
agent_id | A Decentralized Identifier [DID-CORE]. v0.1.0 producers MUST use did:web so that any conformant registry can resolve their keys via §5.11. | RFC-ACDP-0002 |
The ctx_id is assigned by the registry at publish time; producers MUST NOT supply a ctx_id in publish requests. The corresponding URI scheme acdp is registered in §11.
DID method scope by field (NORMATIVE)
The did:web requirement of v0.1.0 applies to fields the registry must resolve to a key, not to every DID-typed field on a body. The following table is normative for v0.1.0; the schema's did definition (schemas/json/acdp-common.schema.json#/$defs/did) is intentionally loose so that contributors[] and audience[] can carry other methods today, even while agent_id and signature.key_id are constrained to did:web by this section.
| Field | v0.1.0 requirement | Rationale |
|---|---|---|
agent_id | MUST be did:web. | The signing agent must be resolvable by every conformant registry via the §5.11 algorithm. Schema-loose is by design; the registry enforces the method. |
signature.key_id (DID portion, before #) | MUST be did:web, and MUST equal agent_id. | The §5.11 resolver fetches the DID document for this DID. Mismatch with agent_id is rejected as key_not_authorized (RFC-ACDP-0003 §2.1 step 6). |
contributors[] | SHOULD be did:web; other DID methods are permitted. | Attribution only — v0.1.0 does not resolve contributor keys. A producer crediting a did:key-only collaborator MUST NOT have its publish rejected for that reason. |
audience[] | MAY be any DID method. | Authorization list — registries match the requester's authenticated DID against audience[] as opaque strings (RFC-ACDP-0008 §4.5). No key resolution is performed. |
body.agent_id of an ancestor in derived_from | Same rule as the ancestor's own agent_id (i.e. for v0.1.0 ancestors, did:web). | A consumer following the ancestor link verifies the ancestor's signature, which requires resolving its key via §5.11. |
Registries MUST reject a publish whose agent_id is not did:web with schema_violation (preferred — caught at request validation) or with key_not_authorized if discovered later in the §5.11 pipeline. Registries MUST NOT reject a publish solely because contributors[] includes a non-did:web entry. Conformance fixtures pub-008, pub-009, and pub-010 pin these behaviors.
5.5 Context-ID assignment
A registry assigns ctx_id at publish time as acdp://<own_authority>/<freshly_generated_uuidv4>. The authority component MUST equal the DNS hostname declared in the registry's capabilities document (RFC-ACDP-0007). Two registries MUST NOT share an authority.
5.6 Lineage Identifier Derivation
A context's lineage_id MUST be derived deterministically from the ctx_id of the lineage's first version (the version with supersedes: null):
lineage_id = "lin:sha256:" + lowercase_hex(SHA-256(first_version_ctx_id))The hash input is the UTF-8 encoding of the ctx_id string. The sha256 algorithm prefix is fixed in v0.1.0; future ACDP versions MAY introduce additional algorithms (lin:sha3-256:..., lin:blake3:...) for new lineages without invalidating existing v0.1.0 lin:sha256: identifiers. Consumers MUST NOT compare lineage_ids across different algorithm prefixes.
For first versions, the registry computes lineage_id from the ctx_id it just assigned. For subsequent versions, the registry MUST walk back through supersedes references to find the version 1 context and apply the same formula.
A producer publishing a first version (supersedes: null) MUST NOT include lineage_id in the publish request — the producer cannot know the registry-assigned ctx_id at signing time, so any supplied value would be a guess. Registries MUST reject first-version requests containing lineage_id with schema_violation (RFC-ACDP-0003 §2.2).
A producer publishing a subsequent version (supersedes != null) MAY include lineage_id for self-verification. If supplied, the registry MUST verify it matches the deterministically-derived value and MUST reject mismatches with superseded_target (details.reason = "lineage_mismatch", RFC-ACDP-0003 §3.1 step 4).
The lin-001 conformance fixture is the dedicated golden-vector set for this derivation; can-001 additionally cross-checks the same vectors alongside the JCS canonicalization vectors, and sig-001 re-derives the lineage from its registry-assigned ctx_id. All three MUST agree byte-for-byte. The bundled conformance runner (scripts/conformance-runner.py) executes lin-* and can-* lineage vectors directly.
5.6.1 Lineage walk failure
If, while walking back through supersedes references to compute lineage_id for a subsequent-version publish, the registry cannot retrieve an intermediate context, the registry MUST reject the publish request with superseded_target (details.reason = "lineage_walk_failed", HTTP 400). In v0.1.0 this most commonly means the immediate-predecessor chain is intact (RFC-ACDP-0003 §3.1 step 1 already gates the head with details.reason = "not_found" if absent) but a deeper intermediate is missing — for example because of out-of-band administrative deletion (which ACDP itself forbids). Cross-registry intermediate cases cannot arise in v0.1.0 because cross-registry supersession is rejected at §3.1 step 2 with cross_registry_supersession_unsupported before any walk runs; the trigger is reserved here for forward compatibility with a future version's cross-registry supersession (RFC-ACDP-0009 §2.8). The error envelope SHOULD include the failing intermediate ctx_id in details for debuggability:
{
"error": {
"code": "superseded_target",
"message": "Could not retrieve intermediate context while walking the supersession chain.",
"details": {
"reason": "lineage_walk_failed",
"unreachable_ctx_id": "acdp://reg.example/2dffa0..."
}
}
}This reason is reserved for the lineage-walk path; it does NOT cover the immediate-target case (RFC-ACDP-0003 §3.1 step 1 returns details.reason = "not_found" for that) and it does NOT cover the cross-registry case (§3.1 step 2 returns cross_registry_supersession_unsupported).
5.7 Content Hash
The content_hash field of a body is the SHA-256 [FIPS 180-4] digest of the JCS-canonicalized ProducerContent (§2), encoded as the literal string sha256: followed by 64 lowercase hexadecimal characters. ProducerContent is the publish request body with the following fields removed:
content_hashitself (a field cannot contain its own hash);signature(the signature is over the hash, so cannot be in the hashed input);ctx_id,lineage_id,origin_registry,created_at(registry-assigned, not known to the producer at signing time).
All other fields present in the publish request are included in the hash input. The producer computes content_hash over this reduced object, then sets the content_hash and signature fields on the request before submission.
The exclusion list permits the producer to compute content_hash and sign before the registry assigns identifiers. The producer commits to the content; the registry separately binds the identifiers.
Implementations MUST produce identical content_hash values for the same body content across all conforming implementations. Test vectors are provided in the conformance fixtures.
Exclusion-set registry (NORMATIVE)
The table below is the authoritative, versioned list of fields excluded from ProducerContent. It is the single source of truth for the §5.7 exclusion set; the prose above and the conformance fixtures (can-008, can-009) are derived from it.
| Field | Location | Included in ProducerContent? | Introduced in | Rationale |
|---|---|---|---|---|
content_hash | body | No | 0.1.0 | Self-referential — a field cannot contain its own hash. |
signature | body | No | 0.1.0 | The signature covers (and therefore cannot be covered by) the hash. |
ctx_id | body | No | 0.1.0 | Registry-assigned at publish time (§5.5). Not known to the producer at signing time. |
lineage_id | body | No | 0.1.0 | Registry-assigned at publish time (§5.6). |
origin_registry | body | No | 0.1.0 | Registry-assigned at publish time. |
created_at | body | No | 0.1.0 | Registry-assigned at publish time (§5.3). |
registry_state | top-level, outside body | N/A — outside body | 0.1.0 | Mutable, registry-derived state (RFC-ACDP-0004 §4). Never part of the body and therefore never an input to content_hash. |
registry_receipt | top-level, outside body | N/A — outside body | reserved | Reserved by RFC-ACDP-0009 §2.7 for future registry-binding receipts. v0.1.0 consumers MUST ignore this field if present. |
Unknown body fields (NORMATIVE). Body fields not listed in this table AND not in the published body schema (schemas/json/acdp-context-body.schema.json) are included in ProducerContent by default. This is the forward-compatibility guarantee from §6 made concrete: future producer-controlled fields added in minor versions are automatically covered by the producer signature without an exclusion-set update. The fixture can-008-body-with-unknown-producer-field.json is the positive case (an unknown producer field MUST be retained for hashing).
Closed-by-name exclusion (NORMATIVE). Conversely, a field whose name appears in the table above is excluded by name even when its value is unexpected or its presence is irregular. The fixture can-009-body-with-unknown-excluded-field.json is the negative case: a body that carries a non-standard value in origin_registry (for example, set by a malicious or misbehaving producer to spoof another registry) MUST still be excluded by name from ProducerContent at the consumer's hash-recomputation step — the producer signature never bound origin_registry, so its value is registry-honesty territory regardless (§5.9, RFC-ACDP-0008 §9.1).
Exclusion-set evolution (NORMATIVE). Implementations MUST NOT expand the exclusion set without a spec version change. Adding a row to the table above — or treating a field as excluded in any implementation when this table does not list it — is a non-conformant deviation: it would silently break signature verification across the ecosystem because consumers and registries would disagree on what bytes the producer signed. Future versions of ACDP that add a registry-assigned field MUST advertise the new exclusion via the acdp_version mechanism (§6, body.acdp_version) so that downstream verifiers can apply the correct exclusion set for the body's declared version.
Hash verification over raw JSON (REQUIRED for forward compatibility)
Consumers and registries MUST recompute content_hash from the raw received JSON object — not from a lossy typed deserialization of it — unless the typed deserialization provably preserves every unknown field byte-for-byte.
The hazard. ACDP body extensibility is forward-compatible via additive producer-controlled fields (§6). A v0.1 producer adding a new optional field (e.g. priority) signs the JCS canonicalization of an object that includes it. A v0.1.0 consumer that deserializes the body into a typed Body struct without an unknown-field catch-all will silently drop priority; recomputing content_hash from the typed struct then yields a different hash than the producer signed, and the body fails verification with a false hash_mismatch. The error appears at exactly the moment minor-version evolution should be invisible to existing consumers.
Required implementation pattern. To verify a received body:
- Parse the wire bytes into a structure that preserves all keys and their original ordering only if needed by your canonicalizer (most JCS libraries re-sort, so order at parse time does not matter — but field presence does). Examples:
serde_json::Valuein Rust,json.loadsintodict[str, Any]in Python,JSON.parsein TypeScript,interface{}/map[string]json.RawMessagein Go. - Remove the four registry-assigned identity fields and the two integrity fields (the §5.7 exclusion set:
ctx_id,lineage_id,origin_registry,created_at,content_hash,signature). - JCS-canonicalize the resulting object and SHA-256 the canonical bytes; compare against
body.content_hash. - Only after the hash matches, verify the signature over the ASCII bytes of
body.content_hash(per §5.8). - Only after both checks pass, deserialize into a typed model for application use.
Implementations that prefer to deserialize into a typed model first (for ergonomics) MUST guarantee that the typed model preserves unknown producer-controlled fields. Concrete patterns:
- Rust (serde): add
#[serde(flatten)] pub extensions: serde_json::Map<String, serde_json::Value>to capture unknown keys; do NOT use#[serde(deny_unknown_fields)]onBody. The flattened map MUST be re-emitted in canonicalization. - Python (pydantic v2): set
model_config = ConfigDict(extra="allow")on theBodymodel, OR keepdict[str, Any]for unknown keys. - TypeScript: define
Bodyas{ … known fields …, [key: string]: unknown }or use a passthrough decoder (zod's.passthrough()). - Go: unmarshal into
map[string]json.RawMessagefor the verification path; the typedBodystruct (used by application code) is built from the verified map.
Discarding unknown fields before hash recomputation is a CONFORMANCE FAILURE. The fixtures can-008-body-with-unknown-producer-field.json (positive: a producer-added unknown field is part of the hash and MUST be retained) and can-009-body-with-unknown-excluded-field.json (negative: a field whose name is in the exclusion set is excluded by name regardless of whether the v0.1.0 consumer recognizes it) pin the rule.
5.8 Signature
The signature field of a body is a JSON object:
| Field | Type | Required | Description |
|---|---|---|---|
algorithm | string | Yes | Signature algorithm identifier. See §5.10. |
key_id | string | Yes | DID URL identifying the signing key. |
value | string | Yes | Base64-encoded signature bytes. |
The signature value is computed over the bytes of the full content_hash string — that is, the ASCII bytes of sha256: followed by the 64 lowercase hex characters. Implementations MUST NOT sign the raw hash bytes alone.
Registries MUST verify the signature at publish time. A context whose signature does not verify MUST be rejected with the invalid_signature error code (RFC-ACDP-0007).
Key generation (NORMATIVE). Producers MUST generate Ed25519 and ECDSA-P256 key pairs using a cryptographically secure random number generator drawing from the OS entropy pool. On Unix-like systems this is getrandom(2) (Linux ≥ 3.17, macOS ≥ 10.12) or a fresh read from /dev/urandom; on Windows this is BCryptGenRandom (or the legacy CryptGenRandom); managed runtimes MUST use the platform's secure-random API (crypto.getRandomValues / crypto.randomBytes in JS, secrets.token_bytes / os.urandom in Python, crypto/rand.Read in Go, ring::rand::SystemRandom or OsRng in Rust). Implementations MUST NOT use:
- all-zero seeds (every keypair would be the same and publicly known);
- timestamp-based seeds, PID-based seeds, or any seed derived from low-entropy inputs;
- the seed
0x00..01(the sig-002 test scalar) or any other test-vector seed published in this specification — these are TEST-ONLY and publicly known; - the language's default non-cryptographic PRNG (
randpackage in Rust,randomin Python,math/randin Go) — these are deterministic from a low-entropy seed and signatures generated under them are forgeable; - a single global seed re-derived per process without re-seeding from the OS — long-running processes that don't re-seed are vulnerable after sufficient state observation.
Recommended pattern: a library-provided SigningKey::generate() (or language equivalent) that invokes the OS RNG directly and returns a fresh keypair. SigningKey::from_bytes (or equivalent) SHOULD be reserved for loading key material already stored securely; it MUST NOT be the path users reach for when they want a "new" key. Library authors SHOULD make this distinction visible in the API — e.g., generate() returns (SigningKey, VerifyingKey), from_bytes(&[u8]) is named and documented as "load existing key".
Key storage (NORMATIVE). Private key bytes MUST NOT be persisted in cleartext on disk, in a config file, in a .env file committed to source control, in a container image, in process environment variables that other processes can read (/proc/<pid>/environ), in container metadata services, or in cloud-function environment-variable pages. Acceptable storage: an OS keychain (macOS Keychain, Windows DPAPI, Linux Secret Service / libsecret), a hardware security module (HSM), a cloud KMS (AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault) with the private key never leaving the boundary, or a trusted execution environment (TPM, SGX, Apple Secure Enclave). The signing operation SHOULD happen inside the boundary — the producer process holds a handle, not the bytes.
For development and CI, the same secure-storage requirement applies; ephemeral test keys generated per-CI-run and discarded at job end are acceptable, but committed test keys MUST be flagged as TEST-ONLY (as sig-002's keypair is) and MUST NOT be used to sign anything that reaches a production registry. A production publish signed by a leaked, committed, or test-only key is a security incident regardless of whether the content is correct.
5.9 Replay, Tamper, and Impersonation Protection
ACDP's protections decompose by what the producer signature does and does not bind:
What the producer signature binds (cryptographic protection):
- Body tampering is detected by recomputing
content_hashover the canonicalized ProducerContent (§2). Any change in any non-excluded field changes the hash; the signature will not verify. - Producer impersonation of content is prevented: a third party cannot forge a signature without the producer's private key.
- Lineage integrity: each ancestor in
derived_fromis independently signed by its own producer.
What the producer signature does NOT bind (registry-honesty protection):
ctx_id,lineage_id,origin_registry, andcreated_atare registry-assigned. The producer signature does not cover them. A consumer cannot cryptographically verify which registry first accepted the content, whatctx_idit was assigned, what lineage it belongs to, or when publication occurred. These facts rely on registry honesty in v0.1.0.
A malicious or compromised registry could republish a producer's signed content under a different ctx_id or origin_registry (the signature would still verify), or backdate created_at. See RFC-ACDP-0008 §9.1 for the full discussion and §9.2 for mitigations.
A future ACDP version will introduce registry receipts (RFC-ACDP-0009 §2.7) that bind registry-assigned identifiers to the registry's DID, closing this gap cryptographically.
Replay at the wire level is mitigated by HTTPS transport security. ACDP itself does not specify per-request nonces — the body's content hash makes "the same body twice" content-level idempotent. Registries SHOULD implement Idempotency-Key (RFC-ACDP-0003 §6) for true publication-level idempotency.
Distinguish two senses of idempotency:
- Content-level idempotency: identical bodies produce identical
content_hashvalues. This is automatic and intrinsic to ACDP. - Publication-level idempotency: identical publish requests produce identical
ctx_ids and do not create duplicate registry records. This requires the optionalIdempotency-Keymechanism (RFC-ACDP-0003 §6).
Without Idempotency-Key, replaying a publish request creates a new ctx_id for the same content. Producers SHOULD deduplicate locally on content_hash.
5.10 Signature Algorithms
Implementations MUST support ed25519 [RFC 8032]. Implementations MAY support additional algorithms (e.g. ecdsa-p256). A registry's supported algorithms MUST be declared in its capabilities document (RFC-ACDP-0007). Registries MUST reject unsupported_algorithm for any algorithm not in their declared list. The full algorithm vocabulary is maintained in registries/signature-algorithms.md.
5.11 Key Resolution
To verify a producer signature, an implementation MUST resolve signature.key_id (a DID URL) to a public key. v0.1.0 mandates support for did:web only; producers MUST use did:web keys, and registries MUST resolve did:web keys.
The resolution algorithm:
-
Parse the DID URL. Split
signature.key_idinto the DID portion (everything before#) and the fragment (everything after#, REQUIRED). Akey_idwithout a fragment MUST be rejected withkey_resolution_failed. -
Verify producer binding. The DID portion MUST equal
body.agent_id. Mismatch MUST be rejected withkey_not_authorized. -
Resolve the DID document. For
did:web:<authority>[:<path>...]:- Construct the URL:
https://<authority>/.well-known/did.jsonfor a baredid:web:<authority>, orhttps://<authority>/<path-with-colons-replaced-by-slashes>/did.jsonfor a path-bearing form. - HTTPS is REQUIRED; HTTP requests MUST NOT be made. The certificate MUST be valid.
- The response MUST be a JSON object with
Content-Type: application/did+json(orapplication/json). - Failures are classified as transient or permanent and reported with distinct error codes:
- Transient (network or upstream-availability): DNS resolution failure, TLS handshake failure, HTTP non-2xx response, or timeout fetching the DID document. MUST be reported as
key_resolution_unreachable(HTTP 502). These are typically retryable. - Permanent (the upstream document is malformed or does not authorize the requested key): JSON parse error of the fetched document, missing fragment in
signature.key_id, or noverificationMethodmatches the requested fragment. MUST be reported askey_resolution_failed(HTTP 400). These are not retryable without producer-side action (republish with a correctedkey_id, fix the DID document, etc.).
- Transient (network or upstream-availability): DNS resolution failure, TLS handshake failure, HTTP non-2xx response, or timeout fetching the DID document. MUST be reported as
- Construct the URL:
-
Locate the verification method. The DID document's
verificationMethodarray contains key entries. Find the entry whoseidends with#<fragment>(matching the parsed fragment from step 1). If no entry matches, returnkey_resolution_failed. -
Verify authorization. The verification method's
idMUST be referenced by the DID document'sassertionMethodarray (either by fullidURL or by relative#<fragment>). If not, returnkey_not_authorized. -
Extract the public key bytes. The verification method MUST have one of:
publicKeyMultibase— base58-btc encoded, with the multibasezprefix and the multicodec algorithm prefix (e.g.,0xed01for ed25519).publicKeyJwk— a JWK object withkty,crv,x(andyforcrv: P-256).
Implementations MUST support
publicKeyJwkand SHOULD supportpublicKeyMultibase. The verification method'stypefield SHOULD match the algorithm insignature.algorithm(Ed25519VerificationKey2020for ed25519,JsonWebKey2020for either); a mismatch MUST be rejected withinvalid_signature. -
Verify the signature. Use the extracted public key to verify
signature.value(base64-decoded bytes) against the ASCII bytes ofbody.content_hash(per §5.8).
Caching. Implementations SHOULD cache resolved DID documents for at least 5 minutes and at most 24 hours. The cache key is the DID (not the full DID URL). Implementations MUST refresh on any verification failure that could plausibly be due to key rotation.
Testability of key resolution (NON-NORMATIVE). Fixture-bound conformance tests for signature verification (pub-001, pub-006, the sig-* goldens) require outbound HTTPS to a DID-document host. Library users in CI environments without outbound network access — or with self-signed test infrastructure — cannot run these fixtures unless the resolver is testable offline, which in practice means many implementations silently skip them. Library authors implementing the §5.11 resolver SHOULD therefore provide one or both of:
- A custom-root-of-trust constructor that accepts an additional root CA certificate (or a custom TLS verifier callback) so the resolver can be pointed at a local HTTPS server with a self-signed cert. Concrete shapes by language:
WebResolver::with_root_cert(cert_der: &[u8])in Rust,WebResolver(root_ca=cert_bytes)in Python,new WebResolver({ rootCAs: [cert] })in TypeScript,WebResolver{RootCAs: pool}in Go. - A pluggable DID-document store that bypasses network I/O entirely — the application supplies
DidDocuments by DID, and the resolver consults the store before any HTTP fetch. This is the strict-offline pattern and is the only option that survives a fully air-gapped CI.
These testing surfaces MUST be guarded so they do not weaken production:
- They SHOULD be behind a test-only build flag (Rust feature
test-resolver, Pythonextras_require={"testing": [...]}, etc.) OR be available only via a separatetestingmodule that is not re-exported from the package root. - The default production constructor MUST validate certificates against the operating system's trust store; injecting an additional root MUST be an explicit, named operation at construction time (no implicit fallback, no env var).
- Constructors that disable certificate validation entirely MUST NOT exist — a self-signed cert can be added to the trust list, but
verify=False-equivalents have no place in the public API.
Without these surfaces, conformance testing for pub-001 and pub-006 requires a live network and a public DID-document host, which discourages running the fixtures at all and silently lowers ecosystem-wide assurance.
Future DID methods. A future ACDP version may add did:key, did:jwk, and other methods. The resolution algorithm above is did:web-specific; other methods will be specified separately.
v0.1.0 strict verification profile (NORMATIVE). A verification implementation conformant with the acdp-consumer profile (§9.1) MUST, when verifying a v0.1.0 context (body.acdp_version absent or "0.1.0"):
- Require
body.agent_idand the DID portion ofsignature.key_idto bedid:webDIDs (RFC-ACDP-0001 §5.4); reject other DID methods. - Run full body schema validation (
acdp-context-body.schema.json) before any cryptographic step — a structurally invalid body MUST NOT reach hash recomputation or signature verification. - Recompute
content_hashover ProducerContent and verify it before checking the signature (§5.7, §5.8). - Verify the producer signature per the resolution algorithm above.
- Verify every embedded
data_ref.content_hashagainst its decodedembedded.content(RFC-ACDP-0002 §6.3); on mismatch, reportdata_ref_hash_mismatch(RFC-ACDP-0007 §5).
Library authors MAY expose configuration to relax these requirements (e.g. for test environments, compatibility bridges, or future protocol versions), but any relaxed mode MUST be explicitly labeled as non-conformant with v0.1.0 and MUST NOT be the default. The RECOMMENDED API shape is a strict default that cannot be loosened without an explicit, named opt-in — e.g. a VerificationPolicy::strict_v0_1_0() (Rust) / VerificationPolicy.strict_v0_1_0() (Python/TypeScript) constructor as the default, with any other mode reachable only through a separately-named constructor. This strict default is the StrictV010 verification profile of §9.2. Implementations MUST document that only the strict mode is covered by the acdp-consumer conformance profile.
Recommended verification report stage names (NON-NORMATIVE). SDKs that expose a diagnostic verification report (a per-stage pass/fail breakdown) SHOULD use the following stage identifiers, so logs and telemetry are comparable across language implementations. The identifier is the snake_case form for code and config; the display name is for human-facing output.
| Stage identifier | What it covers |
|---|---|
schema | Structural body validation (field presence, types, patterns). |
producer_content_hash | SHA-256 recomputation over ProducerContent matches body.content_hash. |
key_binding | The DID portion of signature.key_id equals body.agent_id. |
did_resolution | The DID document was fetched and parsed successfully. |
assertion_method | The verification method is referenced by the DID document's assertionMethod. |
signature | The Ed25519 or ECDSA-P256 signature verifies against the resolved public key. |
embedded_data_refs | Per-DataRef embedded-hash verification (an array of per-ref outcomes). |
external_data_refs | Per-DataRef external fetch + hash verification (an array of per-ref outcomes). |
registry_receipt | Registry receipt verification — reserved for a future version (RFC-ACDP-0009 §2.7); absent in v0.1.0 reports. |
The stage vocabulary is advisory: it does not change the verification algorithm, only the names implementations SHOULD use when surfacing intermediate results. A v0.1.0 report covers schema through external_data_refs; registry_receipt is listed so the vocabulary is stable when receipts ship.
6. Compatibility Model
ACDP uses a layered compatibility model:
- Registry protocol version is advertised in the registry capabilities document as
acdp_version(e.g.0.1.0). It tells consumers which protocol surface the registry implements. - Body protocol version is advertised optionally inside each body as
body.acdp_version. The body field is producer-signed and bound to the body'scontent_hash. An absentbody.acdp_versionMUST be treated as0.1.0, the inaugural release. Producers SHOULD set the field explicitly so verifiers can unambiguously apply the correct exclusion set (§5.7) and algorithm vocabulary for the body's declared version, especially as future versions evolve them. - Body extensibility is forward-compatible only via additive fields. Breaking body changes require a new protocol version, signaled by
body.acdp_version. - Registry-state extensibility is open: future versions add fields (lifecycle events, relationships, attestations); consumers MUST tolerate unknown fields in registry state. Schema enums for known fields (e.g.
status) use open string patterns so unknown values do not fail validation.
acdp_version and the content hash (NORMATIVE). Producers SHOULD include acdp_version: "0.1.0" explicitly in every published body, even though the protocol treats its absence as equivalent to 0.1.0. Explicit inclusion aids debugging, log analysis, and future version negotiation, and removes any ambiguity about which exclusion set (§5.7) and algorithm vocabulary a verifier should apply.
Consumers, verifiers, and registries MUST NOT inject a default acdp_version value into a body before recomputing content_hash. The hash is computed over the body exactly as received (§5.7, "Hash verification over raw JSON"); adding a synthetic acdp_version field — or any other field — before hashing produces a different hash from the one the producer signed and raises a false hash_mismatch. A body that omits acdp_version MUST be hashed without it; a body that includes it MUST be hashed with it. The default-to-0.1.0 rule above governs the interpretation of an absent field, never a mutation of the body: absence is read as 0.1.0, but the absent field is never materialized.
SDK default behavior guidance. For SDKs that auto-construct PublishRequest objects via a builder, the RECOMMENDED default is to emit acdp_version: "0.1.0" explicitly. This makes the protocol version visible in every produced request without the producer having to call an explicit setter, and aligns the builder's default with the producer SHOULD above.
An SDK MAY instead ship with the omission default — emitting no acdp_version field — for golden-vector compatibility or other reasons (an absent field and "0.1.0" are wire-equivalent for consumers: the default-to-0.1.0 interpretation rule makes them semantically identical, though they produce different content_hash values because the bytes differ). An SDK that takes the omission default MUST:
- Document the choice prominently — in the builder's API documentation and in the SDK README.
- Provide a one-line override that emits the version explicitly (e.g.
.acdp_version(ACDP_VERSION)). - State that absent and explicit
"0.1.0"are interpreted identically by conformant consumers, while noting they are distinct byte sequences and therefore distinctcontent_hashpreimages — a producer MUST pick one and sign over exactly what it emits.
Whichever default an SDK chooses, it MUST NOT mutate a received body to add or normalize acdp_version before hash recomputation — that is the verifier-side prohibition in the NORMATIVE paragraph above.
Major protocol version mismatches are not compatible. Minor versions are expected to be backward compatible. Consumers receiving an unknown acdp_version (in capabilities or in a body) SHOULD treat it as a higher version and degrade gracefully, using only operations defined in the version they understand.
7. Transport
ACDP operations are HTTP-based with JSON request and response bodies, content type application/acdp+json. Operations are defined in:
- RFC-ACDP-0003 —
POST /contexts, supersession. - RFC-ACDP-0004 —
GET /contexts/{ctx_id},GET /contexts/{ctx_id}/body,GET /lineages/{lineage_id},GET /lineages/{lineage_id}/current. - RFC-ACDP-0005 —
GET /contexts/search. - RFC-ACDP-0007 —
GET /.well-known/acdp.json.
ACDP v0.1.0 is JSON-only. Binary transport bindings are out of scope for this version and MAY be specified in a future release.
All ACDP traffic MUST run over TLS in production deployments.
8. Registry Hooks
ACDP maintains the following registries under registries/:
- context-types — registered values for
Body.type. - error-codes — protocol-level error codes returned in error envelopes.
- media-types — content types used in transport bindings.
- locator-schemes — well-known dotted-namespace schemes for structured
data_refs.location. - signature-algorithms — open vocabulary for
signature.algorithmandcapabilities.supported_signature_algorithms. - auth-methods — open vocabulary for
capabilities.read_authentication_methods. - profiles — open vocabulary for
capabilities.profiles.
New entries are added via the RFC process. Experimental identifiers SHOULD use reverse-domain notation.
9. Conformance
A conformant ACDP registry MUST:
- Parse and validate publish requests against
acdp-publish-request.schema.json. - Recompute
content_hashper §5.7 and reject on mismatch. - Verify the producer's signature per §5.8 and reject on failure.
- Assign
ctx_id,origin_registry,created_atper §5.5. - Compute
lineage_idper §5.6. - Validate supersession constraints per RFC-ACDP-0003.
- Serve
GET /.well-known/acdp.jsonper RFC-ACDP-0007. - Pass the conformance fixtures in
schemas/conformance/. - Reproduce the JCS test vectors exactly (
can-001-jcs-vector.json).
A conformant ACDP consumer MUST:
- Verify signatures end-to-end for every context it relies on.
- Treat unknown fields in body and registry state as opaque.
- Treat
status: supersededandstatus: expiredas signals that a context's conclusions may not be current. - Resolve cross-registry
acdp://references per RFC-ACDP-0006 if it follows them.
9.1 Implementation Profiles
ACDP defines profiles to allow partial implementations to declare conformance honestly. Implementations declare their profile(s) in the capabilities document profiles field (RFC-ACDP-0007 §3.1). Each profile is a strict superset of its prerequisite.
acdp-registry-core
The minimum profile for any registry. Implementations MUST:
- Implement
POST /contextsper RFC-ACDP-0003 (full validation pipeline §2.1, supersession §3, idempotency §6 ifsupports_idempotency_keyis advertised). - Implement
GET /contexts/{ctx_id}andGET /contexts/{ctx_id}/bodyper RFC-ACDP-0004 §2. - Implement
GET /lineages/{lineage_id}andGET /lineages/{lineage_id}/currentper RFC-ACDP-0004 §5. - Implement
GET /.well-known/acdp.jsonper RFC-ACDP-0007 §3. - Apply visibility rules per RFC-ACDP-0008 §4.5.
- Pass all conformance fixtures in
schemas/conformance/(publish, retrieval, visibility, canonicalization).
acdp-registry-discovery
Adds keyword search. Implementations MUST:
- Be
acdp-registry-coreconformant. - Implement
GET /contexts/searchper RFC-ACDP-0005 §2 (search semantics §2.5 — required fields, AND-of-terms, ranking, cursor stability). - Pass discovery and visibility-discovery conformance fixtures (notably
vis-002).
acdp-registry-federated
Adds cross-registry resolution. Implementations MUST:
- Be
acdp-registry-coreconformant. - Resolve
acdp://references inderived_fromchains per RFC-ACDP-0006 §4.1 (and apply the trust model in §4.2 and caching guidance in §4.3). - Verify the upstream registry's DID at resolution time per RFC-ACDP-0006 §4.1 step 3: fetch
https://<authority>/.well-known/acdp.json, extractregistry_did, resolve the DID document, and confirm the DID's web binding matches<authority>. On mismatch, treat the cross-registry resolution as failed and returncross_registry_resolution_failed(defined in RFC-ACDP-0007 §5; emitted per RFC-ACDP-0006 §7). - Implement SSRF protections per RFC-ACDP-0006 §7 (IP-range filtering, HTTPS-only, response/timeout caps, redirect cap, DNS-rebinding pin).
acdp-consumer
A consumer of contexts (not a registry). Implementations MUST:
- Verify producer signatures end-to-end on every retrieved context they rely on.
- Resolve cross-registry
acdp://references per RFC-ACDP-0006 if they follow them (and apply the SSRF protections of §7 if they perform server-side resolution). - Apply SSRF protections when dereferencing any producer-controlled URL: producer
did:webresolution during signature verification (RFC-ACDP-0008 §4.8) and externaldata_refs[].locationfetches (RFC-ACDP-0008 §4.9). - Apply visibility rules per RFC-ACDP-0008 §4.5 when retrieving (do not assume a registry's results are scoped on their behalf — verify locally where possible).
- Tolerate unknown fields in body and registry state.
There is no producer-only profile: producers MUST be able to verify their own publications, which requires the same cryptographic core as a consumer.
9.2 Verification profile names (RECOMMENDED)
v0.1.0 verification is always strict for any conformance claim — §5.11 ("v0.1.0 strict verification profile") defines the strict pipeline and requires that it be the non-loosenable default. SDKs MAY additionally expose relaxed or diagnostic verification modes for debugging and test harnesses. When they do, they SHOULD use the following identifiers so documentation, logs, and error messages are consistent across language implementations:
| Profile name | What it verifies | Conformant for v0.1.0? |
|---|---|---|
StrictV010 | The full §5.11 strict pipeline: schema validation, content_hash recomputation, did:web resolution, signature verification, and embedded data_ref.content_hash verification. Returns on the first failure. | Yes — the only mode covered by the acdp-consumer conformance profile. |
Diagnostic | Runs every strict-pipeline stage but records each stage's outcome rather than returning on the first failure, producing a per-stage report (the §5.11 "verification report stage names"). | No — debugging only. A Diagnostic run that reports any stage failure MUST be treated as an overall verification failure. |
UnsafeForTests | May skip DID resolution, signature verification, or schema validation (e.g. to exercise fixtures offline). | No — test harness only. |
StrictV010 is the profile-name spelling of the §5.11 strict policy; the two refer to the same behavior. SDKs MUST NOT expose Diagnostic or UnsafeForTests as default behavior — the default constructor MUST be StrictV010, reachable without opt-in, and any weaker mode MUST require an explicit, separately-named opt-in (§5.11). If an SDK offers a VerificationPolicy type, any policy that weakens a strict-mode requirement MUST be documented as non-conformant with v0.1.0, and a verification result produced under it MUST NOT be presented as a conformant verification.
10. Security Considerations
See RFC-ACDP-0008 Security for the full threat model. Implementations MUST use a cryptographically secure RNG for UUIDs, store private keys in secure storage, and validate all inputs against the JSON Schemas before processing.
11. IANA Considerations
11.1 URI Scheme Registration
This document requests provisional registration of the acdp URI scheme:
- Scheme name:
acdp - Status: Provisional
- Applications/protocols that use this scheme: Agent Context Distribution Protocol (ACDP)
- Contact: Zer07 Labs
<specifications@zer07labs.com> - Change controller: Zer07 Labs
- References: This document
- Syntax:
acdp://<authority>/<uuid>, where<authority>is a DNS hostname per [RFC 1035] and<uuid>is a UUID v4 per [RFC 9562] - Security considerations: See RFC-ACDP-0008
- Encoding considerations: See §5
11.2 Media Type Registration
- Type:
application - Subtype:
acdp+json - Required parameters: None
- Optional parameters: None. Protocol version is carried in JSON:
acdp_versionin the capabilities document (RFC-ACDP-0007 §3) and optionalbody.acdp_versionin context bodies (§6). The media type does NOT carry aversionparameter; doing so would create two competing version sources. - Encoding considerations: UTF-8 per [RFC 8259]
- Security considerations: See RFC-ACDP-0008
- Published specification: This document
- Applications that use this media type: ACDP registries and clients
11.3 Well-Known URI Registration
- URI suffix:
acdp.json - Change controller: Zer07 Labs
- Specification document(s): This document; RFC-ACDP-0007 §3
- Related information: None
12. References
12.1 Normative References
- [DID-CORE] World Wide Web Consortium, "Decentralized Identifiers (DIDs) v1.0", W3C Recommendation, July 2022.
- [FIPS 180-4] National Institute of Standards and Technology, "Secure Hash Standard (SHS)", FIPS PUB 180-4, August 2015.
- [RFC 1035] Mockapetris, P., "Domain names — implementation and specification", STD 13, RFC 1035, November 1987.
- [RFC 2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997.
- [RFC 3339] Klyne, G. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, July 2002.
- [RFC 3629] Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, November 2003.
- [RFC 9562] Davis, K., Peabody, B., and P. Leach, "Universally Unique IDentifiers (UUIDs)", RFC 9562, May 2024. Obsoletes RFC 4122.
- [RFC 8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, January 2017.
- [RFC 8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, May 2017.
- [RFC 8259] Bray, T., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, December 2017.
- [RFC 8785] Rundgren, A., Jordan, B., and S. Erdtman, "JSON Canonicalization Scheme (JCS)", RFC 8785, June 2020.