Verification
Verify a MacCrab plugin without trusting the rave catalog
MacCrab Rave is convenience tooling on top of the platform’s Ed25519 signing + reproducible builds. You don’t have to trust the catalog. This page walks through the full verification path for a plugin: download, signature, reproducibility, all the way to a runnable binary you’ve verified end-to-end.
If any step fails, don’t install the plugin. Report to maccrab@peterhanily.com so the catalog can investigate.
First-party plugins
The plugins shipped by the MacCrab team (the com.maccrab.forensics.* namespace) are first-party: they’re built and signed by MacCrab and published from the signed catalog mirror, so their catalog entry has no public source_repo and no community source tree to rebuild from. That means the source-rebuild reproducibility check (Step 5) does not apply — but every other guarantee still does, and you can verify all of them yourself:
- Catalog signature (Step 2) — the catalog index and the per-entry JSON are Ed25519-signed by the rave key; verify them against the published
catalog.pub. - Artifact signature (Step 4) — once binaries land, each first-party artifact is signed by the MacCrab publisher key recorded as
signer_public_key_sha256in the entry;maccrabctl plugin verifychecks the download against it. - Digest pins (Steps 3–4) — the entry pins
artifact_sha256,manifest_sha256, andsignature_sha256; you can hash the download and compare before trusting it. - Revocation (Step 6) — first-party keys are subject to the same signed revocation list as everyone else.
In short: for a first-party plugin, “verify” means confirm the signatures and digests chain back to the published MacCrab keys — not rebuild from source. Skip Step 5; run Steps 1–4 and Step 6.
You can verify the catalog signatures today (Steps 1–2). For a plugin with a published binary, the artifact steps verify that signed binary; an entry still awaiting its binary cut shows placeholder digests until then.
What you’ll need
- macOS 13 or later
- MacCrab v1.16+ (provides
maccrabctl plugin verify) - Standard tools:
gh(GitHub CLI),swift,unzip,python3— preinstalled on a Mac with Xcode Command Line Tools - The canonical-hash tool from this repo:
/tools/maccrab-rave-canonical-hash
curl -O https://rave.maccrab.com/tools/maccrab-rave-canonical-hash
chmod +x maccrab-rave-canonical-hash
Step 1 — Read the catalog entry
PLUGIN_ID="com.example.my-plugin"
VERSION="1.0.3"
curl -s "https://rave.maccrab.com/catalog/${PLUGIN_ID}.json" | jq
Note the values you’ll use below:
source_repo— where the source livessigner_identity— human-readable publisher (e.g.,github:author)signer_public_key_sha256— the trust handle; SHA-256 of the publisher’s Ed25519 public keyversions.${VERSION}.source_commit— the exact source SHA the catalog approvedversions.${VERSION}.artifact_sha256— raw download integrityversions.${VERSION}.canonical_tree_sha256— source-integrity (reproducibility)versions.${VERSION}.signature_sha256— pins the specific Ed25519 signaturerelease_url_template— pattern for the GitHub Release asset URL
Step 2 — Verify the catalog itself is signed
The catalog is Ed25519-signed by the maccrab-rave project key. Verify before consuming any of it.
curl -s "https://rave.maccrab.com/catalog/${PLUGIN_ID}.json" > entry.json
curl -s "https://rave.maccrab.com/catalog/${PLUGIN_ID}.json.sig" > entry.sig
curl -s "https://rave.maccrab.com/keys/catalog.pub" > catalog.pub
# Verify with the platform's CLI (recommended — what MacCrab itself uses)
maccrabctl plugin verify \
--public-key catalog.pub \
--signature entry.sig \
entry.json
# Or with a stand-alone Ed25519 verifier
python3 -c "
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
import sys
pub = Ed25519PublicKey.from_public_bytes(open('catalog.pub','rb').read())
sig = open('entry.sig','rb').read()
msg = open('entry.json','rb').read()
pub.verify(sig, msg)
print('catalog entry signature: OK')
"
If verification fails, stop here. Either the catalog was tampered with or the rave project’s signing key rotated and you have a stale catalog.pub. Re-fetch from https://rave.maccrab.com/keys/catalog.pub and try again; if still fails, escalate.
Step 3 — Download the plugin artifact
DOWNLOAD_URL=$(jq -r --arg ver "${VERSION}" --arg id "${PLUGIN_ID}" \
'.release_url_template | gsub("{tag}"; "v" + $ver) | gsub("{file}"; $id + ".maccrabplugin.zip")' \
entry.json)
curl -L -o plugin.zip "${DOWNLOAD_URL}"
unzip -d plugin/ plugin.zip
# The bundle nests its files under a top-level <plugin-id>/ directory; point PDIR
# at it for the steps below. ${PDIR} holds: manifest.json, binary, signature, signing.key.pub
PDIR="plugin/${PLUGIN_ID}"
Verify the transport-integrity digest matches what the catalog approved:
EXPECTED=$(jq -r --arg ver "${VERSION}" '.versions[$ver].artifact_sha256' entry.json)
ACTUAL=$(shasum -a 256 plugin.zip | awk '{print $1}')
[[ "${EXPECTED}" == "${ACTUAL}" ]] && echo "artifact_sha256 matches" || echo "MISMATCH — do not install"
Step 4 — Verify the Ed25519 signature
The plugin’s signature file is an Ed25519 signature over "maccrab-tierb-plugin-v1\n" ‖ SHA-256(manifest.json) ‖ SHA-256(binary) — a domain-separation prefix followed by the two digests (exactly what the app’s signature verifier reconstructs). The signing key is in signing.key.pub.
First, verify the signature file matches the catalog-pinned digest:
SIG_EXPECTED=$(jq -r --arg ver "${VERSION}" '.versions[$ver].signature_sha256' entry.json)
SIG_ACTUAL=$(shasum -a 256 "${PDIR}/signature" | awk '{print $1}')
if [[ "${SIG_EXPECTED}" =~ ^0+$ ]]; then
echo "signature_sha256 not pinned for this entry yet — the Ed25519 verify below is the proof"
elif [[ "${SIG_EXPECTED}" == "${SIG_ACTUAL}" ]]; then
echo "signature_sha256 matches"
else
echo "MISMATCH"
fi
Then verify the publisher key matches the one the catalog endorses:
KEY_EXPECTED=$(jq -r '.signer_public_key_sha256' entry.json)
KEY_ACTUAL=$(shasum -a 256 "${PDIR}/signing.key.pub" | awk '{print $1}')
[[ "${KEY_EXPECTED}" == "${KEY_ACTUAL}" ]] && echo "signer_public_key_sha256 matches" || echo "MISMATCH — publisher key changed"
Then verify the signature itself:
maccrabctl plugin verify "${PDIR}"
# Internally: reads manifest.json + binary, reconstructs the signed payload
# ("maccrab-tierb-plugin-v1\n" || SHA-256(manifest) || SHA-256(binary)),
# and verifies the signature against signing.key.pub.
If your MacCrab is older than v1.16+ (which provides maccrabctl plugin verify), use Python:
PDIR="${PDIR}" python3 <<'PY'
import hashlib, os
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
d = os.environ['PDIR']
pub = Ed25519PublicKey.from_public_bytes(open(f'{d}/signing.key.pub','rb').read())
sig = open(f'{d}/signature','rb').read()
manifest_hash = hashlib.sha256(open(f'{d}/manifest.json','rb').read()).digest()
binary_hash = hashlib.sha256(open(f'{d}/binary','rb').read()).digest()
# Ed25519 over the domain-separation prefix + the two digests
payload = b'maccrab-tierb-plugin-v1\n' + manifest_hash + binary_hash
pub.verify(sig, payload)
print('plugin Ed25519 signature: OK')
PY
Step 5 — Rebuild from source and compare canonical hashes
This is the reproducibility check. The canonical-hash tool is bundle-format-agnostic.
SOURCE_REPO=$(jq -r '.source_repo' entry.json)
SOURCE_COMMIT=$(jq -r --arg ver "${VERSION}" '.versions[$ver].source_commit' entry.json)
gh repo clone "${SOURCE_REPO}" plugin-source -- --depth=1 --branch="v${VERSION}"
cd plugin-source
# Build in deterministic environment
SOURCE_DATE_EPOCH=0 LANG=C LC_ALL=C TZ=UTC swift build -c release
# Run the project's package script (produces plugin-id-vX.zip under dist/)
./scripts/package-plugin.sh
cd ..
# Compute canonical hash of YOUR rebuild
REBUILD_HASH=$(./maccrab-rave-canonical-hash plugin-source/dist/*.zip)
# Compute canonical hash of the catalog's binary
DOWNLOAD_HASH=$(./maccrab-rave-canonical-hash plugin.zip)
EXPECTED_CANONICAL=$(jq -r --arg ver "${VERSION}" '.versions[$ver].canonical_tree_sha256' entry.json)
echo "catalog-pinned : ${EXPECTED_CANONICAL}"
echo "downloaded : ${DOWNLOAD_HASH}"
echo "your rebuild : ${REBUILD_HASH}"
if [[ "${REBUILD_HASH}" == "${DOWNLOAD_HASH}" && "${DOWNLOAD_HASH}" == "${EXPECTED_CANONICAL}" ]]; then
echo "REPRODUCIBILITY VERIFIED"
else
echo "MISMATCH — do not install"
fi
If they all match, you’ve verified end-to-end:
- The bytes match what the catalog approved (Step 3)
- The signature matches the catalog-endorsed publisher key (Step 4)
- The binary reproduces from the declared source (Step 5)
You can now install the plugin with full provenance certainty.
Step 6 — Check the revocation list
curl -s https://rave.maccrab.com/revocations.json > revocations.json
curl -s https://rave.maccrab.com/revocations.json.sig > revocations.sig
# Verify the revocations list itself first (signed by the same catalog key)
python3 -c "
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
pub = Ed25519PublicKey.from_public_bytes(open('catalog.pub','rb').read())
pub.verify(open('revocations.sig','rb').read(), open('revocations.json','rb').read())
print('revocations.json signature: OK')
"
# Then check whether this plugin version is on the list
jq --arg id "${PLUGIN_ID}" --arg ver "${VERSION}" \
'.revocations[] | select(.plugin_id == $id and (.scope.version == $ver or .scope.kind == "all_versions"))' \
revocations.json
Empty output means no active revocations. Any output means uninstall — the entry’s reason field explains why.
What if reproducibility fails?
If the canonical hashes don’t match, possible causes (in order of likelihood):
- Different macOS / Xcode / Swift version than the catalog used. The catalog records the toolchain pinning in
vetting.vetting_policy_commit(which pins the policy + the toolchain the rave CI used). - A real divergence — the binary the catalog has doesn’t match the declared source. This is the case the official channel is designed to prevent; if it happens, escalate.
- A reproducibility bug in the plugin’s build — the author’s code has timestamp-sensitive or path-sensitive content the normalization recipe doesn’t catch. Open an issue against the plugin (not the catalog).
What changed from v0 verification docs
Previous version of this doc walked through cosign verify-blob + slsa-verifier (Sigstore-era v4.4). The verification flow is now simpler — Ed25519 signature verification doesn’t need a trust-root TUF refresh, doesn’t need Rekor inclusion proofs, and doesn’t need SLSA attestation parsing. The trade-off is no public transparency log; verification still works end-to-end without trusting the rave catalog’s signing key (the key fingerprint is in the catalog entry; the catalog entry is itself signed by the catalog key; both are independently verifiable).
See also
- Catalog API — the signed catalog JSON and how the app fetches it
- Submit a plugin — the reproducible-build + Ed25519 signing requirements
- The app’s signature verifier (
maccrabctl plugin verify) — shipped with MacCrab v1.16+