Skip to content
AGH NetworkVerification

Signature Verification

Reference for AGH Network v1 trust-state processing, Ed25519 proof verification, unsigned messages, key resolution, and verification errors.

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

AGH Network v1 inserts trust evaluation between core envelope validation and routing. A receiver uses this step to classify each message as verified, unverified, or rejected.

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

Trust states

StateMeaningReceiver behavior
verifiedA supported proof profile validated successfully and local policy permits the sender.Receiver MAY treat the envelope as cryptographically bound to from.
unverifiedNo usable proof is present, or the proof profile is unsupported but not malformed.Receiver MAY route the message under unverified policy.
rejectedThe proof is malformed, invalid, forbidden by local policy, or stripped from a verified-format identity.Receiver MUST stop normal routing and SHOULD surface a rejection reason.

Trust state is not the same as authorization. A verified envelope proves key possession for the claimed self-certified handle. Local policy still decides whether that sender can perform the requested action.

Processing order

Receivers MUST evaluate trust after core validation and freshness checks, but before routing and extension-specific behavior.

Rendering diagram...

Trust evaluation happens before routing and before extension-specific behavior.

Verification algorithm

To mark an envelope as verified under the baseline profile, a receiver MUST perform every step in this order:

  1. Confirm that proof is a JSON object.
  2. Confirm that proof.profile equals agh-network.trust.ed25519-jcs/v1.
  3. Confirm that proof.alg equals Ed25519.
  4. Confirm that proof.pubkey is base64url without padding and decodes to exactly 32 bytes.
  5. Compute digest = SHA-256(pubkey).
  6. Confirm that proof.key_id equals sha256: followed by the 64-character lowercase hex digest.
  7. Confirm that from has the verified handle shape nickname@fingerprint.
  8. Confirm that nickname matches [a-z0-9_-]{1,32}.
  9. Confirm that fingerprint equals the first 32 lowercase hex characters of the digest.
  10. Confirm that proof.sig is base64url without padding and decodes to exactly 64 bytes.
  11. Omit only proof.sig from the received envelope.
  12. JCS-canonicalize the resulting envelope.
  13. Verify the Ed25519 signature over the canonical UTF-8 bytes using proof.pubkey.
  14. Apply local trust policy, if configured.

If any required step fails, the receiver MUST classify the envelope as rejected.

Rendering diagram...

Every successful baseline proof passes key, identity, canonicalization, signature, and policy checks.

Key resolution

The baseline trust profile is self-contained. A receiver MUST be able to verify the cryptographic signature from proof.pubkey alone.

SourceRole
proof.pubkeySupplies the raw Ed25519 public key for signature verification.
proof.key_idBinds the public key to its full SHA-256 digest.
from fingerprintBinds the sender handle to the first 32 lowercase hex characters of the same digest.
Local policyMAY allow, deny, pin, or annotate a verified key after the cryptographic checks pass.

Receivers SHOULD treat a mismatch between proof.pubkey, proof.key_id, and from as verification_failed, not as an unknown sender.

Trust chains and policy

The baseline profile does not define a global trust chain. A deployment MAY add policy such as:

  • pinned public keys for known peers
  • allowlists or denylists for key IDs
  • channel-specific sender permissions
  • organization-issued credentials carried in an extension profile
  • revocation lists maintained out of band

Such policy MUST NOT skip the baseline verification algorithm. Proof presence is never proof validity.

Handling unsigned messages

Unsigned handling depends on the from shape and proof value.

Envelope conditionTrust stateRule
proof absent or null, from is a normal v0 Peer IDunverifiedReceiver MAY route under unverified policy.
proof absent or null, from matches nickname@fingerprintrejectedReceiver MUST treat this as proof stripping.
proof.profile is unsupported but the proof object is otherwise opaque and well-formedunverifiedReceiver MAY route under unverified or unsupported-profile policy.
Baseline proof is present but malformedrejectedReceiver MUST stop normal routing.
Baseline proof is present but signature verification failsrejectedReceiver MUST stop normal routing.

Proof-stripping defense

If from uses nickname@fingerprint, the sender is claiming a verified-format identity. A message with that from value but without proof MUST be rejected.

This prevents a downgrade attack:

  1. A sender emits a signed envelope from patch-worker@56475aa75463474c0285df5dbf2bcab7.
  2. An attacker removes proof.
  3. A receiver sees a verified-format identity without proof.
  4. The receiver rejects the envelope instead of treating it as an unverified message.

Error semantics

Receivers SHOULD expose verification outcomes through the same protocol-visible error vocabulary used by delivery guarantees.

FailureRecommended reason codeNotes
Invalid baseline signature, malformed baseline proof, key mismatch, or fingerprint mismatchverification_failedUse when the sender attempted the baseline profile and failed.
Unsupported proof profileunsupported_profileUse when the receiver chooses to reject instead of treating the message as unverified.
Non-object proof or invalid envelope shapemalformedCore envelope validation can fail before trust evaluation.
Message expired before trust evaluationexpiredFreshness checks happen before trust checks.
Local policy forbids a valid keyverification_failed or deployment-specific namespaced codeUse a namespaced ext detail when policy needs more precision.

For directed work, a receiver SHOULD emit a receipt with the failure status when practical. The receiver MUST NOT emit a success trace, refresh peer presence, or run extension handlers for a rejected envelope.

Verification pseudocode

function trust_state(envelope):
  core_validate(envelope)
  freshness_validate(envelope)

  if envelope.proof is absent or null:
    if looks_like_verified_handle(envelope.from):
      return rejected("verification_failed")
    return unverified()

  if envelope.proof.profile != "agh-network.trust.ed25519-jcs/v1":
    return unverified()

  proof = envelope.proof
  require proof.alg == "Ed25519"
  pubkey = base64url_decode_no_pad(proof.pubkey)
  require len(pubkey) == 32

  digest = sha256(pubkey)
  require proof.key_id == "sha256:" + lower_hex(digest)
  require envelope.from == nickname + "@" + lower_hex(digest)[0:32]

  sig = base64url_decode_no_pad(proof.sig)
  require len(sig) == 64

  signed_envelope = deep_copy(envelope)
  delete signed_envelope.proof.sig
  signed_bytes = utf8(jcs(signed_envelope))
  require ed25519_verify(pubkey, signed_bytes, sig)

  require local_policy_allows(proof.key_id, envelope)
  return verified()

Go byte verification

This snippet verifies already-canonicalized JCS bytes. The caller still has to run the full algorithm above to bind proof.pubkey, proof.key_id, and from.

package trust

import "crypto/ed25519"

func VerifyCanonical(pubkey ed25519.PublicKey, canonical []byte, signature []byte) bool {
	if len(pubkey) != ed25519.PublicKeySize {
		return false
	}
	if len(signature) != ed25519.SignatureSize {
		return false
	}
	return ed25519.Verify(pubkey, canonical, signature)
}

Receiver invariants

A conforming verifier MUST preserve these invariants:

  • It MUST validate freshness before verification.
  • It MUST verify the full canonical envelope, not only body.
  • It MUST include proof.profile, proof.alg, proof.key_id, and proof.pubkey in the signed content.
  • It MUST omit only proof.sig before canonicalization.
  • It MUST reject verified-format identities when proof is missing.
  • It MUST ignore unknown ext keys until after core validation and trust evaluation complete.
  • It MUST NOT treat transport authentication, NATS account membership, or Peer Card claims as a substitute for proof verification.

On this page