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
7.3 KiB
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:
bundle-manifest.json→ Bundle creation metadatamanifest.json→ Evidence locker snapshotattestation.json→ Cryptographic attestationtimeline.json→ Audit trail
Offline Verification
For fully air-gapped environments:
- Transfer bundle via approved media
- Extract to isolated verification system
- Run verification scripts without network
- Document verification results for audit
Support
For questions or issues:
- Review bundle contents with
jqand standard Unix tools - Check timeline for error codes and messages
- Contact StellaOps support with bundle ID and merkle root