affaan-m/ECC · error · ContractError

receipt SHA-256 does not match its canonical content

Error message

receipt SHA-256 does not match its canonical content

What it means

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.

Solutions

  1. 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
  2. Regenerate the receipt via the tasteforge pipeline so signing is canonical
  3. If you edited receipt content intentionally, re-sign after every change — never patch content and digest independently
  4. Avoid reformatting/pretty-printing receipt files through external JSON tools without re-signing

Example fix

import hashlib, json
# before: digest stale after editing receipt fields
receipt['receipt_sha256'] = 'old...'
# after
payload = dict(receipt); payload.pop('receipt_sha256', None)
receipt['receipt_sha256'] = hashlib.sha256(
    json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')
).hexdigest()
Defensive patterns

Strategy: validation

Validate before calling

import hashlib, json
payload = dict(receipt); payload.pop('receipt_sha256', None)
actual = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()
assert receipt.get('receipt_sha256') == actual

Type guard

def receipt_digest_is_current(receipt: dict) -> bool:
    payload = {k: v for k, v in receipt.items() if k != 'receipt_sha256'}
    actual = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()
    return receipt.get('receipt_sha256') == actual

Try / catch

try:
    validate_artifact_receipt(out_dir)
except ContractError as e:
    if 'receipt SHA-256 does not match' in str(e):
        re_sign_receipt(receipt)  # recompute canonical digest over full payload
    else:
        raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/90d77b7b17b7cb6b. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/contract.py:441

            if basis == "whole_file" and times:
                raise ContractError(f"artifact {relative} whole-file provenance must not invent times")
            if basis == "media_seconds":
                expected_duration = source_durations.get((source["reference_path"], digest))
                if expected_duration is None or source.get("source_duration") != expected_duration:
                    raise ContractError(f"artifact {relative} has an unbound source duration")
                for time in times:
                    _validate_media_time(
                        time, expected_duration,
                        label=f"artifact {relative} media reference time",
                    )

    digest_payload = dict(receipt)
    claimed_digest = digest_payload.pop("receipt_sha256", None)
    actual_digest = hashlib.sha256(
        json.dumps(digest_payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
    ).hexdigest()
    if claimed_digest != actual_digest:
        raise ContractError("receipt SHA-256 does not match its canonical content")


def validate_bundle(out_dir: str | Path) -> None:
    """Validate required multimodal files and cross-artifact invariants."""
    out_dir = Path(out_dir)
    _validate_output_tree(out_dir)
    specs = [json.loads(path.read_text(encoding="utf-8"))
             for path in sorted((out_dir / "genres").glob("*.json"))]
    validate_genre_specs(specs)

    validate_manifests(out_dir / "manifests")
    provenance_path = out_dir / "provenance.json"
    if not provenance_path.is_file():
        raise ContractError("missing provenance")
    validate_provenance(json.loads(provenance_path.read_text(encoding="utf-8")))

    receipt_path = out_dir / "receipt.json"
    if not receipt_path.is_file():

View on GitHub (pinned to 8321021c54)