# Portable Evidence Bundle Verification Guide This document describes how Advisory AI teams can verify the integrity and authenticity of portable evidence bundles produced by StellaOps Excititor for sealed deployments. ## Overview Portable evidence bundles are self-contained ZIP archives that include: - Evidence locker manifest with cryptographic Merkle root - DSSE attestation envelope (when signing is enabled) - Raw evidence items organized by provider - Audit timeline events - Bundle manifest with content index ## Bundle Structure ``` evidence-bundle-{tenant}-{timestamp}.zip ├── manifest.json # VexLockerManifest with Merkle root ├── attestation.json # DSSE envelope (optional) ├── evidence/ │ └── {provider}/ │ └── sha256_{digest}.json ├── timeline.json # Audit timeline events ├── bundle-manifest.json # Index of all contents └── VERIFY.md # Verification instructions ``` ## Verification Steps ### Step 1: Extract and Validate Structure ```bash # Extract the bundle unzip evidence-bundle-*.zip -d evidence-bundle/ # Verify expected files exist ls -la evidence-bundle/ # Should see: manifest.json, bundle-manifest.json, evidence/, timeline.json, VERIFY.md ``` ### Step 2: Verify Evidence Item Integrity Each evidence item's content hash must match its filename: ```bash cd evidence-bundle/evidence # For each provider directory for provider in */; do for file in "$provider"*.json; do # Extract expected hash from filename (sha256_xxxx.json -> xxxx) expected=$(basename "$file" .json | sed 's/sha256_//') # Compute actual hash actual=$(sha256sum "$file" | cut -d' ' -f1) if [ "$expected" != "$actual" ]; then echo "MISMATCH: $file" fi done done ``` ### Step 3: Verify Merkle Root The Merkle root provides cryptographic proof that all evidence items are included without modification. #### Python Verification Script ```python #!/usr/bin/env python3 import json import hashlib from pathlib import Path def compute_merkle_root(hashes): """Compute Merkle root from list of hex hashes.""" if len(hashes) == 0: return hashlib.sha256(b'').hexdigest() if len(hashes) == 1: return hashes[0] # Pad to even number if len(hashes) % 2 != 0: hashes = hashes + [hashes[-1]] # Compute next level next_level = [] for i in range(0, len(hashes), 2): combined = bytes.fromhex(hashes[i] + hashes[i+1]) next_level.append(hashlib.sha256(combined).hexdigest()) return compute_merkle_root(next_level) def verify_bundle(bundle_path): """Verify a portable evidence bundle.""" bundle_path = Path(bundle_path) # Load manifest with open(bundle_path / 'manifest.json') as f: manifest = json.load(f) # Extract hashes, sorted by observationId then providerId items = sorted(manifest['items'], key=lambda x: (x['observationId'], x['providerId'].lower())) hashes = [] for item in items: content_hash = item['contentHash'] # Strip sha256: prefix if present if content_hash.startswith('sha256:'): content_hash = content_hash[7:] hashes.append(content_hash.lower()) # Compute Merkle root computed_root = 'sha256:' + compute_merkle_root(hashes) expected_root = manifest['merkleRoot'] if computed_root == expected_root: print(f"✓ Merkle root verified: {computed_root}") return True else: print(f"✗ Merkle root mismatch!") print(f" Expected: {expected_root}") print(f" Computed: {computed_root}") return False if __name__ == '__main__': import sys if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} ") sys.exit(1) success = verify_bundle(sys.argv[1]) sys.exit(0 if success else 1) ``` ### Step 4: Verify Attestation (if present) When `attestation.json` exists, verify the DSSE envelope: ```bash # Check if attestation exists if [ -f "evidence-bundle/attestation.json" ]; then # Extract attestation metadata jq '.' evidence-bundle/attestation.json # Verify signature using appropriate tool # For Sigstore/cosign attestations: # cosign verify-attestation --type custom ... fi ``` #### Attestation Fields | Field | Description | |-------|-------------| | `dsseEnvelope` | Base64-encoded DSSE envelope | | `envelopeDigest` | SHA-256 hash of the envelope | | `predicateType` | in-toto predicate type URI | | `signatureType` | Signature algorithm (e.g., "ES256") | | `keyId` | Signing key identifier | | `issuer` | Certificate issuer | | `subject` | Certificate subject | | `signedAt` | Signing timestamp (ISO-8601) | | `transparencyLogRef` | Rekor transparency log entry URL | ### Step 5: Validate Timeline The timeline provides audit trail of bundle creation: ```bash # View timeline events jq '.' evidence-bundle/timeline.json # Check for any failed events jq '.[] | select(.errorCode != null)' evidence-bundle/timeline.json ``` #### Timeline Event Types | Event Type | Description | |------------|-------------| | `airgap.import.started` | Bundle import initiated | | `airgap.import.completed` | Import succeeded | | `airgap.import.failed` | Import failed (check errorCode) | ## Error Codes Reference | Code | Description | Resolution | |------|-------------|------------| | `AIRGAP_EGRESS_BLOCKED` | External URL blocked in sealed mode | Use mirror/portable media | | `AIRGAP_SOURCE_UNTRUSTED` | Publisher not allowlisted | Contact administrator | | `AIRGAP_SIGNATURE_MISSING` | Required signature absent | Re-export with signing | | `AIRGAP_SIGNATURE_INVALID` | Signature verification failed | Check key/certificate | | `AIRGAP_PAYLOAD_STALE` | Timestamp exceeds tolerance | Re-create bundle | | `AIRGAP_PAYLOAD_MISMATCH` | Hash doesn't match metadata | Verify transfer integrity | ## Advisory AI Integration ### Quick Integrity Check For automated pipelines, use the bundle manifest: ```python import json with open('bundle-manifest.json') as f: manifest = json.load(f) # Key fields for Advisory AI print(f"Bundle ID: {manifest['bundleId']}") print(f"Merkle Root: {manifest['merkleRoot']}") print(f"Item Count: {manifest['itemCount']}") print(f"Has Attestation: {manifest['hasAttestation']}") ``` ### Evidence Lookup Find evidence for specific observations: ```python # Index evidence by observation ID evidence_index = {e['observationId']: e for e in manifest['evidence']} # Lookup specific observation obs_id = 'obs-123-abc' if obs_id in evidence_index: entry = evidence_index[obs_id] file_path = f"evidence/{entry['providerId']}/sha256_{entry['contentHash'][7:]}.json" ``` ### Provenance Chain Build complete provenance from bundle: 1. `bundle-manifest.json` → Bundle creation metadata 2. `manifest.json` → Evidence locker snapshot 3. `attestation.json` → Cryptographic attestation 4. `timeline.json` → Audit trail ## Offline Verification For fully air-gapped environments: 1. Transfer bundle via approved media 2. Extract to isolated verification system 3. Run verification scripts without network 4. Document verification results for audit ## Support For questions or issues: - Review bundle contents with `jq` and standard Unix tools - Check timeline for error codes and messages - Contact StellaOps support with bundle ID and merkle root