MacCrabRave
Documentation

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

PathPurposeSchema
/catalog.jsonMaster index of pluginscatalog.json
/catalog.json.sigEd25519 signature for the aboveraw 64 bytes
/catalog/<id>.jsonPer-plugin detail (versions, vetting, digests)catalog-entry.json
/catalog/<id>.json.sigDetached signatureraw 64 bytes
/kits/<id>.jsonPre-bundled plugin setkit.json
/kits/<id>.json.sigDetached signatureraw 64 bytes
/revocations.jsonRevoked plugin versions (monotonic serial)revocation.json
/revocations.json.sigDetached signatureraw 64 bytes
/keys/catalog.pubThe rave project’s Ed25519 public keyraw 32 bytes
/keys/catalog.fingerprintSHA-256 of the public key (operator-readable)hex string
/releases/<tag>/<id>.maccrabplugin.zipPlugin distribution archiveflat-directory zip
/schemas/<name>.jsonJSON Schema definitionsJSON 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 runtime field’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:

FieldPurpose
schema_versionCatalog-index format version. Distinct from each per-entry schema_version. Recorded so a run states which catalog format it verified.
catalog_serialSigned, 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-summary signer_public_key_sha256 are 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:

FieldPurpose
idReverse-DNS plugin id, matches manifest
schema_versionPer-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_descriptionUX-renderable metadata
author{name,url}Detail-page byline
signer_identityHuman-readable publisher (e.g., github:author)
signer_public_key_sha256Load-bearing trust handle — SHA-256 of the 32-byte Ed25519 publisher public key. Also surfaced in the master index per summary.
current_versionDefault install target
versions.<v>.tagGitHub Release tag
versions.<v>.artifact_sha256SHA-256 of the published .zip
versions.<v>.manifest_sha256SHA-256 of manifest.json inside the bundle
versions.<v>.signature_sha256SHA-256 of the publisher signature file inside the bundle
versions.<v>.canonical_tree_sha256Mach-O-normalized reproducibility hash
versions.<v>.vetting.*Automated vetting passes + policy commit + timestamp
release_url_templateRFC6570 template: {tag}, {file} slots
metadata.categoryOne of: system, browser, comms, activity, artifact, analyzer
metadata.tagsFree-form tag list
metadata.min_maccrab_version, metadata.max_maccrab_version, metadata.min_macosVersion 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.
statusactive, unreachable_temporary, unreachable_extended, archived, revoked

Install URL resolution

Given a plugin-id and an optional pinned version:

  1. GET <base>catalog.json + .sig, verify against keys/catalog.pub.
  2. Look up plugins.<plugin-id>.current_version (or use pinned version).
  3. GET <base>catalog/<plugin-id>.json + .sig, verify same key.
  4. Render release_url_template with:
    • {tag} = versions.<v>.tag
    • {file} = <plugin-id>.maccrabplugin.zip
  5. GET the resulting URL, verify SHA-256 matches versions.<v>.artifact_sha256.
  6. Unzip → install via the app’s plugin installer (this is what maccrabctl plugin install automates).

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 serial by exactly +1.
  • serial never decreases across two validly-signed revocations.json a 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

  • $schema URLs point at the schema file the document was authored against (served root rave.maccrab.com).
  • Schema fields are additive — new optional fields land first, old consumers ignore them.
  • Breaking changes get a new path: /v2/catalog.json etc. Old paths stay live until the deprecation window closes.
  • The catalog uses schema v1 throughout. schema_version, catalog_serial, per-summary signer_public_key_sha256, per-entry schema_version, and revocations serial are part of the signed bytes the client verifies.