Skip to content

Plugin And App Upgrade Trust

Scryer never installs code just because it downloaded it. Every catalog, plugin artifact, rule pack, and application upgrade arrives with a Sigstore signature bundle. Scryer verifies that bundle offline against pinned trust roots, confirms the signer is the expected GitHub repository and release workflow, and then checks the bytes against the digests and metadata the signed document promised. This page lists those checks in the order they run.

ObjectRequired signerFetched from
Plugin catalog redirect and catalogscryer-media/scryer-plugins, workflow release-plugin-v3.ymlcdn.scryer.media, with a GitHub fallback
Official plugin artifactThe required_signer the signed catalog declares for that pluginThe catalog’s artifact URL, then its mirrors
Verified community pluginThe community repository the central catalog approvedThat repository’s catalog
Manual GitHub repository pluginThe repository you entered, any workflowThat repository’s catalog
Rule packscryer-media/scryer-plugins, workflow release-plugin-v3.ymlcdn.scryer.media
Application upgrade manifestscryer-media/scryer, workflow scryer.yml, at the exact release tagThe GitHub release for that tag

Uploaded WASM files are the one unsigned path. See Uploaded WASM Files.

Scryer’s release workflows sign with Sigstore’s keyless flow. GitHub Actions presents an OIDC token, Fulcio issues a short-lived certificate that records which repository and workflow ran, and Rekor logs the signature. Scryer verifies the resulting bundle without contacting any Sigstore service. Every step below must pass.

  1. Bundle shape. Scryer accepts a Cosign v2 blob bundle or a Sigstore v0.3 message-signature bundle, never both in one document. A bundle must carry exactly one leaf certificate and exactly one Rekor entry of kind hashedrekord v0.0.1. The SHA-256 digest recorded in the bundle must match the downloaded bytes.
  2. Rekor entry. The entry’s log ID must be a pinned Rekor key that was valid at the entry’s integrated time, and the signed entry timestamp must verify with that key. For v0.3 bundles Scryer also verifies the inclusion proof: the signed checkpoint, its tree size and root, and a Merkle root recomputed from the leaf.
  3. Binding. The digest, signature, and certificate inside the Rekor body must be byte-equal to the bundle’s signature and certificate. A genuine log entry cannot be paired with a different artifact.
  4. Artifact signature. The signature is verified with the public key in the leaf certificate.
  5. Fulcio chain. The leaf must chain to a pinned Fulcio root with the code-signing key usage, and the chain must have been valid at the Rekor integrated time and at every RFC 3161 timestamp the bundle carries.
  6. Certificate transparency. The leaf must embed exactly one signed certificate timestamp, signed by a pinned CT log key and no newer than the Rekor time.
  7. RFC 3161 timestamps. A v0.3 bundle may carry up to four. Each must bind the signature and chain to a pinned timestamp authority with the timestamping key usage.
  8. Signer identity. The OIDC issuer must be https://token.actions.githubusercontent.com. The certificate’s repository must equal the required repository. When a workflow is required, a certificate URI must equal https://github.com/<repository>/<workflow>@refs/tags/<tag>. Only tag refs are accepted, so a build from a branch is refused even from the right repository. Application upgrades require the exact release tag.
  • Embedded snapshot. The binary ships a copy of Sigstore’s trusted_root.json with a provenance record naming its source and SHA-256. The digest is checked when the snapshot loads.
  • Refresh. At startup and every six hours Scryer fetches a fresh root through Sigstore’s TUF repository with a two-minute timeout. A failed or slow refresh keeps the current snapshot and retries in five minutes. Verification never waits on the network.
  • Time windows. Every Rekor key, CT log key, Fulcio chain, and timestamp authority carries a validity window from the root. A key that was not valid at the signing time is not trusted, even if it is in the root.

