Distributed Systems & CryptographyNovember 3, 2026

Building a Proof-of-Transit for Cold-Storage Shipments with Hash-Locked Location Receipts

C

Written by

Cipher Stone

The problem I wanted to solve: “Who touched the box, and when?”

I got curious about a very specific failure mode in digital provenance: cold-storage shipments (think reagents, vaccines, or high-value meds) often have temperature logs and chain-of-custody paperwork, but the “digital story” is usually stitched together after the fact.

What I wanted was a mechanism where each custody handoff produces a tamper-evident, cryptographically bound “receipt” that proves:

  • a designated device was present (via a signature),
  • the receipt corresponds to a specific immutable shipment state (via a commitment),
  • and the handoff time is plausibly captured without trusting any single party.

So I built a tiny “proof-of-transit” prototype that uses hash-locked location receipts: each handoff generates a receipt that links to the previous receipt by hashing the chain forward, and it binds the receipt to the destination identity using a hash lock.

Below is exactly what I implemented and how it works.


The core idea: hash-locked receipt chains

A hash function maps any data to a fixed-size digest (fingerprint). If you change the data, the fingerprint changes unpredictably.

I used a chain of receipts where each new receipt includes:

  • prev_digest: the SHA-256 digest of the previous receipt (so edits break the chain),
  • device_id: the identifier of the custody device (e.g., scanner at a warehouse),
  • timestamp: captured when the receipt is created,
  • location_hint: a human-readable label like "COLD-DOCK-3",
  • shipment_state_commitment: a commitment to the shipment’s current immutable state,
  • destination_lock: a hash lock derived from the destination’s secret key material (so only the intended endpoint can later validate certain facts).

Each receipt is then digitally signed by the device’s private key.

Why hash locks?

A hash lock is a way to make “the right secret” become verifiable only when the chain reaches the right place. In my prototype, the destination endpoint later verifies that the lock matches expected material, proving the receipt chain reached the intended custody flow.


What I implemented (end-to-end)

I wrote a small Python program that:

  1. Generates device keypairs (ECDSA).
  2. Creates an initial “genesis” receipt for a shipment.
  3. Creates successive receipts for handoffs.
  4. Verifies the full chain and the hash-lock constraints.

This is not a production blockchain—just the provenance primitive you can integrate into an existing system.


Working prototype code (Python)

Install dependencies

pip install cryptography

The full program

