What it does
Run the file below with the LLPRO Certified public key and the JSON a certified_lookup call returned. It checks the signature on the observation, that the events are the ones signed, every event's own hash and the chain between them, and that the stated result (issued, not_found, revoked, expired) follows from the events at the time of the lookup. No call to our server.
- Where it sits
- Agents › Trust › Certify
- Who it is for
- Anyone handed a LLPRO Certified answer who does not want to take our word for it: buyers, platforms, auditors.
- Price
- Free. No account.
- Limits
- It does not show that a product is safe, legal or fit for any purpose, or that the register was not rolled back to an older state by whoever runs it: a signed answer proves what the register held when it was signed, and keeping the answers you rely on is how you would notice. The public key is on https://llpro.ai/certified. Vector B below uses a throwaway key on purpose: it is a made-up product, and no real key signed a demo certificate.
For developers: try it
Offline verifier (certified_verify.py):
#!/usr/bin/env python3
# usage: python3 certified_verify.py LLPRO_CERTIFIED_PUBLIC_KEY answer.json (needs: pip install cryptography)
# answer.json is what certified_lookup returned (the whole JSON). No call to our server.
import base64
import calendar
import hashlib
import json
import re
import sys
import time
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
ZERO = "0" * 64
def canon(o):
return json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def sha(o):
return hashlib.sha256(canon(o).encode("utf-8")).hexdigest()
def utc(s):
if not isinstance(s, str) or not re.fullmatch(r"\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ", s):
raise ValueError("bad time")
return calendar.timegm(time.strptime(s, "%Y-%m-%dT%H:%M:%SZ"))
def verify(public_key_b64, ans):
"""-> 'PASS: ...' or 'FAIL: ...'. Checks (1) the LLPRO Certified signature over the observation, (2) the events against events_sha256, (3) every event's own hash and the chain between them
(and from the start of the register, and to its head, when the whole list was returned), (4) that the stated result follows from the events at the time of the lookup."""
try:
raw = base64.b64decode(public_key_b64, validate=True)
if len(raw) != 32:
return "FAIL: public key must be 32 bytes in base64"
obs, sig, evs = ans["observation"], ans["signature"], ans["events"]
if not isinstance(obs, dict) or not isinstance(evs, list):
return "FAIL: not a certified_lookup answer"
signer = "llpro1:" + base64.b32encode(hashlib.sha256(raw).digest()[:16]).decode().lower().rstrip("=")
if obs.get("signer") != signer:
return "FAIL: the answer names a different signer (%s) than this key (%s)" % (obs.get("signer"), signer)
for k in ("subject", "result", "cert_id", "register", "list_complete"):
if k in ans and ans[k] != obs.get(k):
return "FAIL: the unsigned top-level %s differs from the signed observation; read only the signed one" % k
try:
Ed25519PublicKey.from_public_bytes(raw).verify(base64.b64decode(sig, validate=True), canon(obs).encode("utf-8"))
except (InvalidSignature, ValueError):
return "FAIL: signature does not verify with this key"
if obs.get("events_sha256") != sha(evs):
return "FAIL: events were changed after signing"
reg = obs["register"]
prev = None
for e in evs:
if not isinstance(e, dict) or e.get("hash") != sha({k: v for k, v in e.items() if k != "hash"}):
return "FAIL: an event does not match its own hash"
if prev is not None and e.get("seq") == prev["seq"] + 1 and e.get("prev") != prev["hash"]:
return "FAIL: chain broken between events %s and %s" % (prev["seq"], e["seq"])
prev = e
whole = obs.get("type") == "certified-register" and obs.get("list_complete")
if whole:
for i, e in enumerate(evs):
if e["seq"] != i + 1 or e["prev"] != (ZERO if i == 0 else evs[i - 1]["hash"]):
return "FAIL: the register list is not a complete chain from the start"
if (evs[-1]["hash"] if evs else ZERO) != reg["head"] or len(evs) != reg["events"]:
return "FAIL: the list does not end at the signed head"
open_, rev = {}, set()
for e in evs:
if e["type"] == "issue":
open_[e["cert_id"]] = e
elif e["type"] == "revoke":
rev.add(e["cert_id"])
ni = sum(1 for k, e in open_.items() if k not in rev and utc(e["expires_at"]) > obs["epoch"])
nx = sum(1 for k, e in open_.items() if k not in rev and utc(e["expires_at"]) <= obs["epoch"])
if (reg["issued"], reg["revoked"], reg["expired"]) != (ni, len(rev), nx):
return "FAIL: the register counts do not follow from the events"
if obs.get("type") == "certified-lookup":
now, sub = obs["epoch"], obs["subject"]
mine = [e for e in evs if e.get("subject") == sub]
issues = [e for e in mine if e.get("type") == "issue"]
if not issues:
want, cid = "not_found", None
else:
cur = issues[-1]
cid = cur["cert_id"]
if any(e.get("type") == "revoke" and e.get("cert_id") == cid for e in mine):
want = "revoked"
else:
want = "issued" if utc(cur["expires_at"]) > now else "expired"
if obs.get("result") != want or obs.get("cert_id") != cid:
return "FAIL: the stated result does not follow from the events (%s)" % want
return "PASS (as of %s, NOT necessarily current: look up again before relying on it; signer %s): signed by this key; events match the signature and chain; result %s; register head %s, %s events, %s issued" % (
obs["looked_at"], signer, obs.get("result", "(register)"), reg["head"][:16], reg["events"], reg["issued"])
except (KeyError, TypeError, ValueError, AttributeError):
return "FAIL: not a well-formed answer"
def load(p):
with open(p, encoding="utf-8") as f:
return json.load(f)
if __name__ == "__main__":
a = sys.argv[1:]
print(verify(a[0], load(a[1])) if len(a) == 2 else "usage: python3 certified_verify.py LLPRO_CERTIFIED_PUBLIC_KEY answer.json")
Test vector A: real LLPRO Certified key (crwGhTRF43+ydCES1TE2rPGKSpLQnFEagHMgZsVAdxI=); a true not_found answer from the real register. Save as a.json and run: python3 certified_verify.py crwGhTRF43+ydCES1TE2rPGKSpLQnFEagHMgZsVAdxI= a.json
{"observation": {"v": 1, "type": "certified-lookup", "register": {"events": 0, "head": "0000000000000000000000000000000000000000000000000000000000000000", "issued": 0, "revoked": 0, "expired": 0, "criteria_hash": "d72f1964c7196dbb495412b18438ea7b5d6c7a8f4ed984b10603a5a0d6886472", "scheme": "LLPRO Certified v1"}, "looked_at": "2026-11-13T05:33:09Z", "epoch": 1794547989, "signer": "llpro1:a5fyinggks4g4stnncnpcfeihe", "subject": "vector-demo-product", "result": "not_found", "cert_id": null, "events_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945"}, "signature": "wSJHrK/w0dKWRS3OLy5Fo27eQIcVQsO1b2raBBrDL/3lmV6FyhaXIQD3ixRfOXR+XecYah+C6StuvPR48w3FBQ==", "events": []}
Test vector B (made-up product, THROWAWAY public key 3lvT2og7ETdV82MZdlFvGuP8zFEf0cVHNqGqDZgqMkY=): an issued answer, then a revoked one. Save as b1.json / b2.json.
{"observation": {"v": 1, "type": "certified-lookup", "register": {"events": 1, "head": "24851b32d38590696e271e7fd06cdc31c027aa34f6a1a889015f0a210341f7e6", "issued": 1, "revoked": 0, "expired": 0, "criteria_hash": "d72f1964c7196dbb495412b18438ea7b5d6c7a8f4ed984b10603a5a0d6886472", "scheme": "LLPRO Certified v1"}, "looked_at": "2026-11-13T05:33:10Z", "epoch": 1794547990, "signer": "llpro1:vcb5dsm7jmmw2srgjrhvitcxpa", "subject": "vector-demo-product", "result": "issued", "cert_id": "ce-0a2e7504ab2b3f57", "events_sha256": "d7be018004fa332e8597f0818e3f2140ad135992114552584733438db3c0e846"}, "signature": "gRG7JpH07co1RrYgKbf/QvJe1tn2rl2veP9ECyh62ks1Xx7kh8AF/UYxDhrTfIepjDbBRFpBJtWwZMRS7eiDBw==", "events": [{"type": "issue", "cert_id": "ce-0a2e7504ab2b3f57", "subject": "vector-demo-product", "level": "LLPRO Certified v1", "scope": "DEMO ONLY: a made-up product for the LLPRO Certified test vector. Nothing real is certified.", "expires_at": "2027-09-09T05:33:09Z", "criteria_hash": "d72f1964c7196dbb495412b18438ea7b5d6c7a8f4ed984b10603a5a0d6886472", "assessment_sha256": "7e52ecf000a5ce4b32095f754fa2e52db990d6f3c284dee2313f1e788dfeea35", "builder": "demo-builder", "assessor": "demo-assessor", "reviewer": "demo-reviewer", "seq": 1, "prev": "0000000000000000000000000000000000000000000000000000000000000000", "at": "2026-11-13T05:33:09Z", "hash": "24851b32d38590696e271e7fd06cdc31c027aa34f6a1a889015f0a210341f7e6"}]}
{"observation": {"v": 1, "type": "certified-lookup", "register": {"events": 2, "head": "11f1ecf2ce1ad69f040fe7561acc3c73ecef9ca6121e4aa636a4c89eb90bf8cf", "issued": 0, "revoked": 1, "expired": 0, "criteria_hash": "d72f1964c7196dbb495412b18438ea7b5d6c7a8f4ed984b10603a5a0d6886472", "scheme": "LLPRO Certified v1"}, "looked_at": "2026-11-13T05:33:12Z", "epoch": 1794547992, "signer": "llpro1:vcb5dsm7jmmw2srgjrhvitcxpa", "subject": "vector-demo-product", "result": "revoked", "cert_id": "ce-0a2e7504ab2b3f57", "events_sha256": "1cc8faed1a4e54c1b50498b31e63e4f0c256fa55fe0f693edc79f7fe91ae5e3b"}, "signature": "kNIRR1soA8w/MqU5BQzjYfphD0jvFin1sOzrzqYdck7L0zEvFKzNrGXmKBoogT/fiQSBcZcbOIPm/Go3Mm+jDA==", "events": [{"type": "issue", "cert_id": "ce-0a2e7504ab2b3f57", "subject": "vector-demo-product", "level": "LLPRO Certified v1", "scope": "DEMO ONLY: a made-up product for the LLPRO Certified test vector. Nothing real is certified.", "expires_at": "2027-09-09T05:33:09Z", "criteria_hash": "d72f1964c7196dbb495412b18438ea7b5d6c7a8f4ed984b10603a5a0d6886472", "assessment_sha256": "7e52ecf000a5ce4b32095f754fa2e52db990d6f3c284dee2313f1e788dfeea35", "builder": "demo-builder", "assessor": "demo-assessor", "reviewer": "demo-reviewer", "seq": 1, "prev": "0000000000000000000000000000000000000000000000000000000000000000", "at": "2026-11-13T05:33:09Z", "hash": "24851b32d38590696e271e7fd06cdc31c027aa34f6a1a889015f0a210341f7e6"}, {"type": "revoke", "cert_id": "ce-0a2e7504ab2b3f57", "subject": "vector-demo-product", "reason": "issued_in_error", "note": "demo revoke", "seq": 2, "prev": "24851b32d38590696e271e7fd06cdc31c027aa34f6a1a889015f0a210341f7e6", "at": "2026-11-13T05:33:11Z", "hash": "11f1ecf2ce1ad69f040fe7561acc3c73ecef9ca6121e4aa636a4c89eb90bf8cf"}]}
Expected: PASS lines for all three; a changed byte gives FAIL.For agents
Tool certified_lookup on https://llpro.ai/mcp and in the tools list. Signed answers are checked with the public key below (signer llpro1:a5fyinggks4g4stnncnpcfeihe): an Ed25519 signature over the observation as canonical JSON (sorted keys, no spaces).
Public key (base64): crwGhTRF43+ydCES1TE2rPGKSpLQnFEagHMgZsVAdxI=