Installs, upgrades, and automatic updates from a catalog run the same pipeline. A manual GitHub repository install differs only in the signer it requires.

  1. Catalog. The redirect document and the catalog it points to are each signed and verified. Both are parsed with strict schemas that reject unknown fields, and decompressed under fixed size limits. A community catalog must publish exactly one plugin whose id, support tier, source repository, and signer repository all match the entry the central catalog approved.
  2. Download. The artifact and its signature bundle are fetched over HTTPS from the primary URL, then each mirror. Artifact fetches refuse redirects. Catalog fetches follow at most three, each re-validated as a public HTTPS target.
  3. Signature. The bundle is verified as above, using the catalog entry’s required signer.
  4. Digests and size. The compressed artifact must match the catalog’s BLAKE3 digest. It is decompressed under the declared byte count, and the result must match the catalog’s WASM digest and equal that byte count exactly.
  5. Descriptor. The WASM module is loaded and validated for its plugin kind. Its descriptor must report the same id, plugin type, provider type, and version as the catalog entry. The SDK contract is checked against the running host, host permissions such as allowed hosts are validated, and provider types reserved for first-party code are refused.
  6. Host-process capability. A notification plugin that asks to spawn host processes is refused unless its support tier is Official. Verified community and manual plugins can never carry it.
  7. Persist and reload. The compressed bytes, digests, and descriptor are stored. Every time Scryer loads a plugin from its database it re-verifies the WASM digest and re-runs the descriptor checks, so a tampered row fails to load.

Automatic updates take exactly the release the update badge would offer, restricted to stable releases on the installed major.minor. A failed update restores the previous installation row.

Uploading a .wasm or .wasm.zst file from Settings → Plugins skips signature verification, because there is nothing to verify against. Scryer still requires the manage-system-settings permission and an explicit risk acknowledgement, caps the decompressed size, loads and validates the descriptor and SDK contract, checks host permissions, refuses the host-process capability, refuses reserved provider types, and refuses to replace a bundled plugin. The result is recorded as a manual, unverified installation.

The Newznab and Torznab plugins are compiled into the binary. The build fetches them from the signed catalog and verifies them with Cosign before embedding. Their signature bundles and a provenance record listing the catalog version, artifact URLs, and BLAKE3 digests live in the source tree under crates/scryer-plugins/builtins.

Upgrade on the System page runs this pipeline. See Upgrading for the operator view.

  1. Eligibility. Only portable installs and direct MSI installs are upgraded in-app. Docker, Homebrew, Winget, Windows service or session-zero layouts, and installs with SCRYER_DISABLE_SELF_UPGRADE set are operator-managed.
  2. Request. The requested tag and version must match the current update notice, and the version must be strictly newer than the running one. One upgrade runs at a time under the system maintenance lock.
  3. Manifest. scryer-upgrade-manifest.json and its .sigstore.json bundle are fetched from the GitHub release for that tag, HTTPS only, under fixed size caps. The signature must come from scryer-media/scryer, workflow scryer.yml, at refs/tags/<tag>.
  4. Manifest contents. The schema must be scryer.upgrade.manifest.v1. Tag and version must equal the request, and the version must be plain semver. Each artifact URL must start with https://github.com/scryer-media/scryer/releases/download/<tag>/ and end with its asset name. Digests are lowercase BLAKE3 hex. Artifacts are unique and sorted. Portable archives list their members with relative paths and no .. or backslashes.
  5. Download. The artifact for this OS, architecture, and channel is streamed into application-upgrade/staging under the config directory after a free-space check. Neither the reported length nor the streamed bytes may exceed the manifest size. BLAKE3 is computed during the download and again from the file on disk.
  6. Archive. A portable tar.gz must contain exactly the member paths and sizes the manifest lists, all regular files. Extraction refuses absolute paths, parent segments, and drive prefixes, and sets the executable bit only on members the manifest marks executable. An MSI has no members and is handed to Windows Installer by the upgrade helper.
  7. Apply and confirm. A journal is written before the binary moves, and the previous executable is kept as a backup. On the next start Scryer checks that the expected version and executable path actually booted. If not, the run is marked failed and the backup is preserved.
  • Provenance attestations. Scryer does not fetch or evaluate SLSA provenance at install time. Trust rests on the Sigstore certificate identity, which records the repository and workflow that built the artifact, plus the signed digests.
  • Windows installer signatures. Scryer verifies the MSI’s BLAKE3 hash against the signed manifest. It does not check Authenticode itself.
  • Builds without trust support. A Scryer build compiled without the plugin-trust feature refuses every catalog, plugin, and upgrade verification rather than skipping it.
  • Plugins covers plugin kinds, support tiers, and manual installs.
  • Upgrading covers the in-app upgrade from the operator’s side.
  • Extensibility defines plugins, providers, and the provider catalog.