up
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
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
This commit is contained in:
254
docs/airgap/portable-evidence-bundle-verification.md
Normal file
254
docs/airgap/portable-evidence-bundle-verification.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user