Dekimu · provenance docs

Self-verify walkthrough

A long-form, end-to-end verification — done by hand, with curl and openssl. Skip the SDK; trust nothing past your own keyring.

What you'll prove

When you finish this walkthrough you will have independently confirmed three facts about a Dekimu receipt:

  • The receipt's body was signed by an Ed25519 key the issuer advertised at the time of issue (signature check).
  • The receipt ID was committed to a daily Merkle root built and signed by Dekimu's audit-anchor key (inclusion check).
  • That daily root has not been modified or substituted since it was signed (root signature check).

What you will not have proven: that the underlying artifact (a Commander run, a miniterms document) is correct, useful, or true. APR is a provenance protocol, not a quality judgement.

Prerequisites

  • curl, jq, and openssl 3.x (Ed25519 verification needs OpenSSL 1.1.1+; 3.x is recommended).
  • A real receipt ID. Get one from any Dekimu app's receipts view, or paste a known ID into the lookup form on verify.dekimu.com.
  • A scratch directory. The walkthrough writes a handful of small files into the current working directory.

Step 1 — fetch the claim

Replace $ID with your receipt ID. The first call gets the signed claim; we save it to disk so subsequent steps can parse it.

ID="<paste-id-here>"
curl -s "https://verify.dekimu.com/api/v/$ID" > claim.json
jq . claim.json | head -40

You should see the receipt's id, kind, issuer, subject, iat, kid, body, and sig. Note the kid — you'll look up the matching public key in step 3.

Step 2 — fetch the inclusion proof

curl -s "https://verify.dekimu.com/api/proof/$ID" > proof.json
jq '{date, rootKid, rootHash, leafIndex}' proof.json

If the response is {"error":"not_yet_built"}, the receipt is younger than the most recent daily root seal (00:05 UTC). Wait for the next root build and retry — the signed claim itself is already valid.

Note rootHash and rootKid. The first is what you'll reconstruct from the Merkle path; the second tells you which audit-root public key to use.

Step 3 — fetch the public keys

curl -s "https://verify.dekimu.com/.well-known/dekimu-claim-keys.json" \
  > claim-keys.json
curl -s "https://verify.dekimu.com/.well-known/dekimu-audit-root-keys.json" \
  > audit-root-keys.json

