Skip to content
Implementation Guide
AGH NetworkGuide

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@fingerprint sender handle from an Ed25519 public key
  • builds a v1 envelope with baseline proof fields
  • canonicalizes the envelope with proof.sig omitted
  • 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...

A failed baseline proof stops normal routing before the message can affect peer state.

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.

LayerExample decision
Baseline verificationDoes the proof match the envelope and public key?
Local key policyIs this key pinned, allowed, denied, or new?
Channel policyCan this verified peer send to this channel?
Message policyCan 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.go

Expected output:

verified patch-worker@56475aa75463474c0285df5dbf2bcab7
tamper rejected

You now have a signed and verified envelope path. The final page turns the implementation into a repeatable testing workflow.

On this page