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:
| Step | Check |
|---|---|
input | The bundle file exists and is readable. |
archive | The bytes are a gzip tar archive. |
proof | dsse_envelope.json with at least one signature, rekor/tile.tar and rekor/checkpoint.json are present. |
integrity | The 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). |
signature | A declared key with the signature’s key id verifies the signature over the signed payload bytes. |
identity | The signed payload is an evidence bundle manifest; its identity is reported. |
Outcomes and exit codes
| Exit | Outcome | Meaning |
|---|---|---|
| 0 | verified | The signature verified against a declared key. The report carries the identity read from the signed payload. |
| 1 | integrity_failed | The 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. |
| 2 | input_unavailable | The bundle file is missing or unreadable. |
| 3 | proof_missing | The signed payload, its signature or the transparency material is absent. |
| 4 | trust_root_missing | No --trust-root was given. Archive integrity alone does not establish who signed the bundle. |
| 5 | trust_root_invalid | The trust root file is missing, unreadable, holds no supported key, or repeats a member name so that the declared key is ambiguous. |
| 6 | signature_invalid | A 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. |
| 7 | signature_untrusted | No 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.
artifact: the bundle path, SHA-256 and size.tool:stella evidence verify-offlineand the CLI informational version.trust: the trust root path, its SHA-256, its format (jwksorspki-pem), each key with its key id, type and SPKI SHA-256, and the key that verified the signature.identity: read only from the signed payload: bundle id, kind, creation time, payload type, signing key id, the signed metadata (for a release decision bundle:schema,releaseId,releaseName,releaseVersion,targetEnvironment,approvalId,decisionId,outcome,mode,policyId,decidedAt) and the subject digests of the signed entries.checksanderrors.
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
- Only the signed payload is cryptographically bound. Evidence signs the canonical bundle manifest payload (the content of
dsse_envelope.jsonpayload) directly; the signature is not computed over a DSSE pre-authentication encoding, so a generic DSSE verifier rejects it.manifest.json,manifest.sig,checksums.txt,canonical_bom.json,bundle.jsonand therekor/files are unsigned packaging: a reader who rewrites them consistently passes the archive-local checks. The verifier therefore reports identity from the signed payload only, and a changed signed payload fails atsignature. - The
rekor/material is not a transparency-log inclusion proof. The packager generatesrekor/tile.tarandrekor/checkpoint.jsonfrom the bundle metadata; the tile lists the digests it covers and carries no checkpoint signature. The verifier requires their presence and checks their coverage, but they do not establish that an entry was included in a log. - Supported trust keys: Ed25519 (
OKP) and P-256 (EC). Other key types fail astrust_root_invalid. - The trust root is only as trustworthy as its declaration. A key set fetched from the installation proves nothing by itself; compare each SPKI SHA-256 with the declared value.
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:
mkdir bundle && tar -xzf portable-evidence-bundle.tgz -C bundle && cd bundlesed '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
- Manifest schema:
docs/modules/evidence/locker/schemas/bundle.manifest.schema.json - Checksums schema:
docs/modules/evidence/locker/schemas/checksums.schema.json - Packaging:
docs/modules/evidence/locker/bundle-packaging.md
