#!/usr/bin/env python3 """Verify a Predge signed record offline (Python 3.8+). usage: python3 verify_record.py FILE [--keys FILE_OR_URL] [--offline] FILE is a full API response (the envelope is under "attestation"), a bare envelope, or an evidence pack (the envelope fields at the top level). Checks, in order: 1. canonical rebuilt from payload equals canonical (CANONICALIZATION.md) 2. content_hash = sha256(canonical), when the file carries content_hash 3. ed25519 signature over canonical 4. envelope_signature, when present 5. public_key is listed and active in predge-keys.json (skipped with --offline) Exit status 0 only when every check that ran passed. Signature checks need `cryptography` or `PyNaCl`; without either they are reported as skipped and the exit status is 2. """ import hashlib import json import sys import urllib.request KEYS_URL = "https://api.predge.io/.well-known/predge-keys.json" def canonical(value): """The Predge rule: sorted keys at every level, no whitespace, UTF-8, no ASCII escaping.""" return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) def envelope_of(doc): if isinstance(doc.get("attestation"), dict): return doc["attestation"] return doc def ed25519_verify(pub_hex, sig_hex, message): try: from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey try: Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub_hex)).verify(bytes.fromhex(sig_hex), message) return True except (InvalidSignature, ValueError): return False except ImportError: pass try: from nacl.exceptions import BadSignatureError from nacl.signing import VerifyKey try: VerifyKey(bytes.fromhex(pub_hex)).verify(message, bytes.fromhex(sig_hex)) return True except (BadSignatureError, ValueError): return False except ImportError: return None def load_keys(src): if src.startswith("http://") or src.startswith("https://"): with urllib.request.urlopen(src, timeout=10) as r: return json.load(r) with open(src, encoding="utf-8") as f: return json.load(f) def main(argv): if not argv or argv[0].startswith("-"): print(__doc__) return 64 path, keys_src, offline = argv[0], KEYS_URL, "--offline" in argv if "--keys" in argv: keys_src = argv[argv.index("--keys") + 1] with open(path, encoding="utf-8") as f: env = envelope_of(json.load(f)) results = [] canon = env["canonical"] results.append(("payload rebuilds canonical", canonical(env["payload"]) == canon)) digest = hashlib.sha256(canon.encode("utf-8")).hexdigest() if "content_hash" in env: results.append(("content_hash = sha256(canonical)", env["content_hash"] == digest)) results.append(("signature over canonical", ed25519_verify(env["public_key"], env["signature"], canon.encode("utf-8")))) if "envelope_signature" in env: rest = {k: v for k, v in env.items() if k != "envelope_signature"} results.append(("envelope_signature", ed25519_verify(env["public_key"], env["envelope_signature"], canonical(rest).encode("utf-8")))) if not offline: try: keys = load_keys(keys_src).get("keys", []) except Exception as e: # network or TLS trouble: say so, do not pass the check print(f"could not read the key set from {keys_src}: {e}; download it and pass --keys FILE") keys = [] listed = any(k.get("active") and str(k.get("public_key", "")).lower() == env["public_key"].lower() for k in keys) results.append(("key listed and active in predge-keys.json", listed)) print("content_hash (sha256 of canonical):", digest) print("kid:", env["public_key"][:16]) for name, ok in results: print(f"{'PASS' if ok else ('SKIP' if ok is None else 'FAIL')} {name}") if any(ok is False for _, ok in results): return 1 if any(ok is None for _, ok in results): print("install `cryptography` or `PyNaCl` to check signatures") return 2 return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))