Two catalogs: one for issuer signing keys (used to sign the claim's body), one for audit-anchor keys (used to sign the daily root). Pull the public key for the claim's kid:

KID=$(jq -r .kid claim.json)
ISSUER_KEY_B64URL=$(jq -r --arg k "$KID" '.keys[] | select(.kid==$k) | .publicKey' \
  claim-keys.json)
echo "Issuer kid: $KID"
echo "Issuer key: $ISSUER_KEY_B64URL"

Step 4 — verify the issuer signature with openssl

Ed25519 verification with raw keys is supported by OpenSSL via PEM. We need to wrap the base64url public key in the standard SubjectPublicKeyInfo envelope, then verify the claim's signed body against sig.

First, write a small Python helper that turns the base64url key into a PEM file and writes the signed body bytes the issuer actually signed:

python3 - <<'PY'
import base64, json, sys

def b64url_decode(s: str) -> bytes:
    pad = "=" * (-len(s) % 4)
    return base64.urlsafe_b64decode(s + pad)

claim = json.load(open("claim.json"))
keys = json.load(open("claim-keys.json"))["keys"]
key = next(k for k in keys if k["kid"] == claim["kid"])

# Wrap the raw 32-byte Ed25519 public key in SPKI:
#   30 2a 30 05 06 03 2b 65 70 03 21 00 || raw32
raw = b64url_decode(key["publicKey"])
assert len(raw) == 32, "expected 32-byte Ed25519 public key"
spki = bytes.fromhex("302a300506032b6570032100") + raw

import textwrap
b64 = base64.b64encode(spki).decode()
pem = "-----BEGIN PUBLIC KEY-----\n" + "\n".join(textwrap.wrap(b64, 64)) + "\n-----END PUBLIC KEY-----\n"
open("issuer.pem", "w").write(pem)

# The signed body is the canonical JSON of the claim with sig stripped.
# Dekimu uses RFC 8785 JSON Canonicalization Scheme (JCS).
# For a from-scratch verification, install a JCS library; for this walkthrough
# we use the simpler 'sort_keys=True, separators=(",", ":")' form which
# matches the Dekimu canonicalization for claims that contain no floats.
signed = {k: v for k, v in claim.items() if k != "sig"}
signed_bytes = json.dumps(signed, sort_keys=True, separators=(",", ":")).encode()
open("signed.bin", "wb").write(signed_bytes)

# Decode the signature.
sig = b64url_decode(claim["sig"])
open("sig.bin", "wb").write(sig)
print("wrote issuer.pem, signed.bin, sig.bin")
PY

Now verify:

openssl pkeyutl -verify \
  -pubin -inkey issuer.pem \
  -rawin -in signed.bin \
  -sigfile sig.bin
# Expected output: "Signature Verified Successfully"

If you see Signature Verification Failure instead, the most likely cause is canonicalization drift — Dekimu signs the JCS canonical form; the simple sort_keys=True shim above matches it for claim shapes that don't contain floats. For receipts that do, install a real JCS library (pip install jcs) and run the body through jcs.canonicalize.

Step 5 — verify Merkle inclusion locally

The proof you fetched has a path array of {side, hash} entries. To reconstruct the root, hash the leaf and walk the path:

python3 - <<'PY'
import hashlib, json

proof = json.load(open("proof.json"))
def h(b): return hashlib.sha256(b).digest()

cur = bytes.fromhex(proof["leafHash"])
for step in proof["path"]:
    sib = bytes.fromhex(step["hash"])
    cur = h(cur + sib) if step["side"] == "R" else h(sib + cur)

assert cur.hex() == proof["rootHash"], (
    f"Merkle mismatch: reconstructed {cur.hex()} vs proof {proof['rootHash']}"
)
print(f"Merkle root reconstructed: {cur.hex()}")
PY

The script asserts the path actually rebuilds the published root. If it fails, the leaf or the path was modified after signing — treat the receipt as untrusted.

Step 6 — verify the root signature

The daily root file is signed by the audit-anchor key, looked up by the proof's rootKid. Pull the proof body that the anchor key actually signed and run the same openssl recipe:

python3 - <<'PY'
import base64, json, textwrap

proof = json.load(open("proof.json"))
keys = json.load(open("audit-root-keys.json"))["keys"]
key = next(k for k in keys if k["kid"] == proof["rootKid"])

def b64url(s):
    pad = "=" * (-len(s) % 4)
    return base64.urlsafe_b64decode(s + pad)

raw = b64url(key["publicKey"])
assert len(raw) == 32
spki = bytes.fromhex("302a300506032b6570032100") + raw
b64 = base64.b64encode(spki).decode()
pem = "-----BEGIN PUBLIC KEY-----\n" + "\n".join(textwrap.wrap(b64, 64)) + "\n-----END PUBLIC KEY-----\n"
open("root.pem", "w").write(pem)

# The audit anchor signs (v, date, rootKid, rootHash) in JCS form.
signed = {
    "v": proof["v"],
    "date": proof["date"],
    "rootKid": proof["rootKid"],
    "rootHash": proof["rootHash"],
}
open("root-signed.bin", "wb").write(
    json.dumps(signed, sort_keys=True, separators=(",", ":")).encode()
)
open("root-sig.bin", "wb").write(b64url(proof["rootSig"]))
print("wrote root.pem, root-signed.bin, root-sig.bin")
PY

openssl pkeyutl -verify \
  -pubin -inkey root.pem \
  -rawin -in root-signed.bin \
  -sigfile root-sig.bin
# Expected: "Signature Verified Successfully"

Verdict

If steps 4, 5, and 6 all returned successful verifications, the receipt is verified:

  • The body was signed by the issuer's public key at the time stamped by iat.
  • The receipt ID was committed to a daily Merkle root for <anchor-date>.
  • That daily root was signed by Dekimu's audit-anchor key and has not been substituted.

You did not trust Dekimu's servers to perform any of these steps — you trusted only the two key catalogs published at /.well-known/. The same recipe runs against your own copy of those catalogs offline, so long as you fetched them while the issuer keys you care about were still active.