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
255 lines
7.3 KiB
Markdown
255 lines
7.3 KiB
Markdown
# 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]} <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:
|
|
|
|
```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
|