Skip to content

Hold, serve, incinerate

Custody holds what your app may one day have to forget: an ID document, a signed agreement, a private file. Each object has its own key; deleting it destroys the key, recorded on Sui our statement (Custody and deletion).

Source the Quickstart’s first block. No bucket needed.

30-custody-store.sh
# Custody holds a secret for you, encrypted, and answers with two hashes:
# certificate_fingerprint is the salted fingerprint the deletion certificate
# will name (only someone holding the salt can check it), stored_hash is the
# hash of the bytes as stored. Keep both - they are used by different verifiers.
TEXT=${TEXT:-"hello, custody"}
curl -sS -m 60 -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d "$(python3 -c 'import json,sys; print(json.dumps({"text": sys.argv[1]}))' "$TEXT")" \
-o store.json -w 'store %{http_code}\n' "$HOST/v1/api/custody/store"
# jq does this just as well: CID=$(jq -r .id store.json)
CID=$(python3 -c 'import json; print(json.load(open("store.json"))["id"])')
CERT_FINGERPRINT=$(python3 -c 'import json; print(json.load(open("store.json"))["certificate_fingerprint"])')
STORED_HASH=$(python3 -c 'import json; print(json.load(open("store.json"))["stored_hash"])')
echo "id $CID"
echo "certificate_fingerprint $CERT_FINGERPRINT"
echo "stored_hash $STORED_HASH"

Keep the id and:

  • certificate_fingerprint: the salted fingerprint the certificate will name (certificate_fingerprint_kind: salted), checkable only with the salt.
  • stored_hash: what the verify route answers for.
31-custody-status.sh
# alive is true while the key still exists.
curl -sS -m 60 -H "$AUTH" "$HOST/v1/api/custody/status/$CID"
echo

alive is true while the key exists. Unknown and other tenants’ ids both answer 404.

32-custody-plaintext.sh
# While the key lives, the plaintext comes back.
curl -sS -m 60 -H "$AUTH" "$HOST/v1/api/custody/plaintext/$CID"
echo

The tenant gets plaintext while the key lives.

Irreversible.

33-custody-incinerate.sh
# Incinerate: deletion by key destruction, with a certificate as the receipt.
# minted means the certificate landed on chain and the digest below resolves.
curl -sS -m 120 -X POST -H "$AUTH" \
-o incinerate.json -w 'incinerate %{http_code}\n' \
"$HOST/v1/api/custody/incinerate/$CID"
python3 - <<'PY'
import json
d = json.load(open("incinerate.json"))
for k in ("certificate_status", "certificate_object_id", "certificate_tx_digest"):
print(k, "=", d.get(k))
PY
  • minted: the certificate is on Sui at certificate_object_id.
  • not_minted: no certificate landed. The key is destroyed either way.
34-custody-after.sh
# The plaintext is gone for good: 410, and the status says so.
curl -sS -m 60 -H "$AUTH" \
-o after.json -w 'plaintext %{http_code}\n' "$HOST/v1/api/custody/plaintext/$CID"
curl -sS -m 60 -H "$AUTH" -o status.json "$HOST/v1/api/custody/status/$CID"
python3 - <<'PY'
import json
d = json.load(open("status.json"))
print("alive =", d.get("alive"))
print("deletion_mode =", d.get("deletion_mode"))
PY

Plaintext answers 410 and status alive: false, for good. The ciphertext remains, unreadable.

It names a salted fingerprint, not a tenant or user: whoever holds the file and salt can match it, and nobody learns who stored what.

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