Skip to content

Canonical JSON Signing Specification ​

Version: 0.1.0 Status: Draft

This document specifies how Kanoniv Agent Auth signs and verifies JSON payloads using Ed25519 signatures over a canonical byte representation.

1. SignedMessage Structure ​

A signed message envelope has five fields:

json
{
  "payload": <any JSON value>,
  "signer_did": "did:agent:<32-hex-chars>",
  "nonce": "<uuid-v4>",
  "timestamp": "<rfc3339-millis-utc>",
  "signature": "<hex-encoded-ed25519-signature>"
}
FieldTypeDescription
payloadAny JSON valueThe message content. Object, array, string, number, boolean, or null.
signer_didStringThe did:agent: identifier of the signing agent.
nonceStringA UUID v4 string for replay protection. MUST be unique per message.
timestampStringRFC 3339 timestamp with millisecond precision and UTC Z suffix. Example: 2026-03-15T14:30:00.123Z.
signatureStringLowercase hex-encoded 64-byte Ed25519 signature (128 hex characters).

2. Canonical Serialization ​

The signature covers a canonical byte representation of the envelope. The canonical form ensures deterministic serialization across languages and implementations.

2.1 Construction ​

  1. Construct an ordered map with exactly four entries in alphabetical key order:

    • "nonce" -> JSON string of the nonce value
    • "payload" -> the payload value as-is
    • "signer_did" -> JSON string of the signer DID
    • "timestamp" -> JSON string of the timestamp
  2. Serialize the ordered map to compact JSON (UTF-8, no trailing newline, no whitespace between tokens).

2.2 Payload Handling ​

The payload value is included in its original serialization form. Implementations MUST NOT recursively sort keys within the payload. Only the four top-level envelope keys are ordered.

Example: If the payload is {"z": 1, "a": 2}, the canonical form preserves this key order within the payload:

json
{"nonce":"550e8400-e29b-41d4-a716-446655440000","payload":{"z":1,"a":2},"signer_did":"did:agent:21fe31dfa154a261626bf854046fd227","timestamp":"2026-03-15T14:30:00.123Z"}

2.3 Implementation Notes ​

  • Rust: Use BTreeMap<&str, serde_json::Value> with keys inserted as "nonce", "payload", "signer_did", "timestamp". BTreeMap maintains alphabetical order. Serialize with serde_json::to_vec.
  • TypeScript: Manually construct the JSON string by concatenating keys in order. Do NOT use JSON.stringify on an object - JavaScript object key ordering is not guaranteed for integer-like keys.
  • Python: Use collections.OrderedDict with keys in alphabetical order, or manually construct the JSON string.

3. Signing Algorithm ​

Given an AgentKeyPair and a JSON payload:

  1. Derive the agent's identity: identity = keypair.identity().
  2. Generate a fresh nonce: nonce = UUID_v4().
  3. Generate the timestamp: timestamp = now_utc().to_rfc3339_millis() + "Z".
  4. Compute the canonical bytes: bytes = canonical_serialize(nonce, payload, identity.did, timestamp).
  5. Sign with Ed25519: sig = ed25519_sign(keypair.secret_key, bytes).
  6. Hex-encode the 64-byte signature: signature = hex_lower(sig).
  7. Return SignedMessage { payload, signer_did: identity.did, nonce, timestamp, signature }.

4. Verification Algorithm ​

Given a SignedMessage and an AgentIdentity (public key + DID):

  1. Check signer DID. If message.signer_did != identity.did, return error SignatureInvalid.
  2. Recompute canonical bytes. bytes = canonical_serialize(message.nonce, message.payload, message.signer_did, message.timestamp).
  3. Decode signature. sig_bytes = hex_decode(message.signature). If not valid hex, return error InvalidSignatureEncoding. If length is not 64 bytes, return error InvalidSignatureEncoding.
  4. Verify Ed25519 signature. ed25519_verify(identity.public_key, bytes, sig_bytes). If verification fails, return error SignatureInvalid.
  5. Return success.

Important: Verification does not check the timestamp or nonce for freshness. Timestamp and nonce validation (replay protection) is application-specific and left to the verifier.

5. Content Hash ​

The content hash uniquely identifies a signed message. It is used as:

  • parent_id in provenance DAG chaining
  • delegation_hash in invocation payloads
  • Revocation identifiers

5.1 Algorithm ​

  1. Construct an ordered map with exactly five entries in alphabetical key order:

    • "nonce" -> JSON string of the nonce
    • "payload" -> the payload value as-is
    • "signature" -> JSON string of the hex signature
    • "signer_did" -> JSON string of the signer DID
    • "timestamp" -> JSON string of the timestamp
  2. Serialize to compact JSON (UTF-8).

  3. Compute SHA-256 of the JSON bytes.

  4. Return lowercase hex encoding of the 32-byte hash (64 hex characters).

5.2 Distinction from Signing Canonical Form ​

The signing canonical form has 4 keys: {nonce, payload, signer_did, timestamp} (excludes signature because the signature does not exist yet at signing time).

The content hash canonical form has 5 keys: {nonce, payload, signature, signer_did, timestamp} (includes signature because it identifies the complete signed message).

Both use alphabetical key ordering. Both use BTreeMap (Rust) or equivalent ordered map.

5.3 Properties ​

  • Deterministic. The same signed message always produces the same content hash.
  • Collision resistant. SHA-256 provides 128-bit collision resistance.
  • Includes signature. Two messages with identical payloads but different nonces (and therefore different signatures) produce different content hashes.

6. Replay Protection ​

Each signed message includes a UUID v4 nonce. Implementations MUST generate a fresh nonce for every message. Reusing a nonce enables replay attacks.

Verifiers MAY implement replay protection by:

  • Maintaining a set of seen nonces and rejecting duplicates
  • Rejecting messages with timestamps outside a configurable time window
  • Combining both approaches

The protocol provides the nonce and timestamp; enforcement policy is application-specific.

7. Cross-Language Interoperability ​

7.1 Requirements ​

All implementations MUST:

  1. Produce byte-identical canonical forms for the same inputs.
  2. Verify signatures produced by any other implementation.
  3. Compute identical content hashes for the same signed message.

7.2 Test Vector ​

Using the test keypair:

Secret: 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60
Public: d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a
DID:    did:agent:21fe31dfa154a261626bf854046fd227

Given:

  • payload: {"action": "test"}
  • nonce: "550e8400-e29b-41d4-a716-446655440000"
  • timestamp: "2026-01-01T00:00:00.000Z"

The canonical bytes (for signing) MUST be:

json
{"nonce":"550e8400-e29b-41d4-a716-446655440000","payload":{"action":"test"},"signer_did":"did:agent:21fe31dfa154a261626bf854046fd227","timestamp":"2026-01-01T00:00:00.000Z"}

Any implementation signing these canonical bytes with the test secret key MUST produce the same 64-byte Ed25519 signature.

The identity and delegation layer for AI agents.