Files
git.stella-ops.org/docs/airgap/portable-evidence-bundle-verification.md
StellaOps Bot 1c6730a1d2
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
up
2025-11-28 00:45:16 +02:00

7.3 KiB

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

# 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:

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

#!/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]} <bundle-directory>")
        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:

# 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:

# 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:

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:

# 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