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
| State | Meaning | Receiver behavior |
|---|---|---|
verified | A supported proof profile validated successfully and local policy permits the sender. | Receiver MAY treat the envelope as cryptographically bound to from. |
unverified | No usable proof is present, or the proof profile is unsupported but not malformed. | Receiver MAY route the message under unverified policy. |
rejected | The 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...
Verification algorithm
To mark an envelope as verified under the baseline profile, a receiver MUST perform every step in
this order:
- Confirm that
proofis a JSON object. - Confirm that
proof.profileequalsagh-network.trust.ed25519-jcs/v1. - Confirm that
proof.algequalsEd25519. - Confirm that
proof.pubkeyis base64url without padding and decodes to exactly 32 bytes. - Compute
digest = SHA-256(pubkey). - Confirm that
proof.key_idequalssha256:followed by the 64-character lowercase hex digest. - Confirm that
fromhas the verified handle shapenickname@fingerprint. - Confirm that
nicknamematches[a-z0-9_-]{1,32}. - Confirm that
fingerprintequals the first 32 lowercase hex characters of the digest. - Confirm that
proof.sigis base64url without padding and decodes to exactly 64 bytes. - Omit only
proof.sigfrom the received envelope. - JCS-canonicalize the resulting envelope.
- Verify the Ed25519 signature over the canonical UTF-8 bytes using
proof.pubkey. - Apply local trust policy, if configured.
If any required step fails, the receiver MUST classify the envelope as rejected.
Rendering diagram...
Key resolution
The baseline trust profile is self-contained. A receiver MUST be able to verify the cryptographic
signature from proof.pubkey alone.
| Source | Role |
|---|---|
proof.pubkey | Supplies the raw Ed25519 public key for signature verification. |
proof.key_id | Binds the public key to its full SHA-256 digest. |
from fingerprint | Binds the sender handle to the first 32 lowercase hex characters of the same digest. |
| Local policy | MAY 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 condition | Trust state | Rule |
|---|---|---|
proof absent or null, from is a normal v0 Peer ID | unverified | Receiver MAY route under unverified policy. |
proof absent or null, from matches nickname@fingerprint | rejected | Receiver MUST treat this as proof stripping. |
proof.profile is unsupported but the proof object is otherwise opaque and well-formed | unverified | Receiver MAY route under unverified or unsupported-profile policy. |
Baseline proof is present but malformed | rejected | Receiver MUST stop normal routing. |
Baseline proof is present but signature verification fails | rejected | Receiver 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:
- A sender emits a signed envelope from
patch-worker@56475aa75463474c0285df5dbf2bcab7. - An attacker removes
proof. - A receiver sees a verified-format identity without proof.
- 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.
| Failure | Recommended reason code | Notes |
|---|---|---|
| Invalid baseline signature, malformed baseline proof, key mismatch, or fingerprint mismatch | verification_failed | Use when the sender attempted the baseline profile and failed. |
| Unsupported proof profile | unsupported_profile | Use when the receiver chooses to reject instead of treating the message as unverified. |
Non-object proof or invalid envelope shape | malformed | Core envelope validation can fail before trust evaluation. |
| Message expired before trust evaluation | expired | Freshness checks happen before trust checks. |
| Local policy forbids a valid key | verification_failed or deployment-specific namespaced code | Use 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, andproof.pubkeyin the signed content. - It MUST omit only
proof.sigbefore canonicalization. - It MUST reject verified-format identities when proof is missing.
- It MUST ignore unknown
extkeys 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.