Catalog JSON API
The HTTP contract maccrabctl + the in-app browser consume
The rave catalog is a tree of static signed JSON files. Every path is GET-only, every payload has a detached Ed25519 signature, and the schema is versioned. This is the contract anything fetching the catalog should follow — maccrabctl plugin install <id> does, the in-app Catalog tab does, and a community-built mirror or alternative client would too.
Base URL
https://rave.maccrab.com/
Mirrors override this — see enterprise mirror docs. The URL pattern is identical; only the host changes. (The served root is rave.maccrab.com; the legacy subpath form is retired. Plugin entries’ release_url_template and $schema use rave.maccrab.com; the remaining kit $schema values migrate in the next signing ceremony.)
Resource map
| Path | Purpose | Schema |
|---|---|---|
/catalog.json | Master index of plugins | catalog.json |
/catalog.json.sig | Ed25519 signature for the above | raw 64 bytes |
/catalog/<id>.json | Per-plugin detail (versions, vetting, digests) | catalog-entry.json |
/catalog/<id>.json.sig | Detached signature | raw 64 bytes |
/kits/<id>.json | Pre-bundled plugin set | kit.json |
/kits/<id>.json.sig | Detached signature | raw 64 bytes |
/revocations.json | Revoked plugin versions (monotonic serial) | revocation.json |
/revocations.json.sig | Detached signature | raw 64 bytes |
/keys/catalog.pub | The rave project’s Ed25519 public key | raw 32 bytes |
/keys/catalog.fingerprint | SHA-256 of the public key (operator-readable) | hex string |
/releases/<tag>/<id>.maccrabplugin.zip | Plugin distribution archive | flat-directory zip |
/schemas/<name>.json | JSON Schema definitions | JSON Schema 2020-12 |
Signature verification
Every signed file uses raw Ed25519 over the file bytes directly — no canonicalization, no envelope. The verification recipe in Python:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
import urllib.request
base = "https://rave.maccrab.com/"
pub = Ed25519PublicKey.from_public_bytes(
urllib.request.urlopen(base + "keys/catalog.pub").read()
)
catalog = urllib.request.urlopen(base + "catalog.json").read()
sig = urllib.request.urlopen(base + "catalog.json.sig").read()
pub.verify(sig, catalog) # raises on mismatch
The same recipe verifies any /catalog/*.json, any /kits/*.json, and /revocations.json.
The plugin binary inside /releases/<tag>/<id>.zip has a separate signature inside the bundle, signed by the publisher’s key — not the catalog key. See verification docs for the binary verification chain.
Master catalog shape
{
"$schema": "https://rave.maccrab.com/schemas/catalog.json",
"schema_version": "1",
"catalog_serial": 3,
"generated_at": "2026-06-26T00:00:00Z",
"count": 1,
"plugins": {
"com.maccrab.forensics.posture-pro": {
"id": "com.maccrab.forensics.posture-pro",
"display_name": "Mac Security Posture Pro",
"short_description": "Grades the Mac's hardening state and flags unsigned launchd persistence + high-risk privacy-permission exposure.",
"current_version": "0.1.0",
"channel": "official",
"runtime": "tierB",
"trust_tier": "first-party",
"signer_identity": "maccrab-rave:first-party",
"signer_public_key_sha256": "07e39eb12c15b8052f5249134ea3337a0789ebc799d1c58d097aaa548a8aaae3",
"metadata": {
"category": "system",
"tags": ["posture", "collector", "hardening", "persistence"],
"min_maccrab_version": "1.19.0",
"min_macos": "13.0"
}
}
}
}
The master index carries enough summary metadata to render a catalog grid without a second round-trip per plugin. Detail panels fetch the per-plugin /catalog/<id>.json lazily.
The
runtimefield’s wire value"tierB"is the catalog’s internal identifier for the standard plugin runtime: Apple’s App Sandbox + an XPC service with no outbound network. UIs should render the human label (“App Sandbox + XPC service”), not the raw token.
Index-level fields a client should bind into its provenance receipt:
| Field | Purpose |
|---|---|
schema_version | Catalog-index format version. Distinct from each per-entry schema_version. Recorded so a run states which catalog format it verified. |
catalog_serial | Signed, monotonic freshness counter, incremented on every regeneration. A client that has seen serial N MUST reject any later-fetched, validly-signed catalog whose catalog_serial < N as a stale-catalog (rollback/replay) attack. A network attacker can’t forge a higher serial without the offline catalog key. Prefer this over generated_at for staleness ordering. |
signer_public_key_sha256 (per summary) | First-class trust handle surfaced from the per-entry entry so the grid can key-pin a plugin without a per-plugin round-trip. SHA-256 of the publisher’s 32-byte Ed25519 public key. |
schema_version,catalog_serial, and the per-summarysigner_public_key_sha256are part of the signed catalog index and are verified by the client.
Per-plugin entry shape (selected fields)
The full schema is at catalog-entry.json. Key fields for a consumer:
| Field | Purpose |
|---|---|
id | Reverse-DNS plugin id, matches manifest |
schema_version | Per-entry catalog-entry format version ("1"). Distinct from the master-index catalog.schema_version; recorded so a run states which entry shape it verified. Required-going-forward (see re-sign note above). |
display_name, short_description, long_description | UX-renderable metadata |
author{name,url} | Detail-page byline |
signer_identity | Human-readable publisher (e.g., github:author) |
signer_public_key_sha256 | Load-bearing trust handle — SHA-256 of the 32-byte Ed25519 publisher public key. Also surfaced in the master index per summary. |
current_version | Default install target |
versions.<v>.tag | GitHub Release tag |
versions.<v>.artifact_sha256 | SHA-256 of the published .zip |
versions.<v>.manifest_sha256 | SHA-256 of manifest.json inside the bundle |
versions.<v>.signature_sha256 | SHA-256 of the publisher signature file inside the bundle |
versions.<v>.canonical_tree_sha256 | Mach-O-normalized reproducibility hash |
versions.<v>.vetting.* | Automated vetting passes + policy commit + timestamp |
release_url_template | RFC6570 template: {tag}, {file} slots |
metadata.category | One of: system, browser, comms, activity, artifact, analyzer |
metadata.tags | Free-form tag list |
metadata.min_maccrab_version, metadata.max_maccrab_version, metadata.min_macos | Version floor/ceiling. min_maccrab_version is the single load-bearing floor the client gates install on; max_maccrab_version is an optional ceiling; min_macos is the OS floor. These live at the plugin (entry/summary) level — there is no separate per-version floor field, and none is needed: a plugin’s compatibility floor is plugin-wide, not per-published-version. |
status | active, unreachable_temporary, unreachable_extended, archived, revoked |
Install URL resolution
Given a plugin-id and an optional pinned version:
- GET
<base>catalog.json+.sig, verify againstkeys/catalog.pub. - Look up
plugins.<plugin-id>.current_version(or use pinned version). - GET
<base>catalog/<plugin-id>.json+.sig, verify same key. - Render
release_url_templatewith:{tag}=versions.<v>.tag{file}=<plugin-id>.maccrabplugin.zip
- GET the resulting URL, verify SHA-256 matches
versions.<v>.artifact_sha256. - Unzip → install via the app’s plugin installer (this is what
maccrabctl plugin installautomates).
maccrabctl v1.17+ implements this internally as plugin install <id>.
Local rehearsal
For testing against a local catalog, override the base + release URL:
export MACCRAB_RAVE_BASE_URL=http://127.0.0.1:1313/
export MACCRAB_RAVE_CATALOG_PUB_PATH=~/Library/Application\ Support/MacCrab/dev-keys/maccrab-rave-catalog.pub
export MACCRAB_RAVE_URL_REWRITE_FROM=https://rave.maccrab.com/releases
export MACCRAB_RAVE_URL_REWRITE_TO=http://127.0.0.1:1313/releases
python3 -m http.server 1313 --directory rave &
maccrabctl plugin install com.maccrab.forensics.posture-pro
The full local-rehearsal commands are inline above.
Revocation freshness
/revocations.json carries a monotonic serial (integer ≥ 0) alongside version (the format version) and updated_at (a human/diagnostic timestamp). The monotonic-increment rule:
- Every change to the list increments
serialby exactly +1. serialnever decreases across two validly-signedrevocations.jsona client observes.
Because the file is Ed25519-signed, serial is a signed counter: a client that has seen serial N MUST reject any later-fetched, validly-signed list whose serial < N as a stale/rollback (replay) attack — a replay that would silently un-revoke a compromised plugin. A network attacker can’t forge a higher serial without the offline catalog key. Each revocation increments serial, and the offline re-sign ceremony signs the incremented file.
The same stale-rejection logic applies to the master index via catalog_serial (see Master catalog shape).
Versioning + compatibility
$schemaURLs point at the schema file the document was authored against (served rootrave.maccrab.com).- Schema fields are additive — new optional fields land first, old consumers ignore them.
- Breaking changes get a new path:
/v2/catalog.jsonetc. Old paths stay live until the deprecation window closes. - The catalog uses schema v1 throughout.
schema_version,catalog_serial, per-summarysigner_public_key_sha256, per-entryschema_version, and revocationsserialare part of the signed bytes the client verifies.