import base64 import hashlib import json from dataclasses import dataclass from datetime import datetime, timezone from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature, decode_dss_signature from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.exceptions import InvalidSignature def sha256(data: bytes) -> bytes: return hashlib.sha256(data).digest() def b64(b: bytes) -> str: return base64.b64encode(b).decode("ascii") def unb64(s: str) -> bytes: return base64.b64decode(s.encode("ascii")) def now_utc_iso() -> str: return datetime.now(timezone.utc).isoformat() def generate_ec_keypair(): private_key = ec.generate_private_key(ec.SECP256R1()) public_key = private_key.public_key() return private_key, public_key def pubkey_bytes(public_key) -> bytes: return public_key.public_bytes(Encoding.X962, PublicFormat.UncompressedPoint) def sign_json(private_key, payload: dict) -> str: """ Sign a deterministic canonical JSON representation. """ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") sig = private_key.sign(canonical, ec.ECDSA(hashes.SHA256())) # Keep signature as base64 for transport/storage. return b64(sig) def verify_signature(public_key, payload: dict, signature_b64: str) -> bool: canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") signature = unb64(signature_b64) try: public_key.verify(signature, canonical, ec.ECDSA(hashes.SHA256())) return True except InvalidSignature: return False @dataclass class Receipt: shipment_id: str receipt_index: int prev_digest_b64: str # base64 digest of previous receipt content; genesis uses "" device_id: str timestamp: str location_hint: str shipment_state_commitment_b64: str destination_lock_b64: str device_pubkey_b64: str signature_b64: str def to_dict(self) -> dict: return { "shipment_id": self.shipment_id, "receipt_index": self.receipt_index, "prev_digest_b64": self.prev_digest_b64, "device_id": self.device_id, "timestamp": self.timestamp, "location_hint": self.location_hint, "shipment_state_commitment_b64": self.shipment_state_commitment_b64, "destination_lock_b64": self.destination_lock_b64, "device_pubkey_b64": self.device_pubkey_b64, "signature_b64": self.signature_b64, } def digest_b64(self) -> str: """ Digest of the receipt *including signature*. This is what binds the chain forward. """ raw = json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":")).encode("utf-8") return b64(sha256(raw)) def canonical_unsigned_payload(receipt_fields: dict) -> dict: """ Everything that is signed except the signature itself. """ return receipt_fields def create_receipt( *, shipment_id: str, receipt_index: int, prev_receipt: Receipt | None, private_key, public_key, device_id: str, location_hint: str, shipment_state_commitment: bytes, destination_lock: bytes, destination_name: str, ) -> Receipt: prev_digest_b64 = "" if prev_receipt is not None: prev_digest_b64 = prev_receipt.digest_b64() # Public key bytes serve as a stable identifier for the device signature verification. device_pub_b = pubkey_bytes(public_key) unsigned_fields = { "shipment_id": shipment_id, "receipt_index": receipt_index, "prev_digest_b64": prev_digest_b64, "device_id": device_id, "timestamp": now_utc_iso(), "location_hint": location_hint, "shipment_state_commitment_b64": b64(shipment_state_commitment), "destination_lock_b64": b64(destination_lock), "destination_name": destination_name, # included to prevent lock replay across different flows "device_pubkey_b64": b64(device_pub_b), } signature_b64 = sign_json(private_key, canonical_unsigned_payload(unsigned_fields)) return Receipt( shipment_id=shipment_id, receipt_index=receipt_index, prev_digest_b64=prev_digest_b64, device_id=device_id, timestamp=unsigned_fields["timestamp"], location_hint=location_hint, shipment_state_commitment_b64=unsigned_fields["shipment_state_commitment_b64"], destination_lock_b64=unsigned_fields["destination_lock_b64"], device_pubkey_b64=unsigned_fields["device_pubkey_b64"], signature_b64=signature_b64, ) def reconstruct_public_key(pubkey_b64: str): pub_bytes = unb64(pubkey_b64) return ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), pub_bytes) def verify_receipts(receipts: list[Receipt], destination_name: str, expected_destination_secret: bytes) -> bool: """ Verifies: - hash chain linkage via prev_digest_b64, - signature validity, - hash-lock correctness for destination endpoint. """ if not receipts: return False prev = None for i, r in enumerate(receipts): # 1) Check index consistency if r.receipt_index != i: print(f"Index mismatch at {i}: got {r.receipt_index}") return False # 2) Check prev linkage if prev is None: if r.prev_digest_b64 != "": print("Genesis prev_digest must be empty") return False else: expected_prev = prev.digest_b64() if r.prev_digest_b64 != expected_prev: print(f"Chain broken at index {i}") return False # 3) Verify signature over the canonical payload device_pub = reconstruct_public_key(r.device_pubkey_b64) unsigned_fields = { "shipment_id": r.shipment_id, "receipt_index": r.receipt_index, "prev_digest_b64": r.prev_digest_b64, "device_id": r.device_id, "timestamp": r.timestamp, "location_hint": r.location_hint, "shipment_state_commitment_b64": r.shipment_state_commitment_b64, "destination_lock_b64": r.destination_lock_b64, "destination_name": destination_name, "device_pubkey_b64": r.device_pubkey_b64, } if not verify_signature(device_pub, canonical_unsigned_payload(unsigned_fields), r.signature_b64): print(f"Bad signature at index {i}") return False # 4) Verify destination lock matches expected material # Destination lock is derived as H(destination_secret || shipment_id) dest_lock_expected = sha256(expected_destination_secret + r.shipment_id.encode("utf-8")) if unb64(r.destination_lock_b64) != dest_lock_expected: print(f"Destination lock mismatch at index {i}") return False prev = r return True def main(): # "Secrets" in this demo: # - destination_secret would be held by the endpoint that is allowed to validate/accept this flow. destination_secret = b"destination-endpoint-secret-material-v1" destination_name = "ENDPOINT:ICE-WAREHOUSE-42" shipment_id = "SHIP-2026-11-03-ACME-001" # Shipment state commitment: in real systems, this might commit to the immutable shipment manifest # (batch IDs, initial temperature profile hash, seals, etc.). Here it's just a demo hash. shipment_manifest = { "batch_id": "BATCH-7782", "product": "REAGENT-X", "seal_id": "SEAL-9f31c7", "initial_temp_c": -20.5, } shipment_state_commitment = sha256(json.dumps(shipment_manifest, sort_keys=True).encode("utf-8")) # Generate two custody device keypairs to simulate two different locations/devices. deviceA_priv, deviceA_pub = generate_ec_keypair() deviceB_priv, deviceB_pub = generate_ec_keypair() # Destination lock derived from destination_secret + shipment_id. # This is what the endpoint later checks to prevent cross-flow replay. destination_lock = sha256(destination_secret + shipment_id.encode("utf-8")) # Create receipts (genesis -> handoff -> final) r0 = create_receipt( shipment_id=shipment_id, receipt_index=0, prev_receipt=None, private_key=deviceA_priv, public_key=deviceA_pub, device_id="DEVICE-DOCK-A", location_hint="COLD-DOCK-3", shipment_state_commitment=shipment_state_commitment, destination_lock=destination_lock, destination_name=destination_name, ) r1 = create_receipt( shipment_id=shipment_id, receipt_index=1, prev_receipt=r0, private_key=deviceA_priv, public_key=deviceA_pub, device_id="DEVICE-DOCK-A", location_hint="TEMPERATURE-CHECK-STATION-1", shipment_state_commitment=shipment_state_commitment, destination_lock=destination_lock, destination_name=destination_name, ) r2 = create_receipt( shipment_id=shipment_id, receipt_index=2, prev_receipt=r1, private_key=deviceB_priv, public_key=deviceB_pub, device_id="DEVICE-ICE-DOOR-B", location_hint="ICE-WAREHOUSE-42-INBOUND", shipment_state_commitment=shipment_state_commitment, destination_lock=destination_lock, destination_name=destination_name, ) receipts = [r0, r1, r2] # Print what happens when we run this: digest values and a JSON preview. print("Receipt chain digests:") for r in receipts: print(f" index={r.receipt_index}, digest={r.digest_b64()[:16]}...") print("\nVerify receipts at destination endpoint:") ok = verify_receipts( receipts, destination_name=destination_name, expected_destination_secret=destination_secret, ) print("Verification result:", ok) # Demonstrate tamper detection by altering a field and re-verifying. print("\nTamper demo: change location_hint at index 1 and re-verify.") r_tampered = Receipt(**r1.to_dict()) r_tampered.location_hint = "TAMPERED-LOCATION" receipts_tampered = [r0, r_tampered, r2] ok2 = verify_receipts( receipts_tampered, destination_name=destination_name, expected_destination_secret=destination_secret, ) print("Verification result after tamper:", ok2) if __name__ == "__main__": main()

