Skip to content

proven on the hosted testnet instance · 2026-09-13 · QA-14

Verify a file by hash, with no account

A provenance verdict for a hash, from a call that carries no Authorization header at all — and, when the answer is yes, the on-chain receipt to open.

This is the route you hand to somebody who has no relationship with Permafrost and no reason to acquire one. A counterparty, an auditor, a user checking a file they were sent. They need the bytes, or just the hash, and nothing else.

  • A sha256 hash, or the bytes to compute one from. shasum -a 256 <file> on a Mac, sha256sum <file> on Linux.
  • curl, and the instance up (The testnet instance).
  • No account. No key, no token, no invite. That is the point of the page.

Both calls below are deliberately keyless — no Authorization header on either.

40-verify.sh
# Verification takes no key: no Authorization header on either call below.
# Anyone holding the bytes can ask whether Permafrost holds provenance.
#
# Verify indexes the STORED hash - the bytes as stored - so the first call
# answers provenance true. Asking with CONTENT_HASH, the plaintext hash,
# legitimately answers provenance false: the plaintext hash is what the
# certificate names, not what storage indexes.
curl -sS -m 60 "$HOST/v1/api/verify?hash=$STORED_HASH"
echo
curl -sS -m 60 "$HOST/v1/api/verify?hash=$CONTENT_HASH"
echo

The hash goes in the query string as 64 hex characters. A 0x prefix and upper-case letters are normalised for you; anything that is not a sha256 after that is answered 400 {"error": "invalid_hash"}.

When Permafrost holds provenance for those bytes:

{
"hash": "<the normalised hash you asked with>",
"provenance": true,
"nft_id": "0x…",
"content_hash": "<the fingerprint anchored on chain>",
"certified": true,
"explorer_url": "https://…/object/0x…"
}
  • provenance is the verdict, and it is the only field that is always there.
  • nft_id is the on-chain provenance anchor — the receipt object. Empty if nothing has been minted for that record yet.
  • content_hash is the fingerprint the record carries.
  • certified is the registration status: true once the object has reached the storage layer’s certified state. A record can be held with certified: false — the provenance exists, the certification is still catching up.
  • explorer_url resolves nft_id on a Sui explorer. Empty when nft_id is. Following it is Read your receipt on Sui.

When it does not:

{ "hash": "", "provenance": false }

Two fields, nothing else. There is no “close match”, no partial answer and no error — a provenance: false is a clean, complete answer.

Not the owner, not the tenant, not the bucket or the key, not the size, not how many times it has been read, not where the bytes are stored. Those fields exist on the record and none of them is ever mapped into this response.

If several stored objects carry the same bytes, you get one canonical record — and no hint that there are others, no count, and no enumeration of who holds them. A route anybody can call cannot double as a directory of who stored what.

The verify route indexes the bytes the storage layer received. Which bytes those are depends on how the material got there, and that is the whole of the confusion this section exists to prevent.

For an object you stored through the S3 surface, the bytes the storage layer received are the bytes you sent. Hash your local file and ask with that; it matches. The fingerprint is taken before anything is encrypted at rest, so encryption does not change the answer.

For something held in custody, it does not. Custody hands the storage layer ciphertext, so the indexed hash is the ciphertext’s — stored_hash in that surface’s answers. The plaintext hash, content_hash, is the one the certificate names, and asking verify with it legitimately returns provenance: false. That is the wrong hash for this route, not a missing record. The second call in the block above demonstrates exactly that. See Hold, serve, incinerate.

One more case: an object you deleted no longer answers. Deletion tombstones the record, and a tombstoned record is not indexed. The receipt on Sui is untouched by that — it does not depend on this instance at all.

The first call prints "provenance":true with an nft_id and an explorer_url; the second prints "provenance":false. Two different answers from the same route, with no credential on either, is the whole demonstration.

Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.