{"public_key":"0139ad3724c251590c63e4ccccf557abde7e1a407685fd49fdf290f0ccc5a188","algo":"ed25519","domain":"groundcheck-attest-v1","key_mode":"persistent","message_format":"groundcheck-attest-v1:<kind>:<manifest_hash>","canonicalization":"manifest_hash = sha256 of json.dumps(manifest, sort_keys=True, separators=(',', ':'))","manifests":{"verify":"keys: backend (response.backend), claim_sha256 (sha256 hex of response.claim, utf-8), confidence, evidence_root (rolling commitment over ordered non-stub evidence content+stances, = response.provenance.evidence_root), route_hash (model-route commitment), model (response.classifier), signed_at (receipt.signed_at), source_urls (response.sources[].url, response order), verdict","check":"keys: backend, checked, claims ([{claim_sha256, verdict, confidence}] from response.report, in order), input_sha256 (attestation.input_sha256; sha256 hex of the submitted text), model (response.classifier), signed_at (receipt.signed_at)"},"howto":"Take the response JSON you were given. Rebuild the manifest for its kind from the fields above, hash it as canonical JSON (sorted keys, compact separators, sha256 hex), check it equals receipt.manifest_hash, then verify receipt.sig over the message 'groundcheck-attest-v1:<kind>:<manifest_hash>' with any Ed25519 library using receipt.public_key. No call to Groundcheck needed.","verify_example":"import hashlib, json\nfrom cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey\nresp = json.load(open(\"verify_response.json\"))  # the saved /verify response\nr = resp[\"attestation\"][\"receipt\"]\nPD = \"groundcheck-provenance-v1\"\ndef H(s): return hashlib.sha256(s.encode()).hexdigest()\n# evidence_root: rolling commitment over the ordered non-stub evidence\nroll = H(PD)\nfor i, s in enumerate(x for x in resp[\"sources\"] if not x.get(\"stub\")):\n    leaf = H(f\"{i}\\x1f{s.get('url','')}\\x1f{s.get('snippet','')}\\x1f{s.get('stance') or 'none'}\")\n    roll = H(roll + leaf)\nroute = {\"model\": resp.get(\"classifier\",\"\"), \"backend\": resp.get(\"backend\",\"\"),\n         \"ensembled\": str(resp.get(\"classifier\",\"\")).startswith(\"ensemble:\"),\n         \"decomposed\": bool(resp.get(\"atoms\")), \"n_atoms\": len(resp.get(\"atoms\") or []),\n         \"certified\": bool((resp.get(\"guarantee\") or {}).get(\"certified\"))}\nroute_hash = H(PD + \"\\x1f\".join(f\"{k}={route[k]}\" for k in sorted(route)))\nm = {\"claim_sha256\": H(resp[\"claim\"]), \"verdict\": resp[\"verdict\"],\n     \"confidence\": resp[\"confidence\"], \"source_urls\": [s[\"url\"] for s in resp[\"sources\"]],\n     \"evidence_root\": roll, \"route_hash\": route_hash,\n     \"model\": resp[\"classifier\"], \"backend\": resp[\"backend\"], \"signed_at\": r[\"signed_at\"]}\nh = hashlib.sha256(json.dumps(m, sort_keys=True, separators=(\",\", \":\")).encode()).hexdigest()\nassert h == r[\"manifest_hash\"], \"manifest was modified\"\nassert roll == resp[\"provenance\"][\"evidence_root\"], \"evidence path was tampered\"\nEd25519PublicKey.from_public_bytes(bytes.fromhex(r[\"public_key\"])).verify(\n    bytes.fromhex(r[\"sig\"]), f\"groundcheck-attest-v1:verify:{h}\".encode())"}