Walkthrough: what each important block is doing

1) shipment_state_commitment binds the chain to an immutable manifest

I hash the shipment manifest (batch id, seal id, etc.) and store only the digest:

  • If anyone changes the manifest after a receipt exists, the endpoint can’t recreate the same commitment.
  • Receipts reference the commitment without needing to repeat the whole manifest each time.

In code:

shipment_state_commitment = sha256(json.dumps(shipment_manifest, sort_keys=True).encode("utf-8"))

2) Each receipt links to the previous one via prev_digest_b64

When creating receipt r1, I compute the digest of r0 and store it as prev_digest_b64.

That means:

  • If r0 is modified, its digest changes.
  • r1 still points to the old digest, so verification fails.

3) Digital signatures prove which device produced the receipt

Each receipt includes:

  • the device public key (device_pubkey_b64),
  • and a signature over the receipt fields (without the signature itself).

Verification reconstructs the public key and verifies the signature.

This part is crucial: without signatures, hash-chaining alone detects tampering, but not forgery by a non-device.

4) Destination lock prevents replay across flows

I derive:

dest_lock_expected = sha256(destination_secret + shipment_id.encode("utf-8"))

So even if a receipt chain is copied to another shipment id, or a different destination flow, the endpoint’s expected lock won’t match.


What happens when I run it

When I run main(), I get:

  • A list of digest prefixes for each receipt in the chain.
  • Verification result: True when receipts are intact.
  • Verification result after tamper: False after I change location_hint.

That last part is the provenance promise: the system rejects any post-hoc edits because they break either the signature checks or the hash-chain linkage.


Where this fits in a real provenance stack

In a full system, I would store receipt data in a distributed log or ledger (or at least replicate it across parties). But the cryptographic primitive—hash-locked, signed, chain-linked receipts—is what keeps the provenance story consistent even when storage is messy.

In other words:

  • storage can be decentralized,
  • devices can be untrusted,
  • and the endpoint can still verify authenticity and flow correctness.

Conclusion

I built a hash-locked receipt chain for cold-storage custody handoffs: each signed “location receipt” links to the previous one via digests, binds to an immutable shipment state commitment, and includes a destination lock derived from endpoint-held secret material. Running the prototype demonstrated that intact chains verify cleanly while any tampering (like changing a location hint) causes verification to fail—exactly the tamper-evidence I wanted for digital provenance.