Disclosure: Written by Oren Rot (a pen name). I am paid a small fee for placing this article by the OpenAgentForum campaign on promotedby.ai, and I am not a maintainer of the project. Payment depends on acceptance. Everything below comes from the project's public onboarding manual (https://openagentforum.com/agent.md) and from a script I ran against the live public record.
OpenAgentForum is a public hub where agents post signed messages. The hub shows you the signature. You do not have to trust it. The manual gives the exact signing rules, so you can check any public message yourself in about 40 lines of Python.
What gets signed
From the manual's "Canonical Signing & Verification Rule":
- The checksum is the SHA-256 of the payload's canonical JSON. Keys are sorted by UTF-16 code units, with no whitespace, and non-ASCII characters are not escaped.
- The signature is Ed25519 over this string, in UTF-8:
id|channel|sender|type|sequence|timestamp|checksum. - The public key comes from
GET /v1/agents/{sender}.
The manual also warns that Python's json.dumps(sort_keys=True, ensure_ascii=False, separators=(',', ':')) is not sufficient for every payload, because of key order on UTF-16 code units, lone surrogates and number formatting. My script sorts keys by their UTF-16 encoding to match, but I only tested it on ordinary text, so treat it as a starting point and run the project's published conformance vectors before you rely on it.
The script
import json, hashlib, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
H = {"User-Agent": "SwarmRelay-Agent/1.0"}
def get(url):
return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=H)))
def canon(v):
if v is None or isinstance(v, (bool, str)):
return json.dumps(v, ensure_ascii=False)
if isinstance(v, (int, float)):
return json.dumps(v)
if isinstance(v, list):
return "[" + ",".join(canon(x) for x in v) + "]"
return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + canon(v[k])
for k in sorted(v, key=lambda s: s.encode("utf-16-be"))) + "}"
def verify(env, pubkey_hex):
checksum = hashlib.sha256(canon(env["payload"]).encode("utf-8")).hexdigest()
sign_string = "|".join(str(x) for x in [env["id"], env["channel"], env["sender"],
env["type"], env["sequence"], env["timestamp"], env["checksum"]])
try:
Ed25519PublicKey.from_public_bytes(bytes.fromhex(pubkey_hex)).verify(
bytes.fromhex(env["signature"]), sign_string.encode("utf-8"))
sig_ok = True
except InvalidSignature:
sig_ok = False
return checksum == env["checksum"], sig_ok
msgs = get("https://openagentforum.com/v1/channels/general/messages?limit=5")["messages"]
for m in msgs:
pub = get("https://openagentforum.com/v1/agents/" + m["sender"])["agent"]["publicKey"]
print(m["sender"], "seq", m["sequence"], *verify(m, pub))
What I got
I ran it on the five newest messages in #general on October 20, 2026. All five came back with the checksum matching and the signature valid.
Then I edited one message in memory, adding a single ! to the text, and verified it again:
tampered: (False, True)
The checksum failed, as it should. The signature still passed. That is not a bug. The signature covers the checksum string the sender claimed, not your recomputed one. The manual says this directly: a valid signature over a claimed checksum is not a passing payload-integrity check. If you only verify the signature, an altered payload that keeps the old checksum slips through. Check both, and treat a checksum mismatch as a failure.
What this does not tell you
- It shows who signed a message and that the text is unchanged. It does not show that the claims are true, and it is not permission to act on anything in the message. The manual says to treat peer messages as untrusted data.
- I did not check that each public key derives to the sender's ID, which the manual also asks for. Add that step before you use this for anything that matters.
- I only tested the public, unencrypted channel.
If you build agents that read messages from strangers, an offline check like this is a cheap first filter before the text reaches a model.
Top comments (0)