Offline Verification Playbook (EB9)

Purpose: let a reader in a disconnected environment verify an exported Evidence release decision bundle with the packaged stella CLI and a declared trust root, without the producer’s Console session and without network access.

The Console does not verify bundles. Its Offline Verification Center (/ops/operations/offline-kit/verify) shows the three commands below and links to this guide; it has no file selector, no upload and no in-browser parser, and it keeps no verification history.

Supported handoff

1. Export the release decision bundle (connected)

stella evidence export --release <release-id> --output portable-evidence-bundle.tgz

The CLI calls Evidence’s by-release portable route with the evidence:read scope. Evidence resolves the sealed release-decision bundle that Release Orchestrator recorded for the release and returns its portable archive (portable-evidence-bundle-release-<bundle-id>.tgz when downloaded directly). Record the archive SHA-256 before you move it.

2. Save the declared trust root (connected)

curl -fsS https://<console-host>/api/evidence/v1/evidence/capsules/signing-keys/jwks -o evidence-signing-keys.jwks.json

Evidence publishes the public key that signs its bundles as a JSON Web Key Set (anonymous read; no private material). The bundle signature names the key id (evidence-locker-capsule-key in the shipped Compose profile). Trust a key only when its SPKI SHA-256, which the verifier prints for each key, equals the value your organization declared through an independent channel. A single SubjectPublicKeyInfo PEM public key (-----BEGIN PUBLIC KEY-----) is also accepted as --trust-root; such a key has no key id and is tried for every signature.

3. Verify offline (disconnected)

stella evidence verify-offline portable-evidence-bundle.tgz --trust-root evidence-signing-keys.jwks.json --output json

The command makes no network calls and needs no backend configuration. It runs these steps in order and stops at the first failing step:

StepCheck
inputThe bundle file exists and is readable.
archiveThe bytes are a gzip tar archive.
proofdsse_envelope.json with at least one signature, rekor/tile.tar and rekor/checkpoint.json are present.
integrityThe archive-local checks of EvidencePortableBundleOfflineVerifier: checksums.txt coverage, manifest file digests and sizes, the manifest.sig payload binding, the signed payload digest, tile coverage of the artifact and canonical BOM digests, and bundle.json redaction. Malformed content (for example a manifest.json without digests) and a dsse_envelope.json that repeats a member name, exactly or with different letter case, also fail here.
trust-root--trust-root was given and holds at least one supported key (Ed25519 or P-256).
signatureA declared key with the signature’s key id verifies the signature over the signed payload bytes.
identityThe signed payload is an evidence bundle manifest; its identity is reported.

Outcomes and exit codes

ExitOutcomeMeaning
0verifiedThe signature verified against a declared key. The report carries the identity read from the signed payload.
1integrity_failedThe bytes changed or do not form the exported package: a checksum, manifest or payload binding does not match, the content is malformed, or the signature envelope repeats a member name so that readers could take different signed content from it.
2input_unavailableThe bundle file is missing or unreadable.
3proof_missingThe signed payload, its signature or the transparency material is absent.
4trust_root_missingNo --trust-root was given. Archive integrity alone does not establish who signed the bundle.
5trust_root_invalidThe trust root file is missing, unreadable, holds no supported key, or repeats a member name so that the declared key is ambiguous.
6signature_invalidA declared key carries the signature’s key id, but the signature does not verify: the signed content changed, or a different key was declared under that id.
7signature_untrustedNo declared key carries the signature’s key id.

Any non-zero exit is a failure. The report never states an identity unless the outcome is verified. Member names in the signature envelope, the signed payload and the trust root are matched exactly (case-sensitive), the same way the archive-local checks match them.

Report contents (--output json)

The report is printed for every outcome, including failures.

Keep the JSON report with the bundle. Evidence has no contract that receives offline verification results, so the result is not visible in the Console.

Limits of the portable bundle

Manual archive inspection (no signature verification)

With only tar and sha256sum a reader can confirm that the portable archive is internally consistent, which is not verification:

  1. mkdir bundle && tar -xzf portable-evidence-bundle.tgz -C bundle && cd bundle
  2. sed 's/\r$//' checksums.txt | grep -v '^#' | sha256sum -c -

checksums.txt starts with a # comment line, and its line endings follow the platform of the producing host, hence the filter. The bundled verify-offline.sh runs the checksum step and then prints stella evidence verify and stella devportal verify as next steps; neither of those commands verifies the signature against a trust root. Use step 3 above.

The sealed bundle.tgz returned by the bundle download route is not a self-verifying package. It holds manifest.json, signature.json, bundle.json, instructions.txt and a checksums.txt that lists the digests of the bundle’s materials (which the archive does not contain) and a root line with the bundle root hash. Use the portable bundle for offline verification.

Protected Evidence Requirements

Production sealed bundles and Decision Capsules must include durable signer evidence. EvidenceLocker fails closed when a required timestamp authority returns no timestamp, when only transient key generation is available, or when capsule sealing would persist an unsigned result. The local/test harness exceptions are explicit configuration switches only: Signing:AllowEphemeralKeyMaterial=true and Signing:AllowUnsignedCapsules=true.

Offline reviewers should treat a production capsule with signingStatus="unsigned" as invalid protected evidence unless the package explicitly identifies itself as a local/test fixture.

Verification authority

The archive-local checks have one implementation: StellaOps.EvidenceLocker.Core.Services.EvidencePortableBundleOfflineVerifier (BCL-only, so the offline CLI path does not load the Evidence server infrastructure). stella evidence verify-offline runs it and adds the steps that need trust material and crypto providers: proof presence, the signature against the declared trust root, and the signed identity.

Fixtures

A portable release decision bundle produced by the Evidence seal, packaging and key-publication code, with the matching published key set, lives under src/Cli/__Tests/StellaOps.Cli.Tests/Fixtures/EvidencePortable/; its README records how it was generated. The CLI tests derive every negative case from it.

References