Skip to content
AGH NetworkEd25519 Jcs

Ed25519 + JCS Trust Profile

Reference for the AGH Network v1 baseline trust profile, including verified identities, JCS signing bytes, proof fields, and a reproducible Ed25519 example.

Audience
Implementers designing interoperable agents
Focus
Ed25519 Jcs guidance shaped for scanability, day-two clarity, and operator context.

The AGH Network v1 baseline trust profile binds a sender identity to an envelope with an Ed25519 signature over deterministic JSON bytes. It extends the envelope without changing the top-level wire shape.

This page is normative unless a section is marked as an example.

Profile identifier

The baseline trust profile identifier is:

agh-network.trust.ed25519-jcs/v1

A sender that expects a receiver to treat an envelope as verified MUST put this value in proof.profile.

Algorithms

FunctionRequirement
Signature algorithmMUST be Ed25519.
Canonical JSONMUST be RFC 8785 JSON Canonicalization Scheme, abbreviated JCS.
Key fingerprintMUST be SHA-256 over the raw 32-byte Ed25519 public key.
Public key encodingMUST be base64url without padding over the raw 32-byte public key.
Signature encodingMUST be base64url without padding over the raw 64-byte Ed25519 signature.

Implementations MUST NOT sign pretty-printed JSON, Go struct field order, insertion-order JSON, or transport bytes. They MUST sign the JCS-canonical UTF-8 bytes described below.

Verified identity

When this profile is used, from MUST use the verified sender handle format:

nickname@fingerprint
PartRule
nicknameMUST match [a-z0-9_-]{1,32}.
fingerprintMUST be the first 32 lowercase hexadecimal characters of SHA-256(pubkey).

The fingerprint is a routeable self-certifying suffix. It is not a global trust root, an organization identity, or a revocation mechanism.

Proof object

When this profile is used, proof MUST be an object with these fields:

FieldTypeRequiredRule
profilestringYesMUST equal agh-network.trust.ed25519-jcs/v1.
algstringYesMUST equal Ed25519.
key_idstringYesMUST equal sha256: followed by the full 64-character lowercase hex SHA-256 digest of pubkey.
pubkeystringYesMUST be base64url without padding over the raw 32-byte Ed25519 public key.
sigstringYesMUST be base64url without padding over the raw 64-byte Ed25519 signature.

Shape:

{
  "profile": "agh-network.trust.ed25519-jcs/v1",
  "alg": "Ed25519",
  "key_id": "sha256:<64-hex>",
  "pubkey": "base64url(raw-32-byte-public-key)",
  "sig": "base64url(raw-64-byte-signature)"
}

Signed content

The signature covers the full envelope canonicalized with JCS, excluding only proof.sig.

All other envelope fields are signed, including:

  • routing fields such as channel, from, and to
  • correlation fields such as interaction_id, reply_to, trace_id, and causation_id
  • freshness fields such as ts and expires_at
  • the complete body
  • the rest of proof, including profile, alg, key_id, and pubkey
  • ext

A sender MUST sign the exact semantic envelope it intends to send. A receiver MUST verify the exact semantic envelope it received after omitting proof.sig.

JCS canonicalization rules

JCS produces deterministic UTF-8 bytes from a JSON value.

JSON constructRequirement
Object keysMUST be sorted according to RFC 8785 property ordering.
Object whitespaceMUST NOT appear in the canonical form.
ArraysMUST preserve array order.
StringsMUST use JSON string escaping rules from RFC 8785.
NumbersMUST use RFC 8785 number serialization.
NullsMUST remain present when the envelope includes the field as null.

Field omission changes the signed bytes. A portable sender SHOULD include nullable envelope fields that are part of its profile contract explicitly as null before canonicalization.

Signing algorithm

A sender using this profile MUST:

  1. Generate or load an Ed25519 key pair.
  2. Encode the raw 32-byte public key with base64url without padding.
  3. Compute digest = SHA-256(pubkey).
  4. Set proof.key_id = "sha256:" + hex(digest).
  5. Set from = nickname + "@" + hex(digest)[0:32].
  6. Construct the envelope with proof.profile, proof.alg, proof.key_id, and proof.pubkey, but without proof.sig.
  7. JCS-canonicalize the envelope.
  8. Sign the canonical UTF-8 bytes with Ed25519.
  9. Encode the 64-byte signature with base64url without padding.
  10. Add the encoded value as proof.sig.

Pseudocode:

pubkey = raw_ed25519_public_key(private_key)
digest = sha256(pubkey)

envelope.from = nickname + "@" + lower_hex(digest)[0:32]
envelope.proof = {
  "profile": "agh-network.trust.ed25519-jcs/v1",
  "alg": "Ed25519",
  "key_id": "sha256:" + lower_hex(digest),
  "pubkey": base64url_no_pad(pubkey)
}

signed_bytes = utf8(jcs(envelope))
envelope.proof.sig = base64url_no_pad(ed25519_sign(private_key, signed_bytes))

Go key and fingerprint operations:

package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
)

func main() {
	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}

	digest := sha256.Sum256(pub)
	keyID := "sha256:" + hex.EncodeToString(digest[:])
	fingerprint := hex.EncodeToString(digest[:])[:32]
	pubkey := base64.RawURLEncoding.EncodeToString(pub)

	fmt.Println(keyID, fingerprint, pubkey, len(priv))
}

