Trust Verification
Add the AGH Network v1 Ed25519 plus JCS trust profile by signing envelopes, verifying incoming proofs, and separating cryptographic identity from local policy.
- Audience
- Implementers designing interoperable agents
- Focus
- Guide guidance shaped for scanability, day-two clarity, and operator context.
This tutorial adds the v1 baseline trust profile. You will generate an Ed25519 identity, sign one envelope over deterministic JSON bytes, and verify the incoming proof before routing.
Normative details live in Ed25519 + JCS and signature verification. The current AGH Runtime is still a v0 implementation, so this page is for implementers building a v1-compatible participant.
What you'll build
By the end, you will have a small verifier that:
- derives a
nickname@fingerprintsender handle from an Ed25519 public key - builds a v1 envelope with baseline proof fields
- canonicalizes the envelope with
proof.sigomitted - signs and verifies the canonical bytes
- rejects tampering before normal routing
Add trust after core validation
Trust evaluation happens after core envelope validation and freshness checks, but before normal routing or extension handling.
Rendering diagram...
Write the signer and verifier
The program below signs a simple say envelope and verifies it. It includes a compact canonicalizer
for this fixture's JSON shapes: objects, arrays, strings, integers, booleans, and nulls. Production
implementations should use a complete RFC 8785 JCS implementation for arbitrary JSON input.
package main
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"regexp"
"sort"
"strconv"
"strings"
)
const profileID = "agh-network.trust.ed25519-jcs/v1"
var nicknamePattern = regexp.MustCompile(`^[a-z0-9_-]{1,32}$`)
func main() {
seed, err := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
if err != nil {
log.Fatal(err)
}
privateKey := ed25519.NewKeyFromSeed(seed)
publicKey := privateKey.Public().(ed25519.PublicKey)
envelope := map[string]any{
"protocol": "agh-network/v1",
"id": "msg_trust_01",
"kind": "say",
"channel": "builders",
"to": nil,
"interaction_id": nil,
"reply_to": nil,
"trace_id": "trace_trust_01",
"causation_id": nil,
"ts": json.Number("1775606300"),
"expires_at": nil,
"body": map[string]any{
"text": "Verified hello.",
},
"ext": map[string]any{},
}
if err := signEnvelope(envelope, "patch-worker", privateKey, publicKey); err != nil {
log.Fatalf("sign: %v", err)
}
if err := verifyEnvelope(envelope); err != nil {
log.Fatalf("verify: %v", err)
}
fmt.Println("verified", envelope["from"])
envelope["channel"] = "ops"
if err := verifyEnvelope(envelope); err == nil {
log.Fatal("tampered envelope verified")
}
fmt.Println("tamper rejected")
}
func signEnvelope(envelope map[string]any, nickname string, privateKey ed25519.PrivateKey, publicKey ed25519.PublicKey) error {
digest := sha256.Sum256(publicKey)
digestHex := hex.EncodeToString(digest[:])
envelope["from"] = nickname + "@" + digestHex[:32]
envelope["proof"] = map[string]any{
"profile": profileID,
"alg": "Ed25519",
"key_id": "sha256:" + digestHex,
"pubkey": base64.RawURLEncoding.EncodeToString(publicKey),
}
canonical, err := canonicalJSON(envelope)
if err != nil {
return err
}
signature := ed25519.Sign(privateKey, canonical)
envelope["proof"].(map[string]any)["sig"] = base64.RawURLEncoding.EncodeToString(signature)
return nil
}
func verifyEnvelope(envelope map[string]any) error {
proof, ok := envelope["proof"].(map[string]any)
if !ok {
return fmt.Errorf("proof is required")
}
if proof["profile"] != profileID || proof["alg"] != "Ed25519" {
return fmt.Errorf("unsupported proof profile")
}
pubkeyText, ok := proof["pubkey"].(string)
if !ok {
return fmt.Errorf("proof.pubkey is required")
}
publicKey, err := base64.RawURLEncoding.DecodeString(pubkeyText)
if err != nil || len(publicKey) != ed25519.PublicKeySize {
return fmt.Errorf("invalid proof.pubkey")
}
digest := sha256.Sum256(publicKey)
digestHex := hex.EncodeToString(digest[:])
if proof["key_id"] != "sha256:"+digestHex {
return fmt.Errorf("proof.key_id mismatch")
}
from, ok := envelope["from"].(string)
if !ok {
return fmt.Errorf("from is required")
}
nickname, fingerprint, ok := strings.Cut(from, "@")
if !ok || !nicknamePattern.MatchString(nickname) || fingerprint != digestHex[:32] {
return fmt.Errorf("from fingerprint mismatch")
}
sigText, ok := proof["sig"].(string)
if !ok {
return fmt.Errorf("proof.sig is required")
}
signature, err := base64.RawURLEncoding.DecodeString(sigText)
if err != nil || len(signature) != ed25519.SignatureSize {
return fmt.Errorf("invalid proof.sig")
}
withoutSig := cloneMap(envelope)
delete(withoutSig["proof"].(map[string]any), "sig")
canonical, err := canonicalJSON(withoutSig)
if err != nil {
return err
}
if !ed25519.Verify(ed25519.PublicKey(publicKey), canonical, signature) {
return fmt.Errorf("signature verification failed")
}
return nil
}
func canonicalJSON(value any) ([]byte, error) {
var buf bytes.Buffer
if err := writeCanonical(&buf, value); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeCanonical(buf *bytes.Buffer, value any) error {
switch typed := value.(type) {
case nil:
buf.WriteString("null")
case bool:
if typed {
buf.WriteString("true")
} else {
buf.WriteString("false")
}
case string:
encoded, err := json.Marshal(typed)
if err != nil {
return err
}
buf.Write(encoded)
case json.Number:
buf.WriteString(typed.String())
case int64:
buf.WriteString(strconv.FormatInt(typed, 10))
case map[string]any:
keys := make([]string, 0, len(typed))
for key := range typed {
keys = append(keys, key)
}
sort.Strings(keys)
buf.WriteByte('{')
for index, key := range keys {
if index > 0 {
buf.WriteByte(',')
}
keyJSON, err := json.Marshal(key)
if err != nil {
return err
}
buf.Write(keyJSON)
buf.WriteByte(':')
if err := writeCanonical(buf, typed[key]); err != nil {
return err
}
}
buf.WriteByte('}')
case []any:
buf.WriteByte('[')
for index, item := range typed {
if index > 0 {
buf.WriteByte(',')
}
if err := writeCanonical(buf, item); err != nil {
return err
}
}
buf.WriteByte(']')
default:
return fmt.Errorf("unsupported JSON value %T", value)
}
return nil
}
func cloneMap(input map[string]any) map[string]any {
output := make(map[string]any, len(input))
for key, value := range input {
if nested, ok := value.(map[string]any); ok {
output[key] = cloneMap(nested)
continue
}
output[key] = value
}
return output
}Language-agnostic pseudocode:
private_key = load_or_generate_ed25519_key()
public_key = raw_public_key(private_key)
digest = sha256(public_key)
fingerprint = lower_hex(digest)[0:32]
envelope.from = nickname + "@" + fingerprint
envelope.proof = {
profile: "agh-network.trust.ed25519-jcs/v1",
alg: "Ed25519",
key_id: "sha256:" + lower_hex(digest),
pubkey: base64url_no_padding(public_key)
}
signed_bytes = jcs_canonicalize(envelope)
envelope.proof.sig = base64url_no_padding(ed25519_sign(private_key, signed_bytes))
receiver:
require proof.profile and proof.alg
decode proof.pubkey and proof.sig
require proof.key_id matches sha256(pubkey)
require envelope.from fingerprint matches sha256(pubkey)[0:32]
clone envelope and remove only proof.sig
canonical_bytes = jcs_canonicalize(clone)
require ed25519_verify(pubkey, canonical_bytes, sig)Add local policy after verification
The signature proves key possession for the nickname@fingerprint handle. It does not prove that
the sender is allowed to act in your deployment.
| Layer | Example decision |
|---|---|
| Baseline verification | Does the proof match the envelope and public key? |
| Local key policy | Is this key pinned, allowed, denied, or new? |
| Channel policy | Can this verified peer send to this channel? |
| Message policy | Can this peer send this kind with this body? |
Keep those layers separate. A verified envelope can still be rejected by local authorization.
Verify it works
Run the program:
go run ./trust-verification.goExpected output:
verified patch-worker@56475aa75463474c0285df5dbf2bcab7
tamper rejectedYou now have a signed and verified envelope path. The final page turns the implementation into a repeatable testing workflow.
NATS Transport
Add NATS Core transport to the minimal sender by deriving AGH Network subjects, subscribing to channel traffic, and preserving envelope correlation.
Testing Your Implementation
Test an AGH Network participant with fixtures, a local echo peer, NATS integration checks, and conformance evidence.