|
|
| """
|
| PAX-Coder Post-Quantum Clone Integrity Verification
|
|
|
| Upgrades verify-clone to ML-DSA-44 (CRYSTALS-Dilithium, NIST FIPS 204).
|
|
|
| Ed25519 signatures on release manifests are vulnerable to harvest-now-
|
| decrypt-later attacks by quantum adversaries. This script verifies the
|
| release manifest signature using ML-DSA-44 β Shor-resistant, 128-bit
|
| post-quantum security.
|
|
|
| Exit codes:
|
| 0 = Integrity verified (ML-DSA-44 signature valid)
|
| 1 = Integrity verification failed
|
| 2 = Script error
|
|
|
| Requirements:
|
| pip install dilithium-py pydantic
|
|
|
| Environment:
|
| PAX_MLDSA_PUBLIC_KEY_HEX β authority ML-DSA-44 public key (2624 hex chars)
|
| OR sovereign/mldsa_authority.pub
|
|
|
| Authors: Ahmad Ali Parr, Jessica L. Williams (SNAPKITTYWEST)
|
| """
|
|
|
| import hashlib
|
| import json
|
| import os
|
| import subprocess
|
| import sys
|
| from pathlib import Path
|
| from typing import Optional
|
|
|
|
|
| MLDSA_PK_BYTES = 1312
|
| MLDSA_SIG_BYTES = 2420
|
| MLDSA_PK_HEX = MLDSA_PK_BYTES * 2
|
| MLDSA_SIG_HEX = MLDSA_SIG_BYTES * 2
|
|
|
| EXIT_OK = 0
|
| EXIT_FAILED = 1
|
| EXIT_ERROR = 2
|
|
|
|
|
|
|
| def mldsa_verify(pk: bytes, msg: bytes, sig: bytes) -> bool:
|
| try:
|
| from dilithium_py.dilithium import Dilithium2
|
| return Dilithium2.verify(pk, msg, sig)
|
| except ImportError:
|
| pass
|
|
|
| binary = os.environ.get("PAX_MLDSA_VERIFY_BIN", "pax_verify_mldsa")
|
| try:
|
| import tempfile
|
| with tempfile.TemporaryDirectory() as d:
|
| pk_p = Path(d) / "pk.bin"; pk_p.write_bytes(pk)
|
| msg_p = Path(d) / "msg.bin"; msg_p.write_bytes(msg)
|
| sig_p = Path(d) / "sig.bin"; sig_p.write_bytes(sig)
|
| r = subprocess.run([binary, str(pk_p), str(msg_p), str(sig_p)],
|
| capture_output=True, timeout=10)
|
| return r.returncode == 0
|
| except Exception:
|
| return False
|
|
|
|
|
|
|
| def sha256_file(path: Path) -> str:
|
| return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
| def sha256_dir(repo_root: Path) -> str:
|
| """Deterministic SHA-256 of all tracked files (git ls-files order)."""
|
| try:
|
| r = subprocess.run(["git", "ls-files", "-z"],
|
| capture_output=True, cwd=repo_root, timeout=30)
|
| files = [f for f in r.stdout.split(b"\x00") if f]
|
| except Exception:
|
| return ""
|
| h = hashlib.sha256()
|
| for f in sorted(files):
|
| path = repo_root / f.decode()
|
| if path.is_file():
|
| h.update(f + b"\x00")
|
| h.update(path.read_bytes())
|
| return h.hexdigest()
|
|
|
| def git_head(repo_root: Path) -> Optional[str]:
|
| try:
|
| r = subprocess.run(["git", "rev-parse", "HEAD"],
|
| capture_output=True, text=True, cwd=repo_root, timeout=10)
|
| return r.stdout.strip() if r.returncode == 0 else None
|
| except Exception:
|
| return None
|
|
|
|
|
|
|
| def verify(repo_root: Path) -> int:
|
| sovereign = repo_root / "sovereign"
|
|
|
| print("========================================")
|
| print("PAX-CODER PQ CLONE VERIFICATION")
|
| print("ML-DSA-44 / NIST FIPS 204")
|
| print("========================================")
|
| print()
|
|
|
|
|
| print("[1] Reading release metadata...")
|
| release_file = sovereign / "release.json"
|
| if not release_file.exists():
|
| print(" ERROR: sovereign/release.json not found")
|
| return EXIT_FAILED
|
| try:
|
| release = json.loads(release_file.read_text())
|
| except json.JSONDecodeError as e:
|
| print(f" ERROR: Cannot parse release.json: {e}")
|
| return EXIT_ERROR
|
| print(f" Repository: {release.get('repository','')}")
|
| print(f" Version: {release.get('release_version','')}")
|
| print(f" Timestamp: {release.get('release_timestamp_utc','')}")
|
| print(" OK")
|
| print()
|
|
|
|
|
| print("[2] Verifying git commit...")
|
| current = git_head(repo_root)
|
| if current is None:
|
| print(" ERROR: Not a git repository")
|
| return EXIT_ERROR
|
| expected = release.get("git_commit", "")
|
| if current != expected:
|
| print(f" FAILED: Commit mismatch")
|
| print(f" Expected: {expected}")
|
| print(f" Current: {current}")
|
| return EXIT_FAILED
|
| print(f" OK: {current[:12]}")
|
| print()
|
|
|
|
|
| print("[3] Verifying manifest hash...")
|
| manifest_file = sovereign / "manifest.json"
|
| expected_hash = release.get("manifest_sha256", "")
|
| if manifest_file.exists() and expected_hash:
|
| computed = sha256_file(manifest_file)
|
| if computed != expected_hash:
|
| print(f" FAILED: Manifest hash mismatch")
|
| print(f" Expected: {expected_hash}")
|
| print(f" Computed: {computed}")
|
| return EXIT_FAILED
|
| print(f" OK: {computed[:16]}...")
|
| else:
|
| print(" SKIP: No manifest.json or no expected hash")
|
| print()
|
|
|
|
|
| print("[4] Verifying tracked file tree...")
|
| expected_tree = release.get("tree_sha256", "")
|
| if expected_tree:
|
| computed_tree = sha256_dir(repo_root)
|
| if computed_tree != expected_tree:
|
| print(" FAILED: File tree hash mismatch β files may have been tampered")
|
| return EXIT_FAILED
|
| print(f" OK: {computed_tree[:16]}...")
|
| else:
|
| print(" SKIP: No tree_sha256 in release.json")
|
| print()
|
|
|
|
|
| print("[5] Verifying ML-DSA-44 release signature...")
|
| sig_file = sovereign / "release.sig.hex"
|
| sig_hex = os.environ.get("PAX_RELEASE_SIG_HEX", "")
|
| if not sig_hex and sig_file.exists():
|
| sig_hex = sig_file.read_text().strip()
|
| if not sig_hex:
|
| print(" SKIP: No ML-DSA-44 release signature found")
|
| print(" Set PAX_RELEASE_SIG_HEX or create sovereign/release.sig.hex")
|
| print(" (Clone integrity cannot be fully verified without signature)")
|
| print()
|
| print("========================================")
|
| print("STATUS: PARTIAL β commit + hash verified")
|
| print(" ML-DSA-44 signature not present")
|
| print("========================================")
|
| return EXIT_OK
|
|
|
| pk_hex = os.environ.get("PAX_MLDSA_PUBLIC_KEY_HEX", "")
|
| if not pk_hex:
|
| pk_file = sovereign / "mldsa_authority.pub"
|
| if pk_file.exists():
|
| pk_hex = pk_file.read_text().strip()
|
| if not pk_hex:
|
| print(" ERROR: No ML-DSA-44 authority public key")
|
| print(" Set PAX_MLDSA_PUBLIC_KEY_HEX or create sovereign/mldsa_authority.pub")
|
| return EXIT_ERROR
|
|
|
| if len(sig_hex) != MLDSA_SIG_HEX:
|
| print(f" ERROR: Signature must be {MLDSA_SIG_HEX} hex chars (ML-DSA-44)")
|
| return EXIT_FAILED
|
|
|
|
|
| message = json.dumps(release, separators=(",",":"), sort_keys=True).encode()
|
|
|
| try:
|
| pk = bytes.fromhex(pk_hex)
|
| sig = bytes.fromhex(sig_hex)
|
| except ValueError:
|
| print(" ERROR: Non-hex characters in key or signature")
|
| return EXIT_ERROR
|
|
|
| if mldsa_verify(pk, message, sig):
|
| print(" OK: ML-DSA-44 signature VALID")
|
| else:
|
| print(" FAILED: ML-DSA-44 signature INVALID")
|
| print(" This clone may have been tampered with or is unsigned")
|
| return EXIT_FAILED
|
| print()
|
|
|
| print("========================================")
|
| print("STATUS: AUTHENTIC PAX-CODER RELEASE")
|
| print(" ML-DSA-44 verified (FIPS 204)")
|
| print("========================================")
|
| return EXIT_OK
|
|
|
|
|
| if __name__ == "__main__":
|
| import argparse
|
| parser = argparse.ArgumentParser()
|
| parser.add_argument("--repo-root", type=Path,
|
| default=Path(__file__).resolve().parent)
|
| args = parser.parse_args()
|
| sys.exit(verify(args.repo_root))
|
|
|