{"record":{"id":"90d77b7b17b7cb6b","repo":"affaan-m/ECC","slug":"receipt-sha-256-does-not-match-its-canonical-content","errorCode":null,"errorMessage":"receipt SHA-256 does not match its canonical content","messagePattern":"receipt SHA-256 does not match its canonical content","errorType":"exception","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":441,"sourceCode":"            if basis == \"whole_file\" and times:\n                raise ContractError(f\"artifact {relative} whole-file provenance must not invent times\")\n            if basis == \"media_seconds\":\n                expected_duration = source_durations.get((source[\"reference_path\"], digest))\n                if expected_duration is None or source.get(\"source_duration\") != expected_duration:\n                    raise ContractError(f\"artifact {relative} has an unbound source duration\")\n                for time in times:\n                    _validate_media_time(\n                        time, expected_duration,\n                        label=f\"artifact {relative} media reference time\",\n                    )\n\n    digest_payload = dict(receipt)\n    claimed_digest = digest_payload.pop(\"receipt_sha256\", None)\n    actual_digest = hashlib.sha256(\n        json.dumps(digest_payload, sort_keys=True, separators=(\",\", \":\")).encode(\"utf-8\")\n    ).hexdigest()\n    if claimed_digest != actual_digest:\n        raise ContractError(\"receipt SHA-256 does not match its canonical content\")\n\n\ndef validate_bundle(out_dir: str | Path) -> None:\n    \"\"\"Validate required multimodal files and cross-artifact invariants.\"\"\"\n    out_dir = Path(out_dir)\n    _validate_output_tree(out_dir)\n    specs = [json.loads(path.read_text(encoding=\"utf-8\"))\n             for path in sorted((out_dir / \"genres\").glob(\"*.json\"))]\n    validate_genre_specs(specs)\n\n    validate_manifests(out_dir / \"manifests\")\n    provenance_path = out_dir / \"provenance.json\"\n    if not provenance_path.is_file():\n        raise ContractError(\"missing provenance\")\n    validate_provenance(json.loads(provenance_path.read_text(encoding=\"utf-8\")))\n\n    receipt_path = out_dir / \"receipt.json\"\n    if not receipt_path.is_file():","sourceCodeStart":423,"sourceCodeEnd":459,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L423-L459","documentation":"The receipt itself is self-verifying: receipt_sha256 must equal the SHA-256 of the receipt's canonical JSON (all other keys, sorted keys, compact separators). This error is raised when the claimed digest does not match that recomputation, meaning the receipt's content was altered after signing or the digest was computed non-canonically.","triggerScenarios":"Editing any receipt field (artifacts, provenance, durations) after the digest was written; computing the digest with json.dumps defaults (spaces, insertion order) instead of sort_keys=True with separators=(',',':'); round-tripping the receipt through a tool that reorders or reformats content.","commonSituations":"Hand-editing receipts to fix a field without re-signing, scripts that pretty-print JSON then reuse the old digest, keys added/removed by schema migrations, digest computed over a Python dict before a key pop of receipt_sha256.","solutions":["Recompute the digest over the receipt minus receipt_sha256 using json.dumps(payload, sort_keys=True, separators=(',',':')).encode('utf-8'), hash with sha256, and write the hex digest back","Regenerate the receipt via the tasteforge pipeline so signing is canonical","If you edited receipt content intentionally, re-sign after every change — never patch content and digest independently","Avoid reformatting/pretty-printing receipt files through external JSON tools without re-signing"],"exampleFix":"import hashlib, json\n# before: digest stale after editing receipt fields\nreceipt['receipt_sha256'] = 'old...'\n# after\npayload = dict(receipt); payload.pop('receipt_sha256', None)\nreceipt['receipt_sha256'] = hashlib.sha256(\n    json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')\n).hexdigest()","handlingStrategy":"validation","validationCode":"import hashlib, json\npayload = dict(receipt); payload.pop('receipt_sha256', None)\nactual = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()\nassert receipt.get('receipt_sha256') == actual","typeGuard":"def receipt_digest_is_current(receipt: dict) -> bool:\n    payload = {k: v for k, v in receipt.items() if k != 'receipt_sha256'}\n    actual = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()\n    return receipt.get('receipt_sha256') == actual","tryCatchPattern":"try:\n    validate_artifact_receipt(out_dir)\nexcept ContractError as e:\n    if 'receipt SHA-256 does not match' in str(e):\n        re_sign_receipt(receipt)  # recompute canonical digest over full payload\n    else:\n        raise","preventionTips":["Re-sign the receipt after every content change — content and digest together","Always sign with sort_keys=True and compact separators; no pretty-printed digests","Sign as the last step of receipt generation, after all fields are final","Never round-trip receipts through reformatting tools without re-signing"],"tags":["checksum","tampering","canonical-json"],"backgroundTag":"checksum-mismatch","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}