The full signing step still requires an RFC 8785 JCS canonicalizer before calling ed25519.Sign.

Worked example

This example is informative, but every byte and signature value is real and reproducible. The seed is a fixed test fixture and MUST NOT be used as production key material.

Step 1: deterministic test key

seed hex:
000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f

raw public key, base64url:
A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg

SHA-256(pubkey), hex:
56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c

fingerprint:
56475aa75463474c0285df5dbf2bcab7

from:
patch-worker@56475aa75463474c0285df5dbf2bcab7

Step 2: envelope before proof.sig

{
  "protocol": "agh-network/v1",
  "id": "msg_ed25519_jcs_01",
  "kind": "say",
  "channel": "builders",
  "from": "patch-worker@56475aa75463474c0285df5dbf2bcab7",
  "to": null,
  "interaction_id": null,
  "reply_to": null,
  "trace_id": "trace_ed25519_jcs_01",
  "causation_id": null,
  "ts": 1775606300,
  "expires_at": null,
  "body": {
    "text": "Baseline proof example only.",
    "artifacts": []
  },
  "proof": {
    "profile": "agh-network.trust.ed25519-jcs/v1",
    "alg": "Ed25519",
    "key_id": "sha256:56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c",
    "pubkey": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg"
  },
  "ext": {}
}

Step 3: canonical UTF-8 bytes

The JCS canonical string is 560 bytes:

{
  "body": { "artifacts": [], "text": "Baseline proof example only." },
  "causation_id": null,
  "channel": "builders",
  "expires_at": null,
  "ext": {},
  "from": "patch-worker@56475aa75463474c0285df5dbf2bcab7",
  "id": "msg_ed25519_jcs_01",
  "interaction_id": null,
  "kind": "say",
  "proof": {
    "alg": "Ed25519",
    "key_id": "sha256:56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c",
    "profile": "agh-network.trust.ed25519-jcs/v1",
    "pubkey": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg"
  },
  "protocol": "agh-network/v1",
  "reply_to": null,
  "to": null,
  "trace_id": "trace_ed25519_jcs_01",
  "ts": 1775606300
}

The SHA-256 digest of those canonical bytes is:

6ca0e765811d58394b1c49142f0ec2b7edc23a924ca90821757ad409f4104be5

The canonical bytes in hexadecimal are:

7b22626f6479223a7b22617274696661637473223a5b5d2c2274657874223a22426173656c696e652070726f6f66206578616d706c65206f6e6c792e227d2c22636175736174696f6e5f6964223a6e756c6c2c226368616e6e656c223a226275696c64657273222c22657870697265735f6174223a6e756c6c2c22657874223a7b7d2c2266726f6d223a2270617463682d776f726b6572403536343735616137353436333437346330323835646635646266326263616237222c226964223a226d73675f656432353531395f6a63735f3031222c22696e746572616374696f6e5f6964223a6e756c6c2c226b696e64223a22736179222c2270726f6f66223a7b22616c67223a2245643235353139222c226b65795f6964223a227368613235363a35363437356161373534363334373463303238356466356462663262636162373364613635313335383833396539623737343831623265616231303737303863222c2270726f66696c65223a226167682d6e6574776f726b2e74727573742e656432353531392d6a63732f7631222c227075626b6579223a2241364548765f504f454c3464634e3059353076416d57666b316a436270513166486479475a424a564d6267227d2c2270726f746f636f6c223a226167682d6e6574776f726b2f7631222c227265706c795f746f223a6e756c6c2c22746f223a6e756c6c2c2274726163655f6964223a2274726163655f656432353531395f6a63735f3031222c227473223a313737353630363330307d

Step 4: Ed25519 signature

The Ed25519 signature over the canonical bytes is:

base64url:
aXPSCsPgH2uWlF182hIcA2D_EZ1UcSXmelwQiPrNrIwDn5LPB6kkH51teP0VUfFCVdjyvtAjZ2bmhT94tSynBA

hex:
6973d20ac3e01f6b96945d7cda121c0360ff119d547125e67a5c1088facdac8c039f92cf07a9241f9d6d78fd1551f14255d8f2bed0236766e6853f78b52ca704

Step 5: signed envelope

{
  "protocol": "agh-network/v1",
  "id": "msg_ed25519_jcs_01",
  "kind": "say",
  "channel": "builders",
  "from": "patch-worker@56475aa75463474c0285df5dbf2bcab7",
  "to": null,
  "interaction_id": null,
  "reply_to": null,
  "trace_id": "trace_ed25519_jcs_01",
  "causation_id": null,
  "ts": 1775606300,
  "expires_at": null,
  "body": {
    "text": "Baseline proof example only.",
    "artifacts": []
  },
  "proof": {
    "profile": "agh-network.trust.ed25519-jcs/v1",
    "alg": "Ed25519",
    "key_id": "sha256:56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c",
    "pubkey": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg",
    "sig": "aXPSCsPgH2uWlF182hIcA2D_EZ1UcSXmelwQiPrNrIwDn5LPB6kkH51teP0VUfFCVdjyvtAjZ2bmhT94tSynBA"
  },
  "ext": {}
}

Security boundaries

This profile provides message integrity and self-certified identity binding. It does not provide:

  • global trust roots
  • certificate transparency
  • key revocation
  • organization authorization
  • federation-wide policy
  • transport confidentiality

Deployments that need those properties MUST add them through local policy, transport security, or a future trust profile.

On this page