# Proof Chain Verification in Air-Gap Mode > **Version**: 1.0.0 > **Last Updated**: 2025-12-17 > **Related**: [Proof Chain API](../api/proofs.md), [Key Rotation Runbook](../operations/key-rotation-runbook.md) This document describes how to verify proof chains in air-gapped (offline) environments where Rekor transparency log access is unavailable. --- ## Overview Proof chains in StellaOps consist of cryptographically-linked attestations: 1. **Evidence statements** - Raw vulnerability findings 2. **Reasoning statements** - Policy evaluation traces 3. **VEX verdict statements** - Final vulnerability status determinations 4. **Graph root statements** - Merkle root commitments to graph analysis results 5. **Proof spine** - Merkle tree aggregating all components In online mode, proof chains include Rekor inclusion proofs for transparency. In air-gap mode, verification proceeds without Rekor but maintains cryptographic integrity. --- ## Verification Levels ### Level 1: Content-Addressed ID Verification Verifies that content-addressed IDs match payload hashes. ```bash # Verify a proof bundle ID stellaops proof verify --offline \ --proof-bundle sha256:1a2b3c4d... \ --level content-id # Expected output: # ✓ Content-addressed ID verified # ✓ Payload hash: sha256:1a2b3c4d... ``` ### Level 2: DSSE Signature Verification Verifies DSSE envelope signatures against trust anchors. ```bash # Verify signatures with local trust anchors stellaops proof verify --offline \ --proof-bundle sha256:1a2b3c4d... \ --anchor-file /path/to/trust-anchors.json \ --level signature # Expected output: # ✓ DSSE signature valid # ✓ Signer: key-2025-prod # ✓ Trust anchor: 550e8400-e29b-41d4-a716-446655440000 ``` ### Level 3: Merkle Path Verification Verifies the proof spine merkle tree structure. ```bash # Verify merkle paths stellaops proof verify --offline \ --proof-bundle sha256:1a2b3c4d... \ --level merkle # Expected output: # ✓ Merkle root verified # ✓ Evidence paths: 3/3 valid # ✓ Reasoning path: valid # ✓ VEX verdict path: valid ``` ### Level 4: Full Verification (Offline) Performs all verification steps except Rekor. ```bash # Full offline verification stellaops proof verify --offline \ --proof-bundle sha256:1a2b3c4d... \ --anchor-file /path/to/trust-anchors.json # Expected output: # Proof Chain Verification # ═══════════════════════ # ✓ Content-addressed IDs verified # ✓ DSSE signatures verified (3 envelopes) # ✓ Merkle paths verified # ⊘ Rekor verification skipped (offline mode) # # Overall: VERIFIED (offline) ``` --- ## Trust Anchor Distribution In air-gap environments, trust anchors must be distributed out-of-band. ### Export Trust Anchors ```bash # On the online system, export trust anchors stellaops anchor export --format json > trust-anchors.json # Verify export integrity sha256sum trust-anchors.json > trust-anchors.sha256 ``` ### Trust Anchor File Format ```json { "version": "1.0", "exportedAt": "2025-12-17T00:00:00Z", "anchors": [ { "trustAnchorId": "550e8400-e29b-41d4-a716-446655440000", "purlPattern": "pkg:*", "allowedKeyids": ["key-2024-prod", "key-2025-prod"], "allowedPredicateTypes": [ "evidence.stella/v1", "reasoning.stella/v1", "cdx-vex.stella/v1", "proofspine.stella/v1" ], "revokedKeys": ["key-2023-prod"], "keyMaterial": { "key-2024-prod": { "algorithm": "ECDSA-P256", "publicKey": "-----BEGIN PUBLIC KEY-----\n..." }, "key-2025-prod": { "algorithm": "ECDSA-P256", "publicKey": "-----BEGIN PUBLIC KEY-----\n..." } } } ] } ``` ### Import Trust Anchors ```bash # On the air-gapped system stellaops anchor import --file trust-anchors.json # Verify import stellaops anchor list ``` --- ## Proof Bundle Distribution ### Export Proof Bundles ```bash # Export a proof bundle for offline transfer stellaops proof export \ --entry sha256:abc123:pkg:npm/lodash@4.17.21 \ --output proof-bundle.zip # Bundle contents: # proof-bundle.zip # ├── proof-spine.json # The proof spine # ├── evidence/ # Evidence statements # │ ├── sha256_e1.json # │ └── sha256_e2.json # ├── reasoning.json # Reasoning statement # ├── vex-verdict.json # VEX verdict statement # ├── envelopes/ # DSSE envelopes # │ ├── evidence-e1.dsse # │ ├── evidence-e2.dsse # │ ├── reasoning.dsse # │ ├── vex-verdict.dsse # │ └── proof-spine.dsse # └── VERIFY.md # Verification instructions ``` ### Verify Exported Bundle ```bash # On the air-gapped system stellaops proof verify --offline \ --bundle-file proof-bundle.zip \ --anchor-file trust-anchors.json ``` --- ## Batch Verification For audits, verify multiple proof bundles efficiently: ```bash # Create a verification manifest cat > verify-manifest.json << 'EOF' { "bundles": [ "sha256:1a2b3c4d...", "sha256:5e6f7g8h...", "sha256:9i0j1k2l..." ], "options": { "checkRekor": false, "failFast": false } } EOF # Run batch verification stellaops proof verify-batch \ --manifest verify-manifest.json \ --anchor-file trust-anchors.json \ --output verification-report.json ``` ### Verification Report Format ```json { "verifiedAt": "2025-12-17T10:00:00Z", "mode": "offline", "anchorsUsed": ["550e8400..."], "results": [ { "proofBundleId": "sha256:1a2b3c4d...", "verified": true, "checks": { "contentId": true, "signature": true, "merklePath": true, "rekorInclusion": null } } ], "summary": { "total": 3, "verified": 3, "failed": 0, "skipped": 0 } } ``` --- ## Graph Root Attestation Verification (Offline) Graph root attestations provide tamper-evident commitment to graph analysis results. In air-gap mode, these attestations can be verified without network access. ### Verify Graph Root Attestation ```bash # Verify a single graph root attestation stellaops graph-root verify --offline \ --envelope graph-root.dsse \ --anchor-file trust-anchors.json # Expected output: # Graph Root Verification # ═══════════════════════ # ✓ DSSE signature verified # ✓ Predicate type: graph-root.stella/v1 # ✓ Graph type: ReachabilityGraph # ✓ Canon version: stella:canon:v1 # ⊘ Rekor verification skipped (offline mode) # # Overall: VERIFIED (offline) ``` ### Verify with Node/Edge Reconstruction When you have the original graph data, you can recompute and verify the Merkle root: ```bash # Verify with reconstruction stellaops graph-root verify --offline \ --envelope graph-root.dsse \ --nodes nodes.json \ --edges edges.json \ --anchor-file trust-anchors.json # Expected output: # Graph Root Verification (with reconstruction) # ═════════════════════════════════════════════ # ✓ DSSE signature verified # ✓ Nodes canonicalized: 1234 entries # ✓ Edges canonicalized: 5678 entries # ✓ Merkle root recomputed: sha256:abc123... # ✓ Merkle root matches claimed: sha256:abc123... # # Overall: VERIFIED (reconstructed) ``` ### Graph Data File Formats **nodes.json** - Array of node identifiers: ```json { "canonVersion": "stella:canon:v1", "nodes": [ "pkg:npm/lodash@4.17.21", "pkg:npm/express@4.18.2", "pkg:npm/body-parser@1.20.0" ] } ``` **edges.json** - Array of edge identifiers: ```json { "canonVersion": "stella:canon:v1", "edges": [ "pkg:npm/express@4.18.2->pkg:npm/body-parser@1.20.0", "pkg:npm/express@4.18.2->pkg:npm/lodash@4.17.21" ] } ``` ### Verification Steps (Detailed) The offline graph root verification algorithm: 1. **Parse DSSE envelope** - Extract payload and signatures 2. **Decode in-toto statement** - Parse subject and predicate 3. **Verify signature** - Check DSSE signature against trust anchor allowed keys 4. **Validate predicate type** - Confirm `graph-root.stella/v1` 5. **Extract Merkle root** - Get claimed root from predicate 6. **If reconstruction requested**: - Load nodes.json and edges.json - Verify canon version matches predicate - Sort nodes lexicographically - Sort edges lexicographically - Concatenate sorted lists - Build SHA-256 Merkle tree - Compare computed root to claimed root 7. **Emit verification result** ### Programmatic Verification (.NET) ```csharp using StellaOps.Attestor.GraphRoot; // Load trust anchors var anchors = await TrustAnchors.LoadFromFileAsync("trust-anchors.json"); // Create verifier var verifier = new GraphRootAttestor(signer, canonicalJsonSerializer); // Load envelope var envelope = await DsseEnvelope.LoadAsync("graph-root.dsse"); // Verify without reconstruction var result = await verifier.VerifyAsync( envelope, trustAnchors: anchors, verifyRekor: false); // Verify with reconstruction var nodeIds = new[] { "pkg:npm/lodash@4.17.21", "pkg:npm/express@4.18.2" }; var edgeIds = new[] { "pkg:npm/express@4.18.2->pkg:npm/lodash@4.17.21" }; var fullResult = await verifier.VerifyAsync( envelope, nodeIds: nodeIds, edgeIds: edgeIds, trustAnchors: anchors, verifyRekor: false); Console.WriteLine($"Verified: {fullResult.IsValid}"); Console.WriteLine($"Merkle root: {fullResult.MerkleRoot}"); ``` ### Integration with Proof Spine Graph roots can be included in proof spines for comprehensive verification: ```bash # Export proof bundle with graph roots stellaops proof export \ --entry sha256:abc123:pkg:npm/lodash@4.17.21 \ --include-graph-roots \ --output proof-bundle.zip # Bundle now includes: # proof-bundle.zip # ├── proof-spine.json # ├── evidence/ # ├── reasoning.json # ├── vex-verdict.json # ├── graph-roots/ # Graph root attestations # │ ├── reachability.dsse # │ └── dependency.dsse # ├── envelopes/ # └── VERIFY.md # Verify with graph roots stellaops proof verify --offline \ --bundle-file proof-bundle.zip \ --verify-graph-roots \ --anchor-file trust-anchors.json ``` ### Determinism Requirements For offline verification to succeed: 1. **Same canonicalization** - Use `stella:canon:v1` consistently 2. **Same ordering** - Lexicographic sort for nodes and edges 3. **Same encoding** - UTF-8 for all string operations 4. **Same hash algorithm** - SHA-256 for Merkle tree --- ## Key Rotation in Air-Gap Mode When keys are rotated, trust anchor updates must be distributed: ### 1. Export Updated Anchors ```bash # On online system after key rotation stellaops anchor export --since 2025-01-01 > anchor-update.json sha256sum anchor-update.json > anchor-update.sha256 ``` ### 2. Verify and Import Update ```bash # On air-gapped system sha256sum -c anchor-update.sha256 stellaops anchor import --file anchor-update.json --merge # Verify key history stellaops anchor show --anchor-id 550e8400... --show-history ``` ### 3. Temporal Verification When verifying old proofs after key rotation: ```bash # Verify proof signed with now-revoked key stellaops proof verify --offline \ --proof-bundle sha256:old-proof... \ --anchor-file trust-anchors.json \ --at-time "2024-06-15T12:00:00Z" # The verification uses key validity at the specified time ``` --- ## Manual Verification (No CLI) For environments without the StellaOps CLI, manual verification is possible: ### 1. Verify Content-Addressed ID ```bash # Extract payload from DSSE envelope jq -r '.payload' proof-spine.dsse | base64 -d > payload.json # Compute hash sha256sum payload.json # Compare with proof bundle ID ``` ### 2. Verify DSSE Signature ```python #!/usr/bin/env python3 import json import base64 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import load_pem_public_key def verify_dsse(envelope_path, public_key_pem): """Verify a DSSE envelope signature.""" with open(envelope_path) as f: envelope = json.load(f) payload_type = envelope['payloadType'] payload = base64.b64decode(envelope['payload']) # Build PAE (Pre-Authentication Encoding) pae = f"DSSEv1 {len(payload_type)} {payload_type} {len(payload)} ".encode() + payload public_key = load_pem_public_key(public_key_pem.encode()) for sig in envelope['signatures']: signature = base64.b64decode(sig['sig']) try: public_key.verify(signature, pae, ec.ECDSA(hashes.SHA256())) print(f"✓ Signature valid for keyid: {sig['keyid']}") return True except Exception as e: print(f"✗ Signature invalid: {e}") return False ``` ### 3. Verify Merkle Path ```python #!/usr/bin/env python3 import json import hashlib def verify_merkle_path(leaf_hash, path, root_hash, leaf_index): """Verify a Merkle inclusion path.""" current = bytes.fromhex(leaf_hash) index = leaf_index for sibling in path: sibling_bytes = bytes.fromhex(sibling) if index % 2 == 0: # Current is left child combined = current + sibling_bytes else: # Current is right child combined = sibling_bytes + current current = hashlib.sha256(combined).digest() index //= 2 computed_root = current.hex() if computed_root == root_hash: print("✓ Merkle path verified") return True else: print(f"✗ Merkle root mismatch: {computed_root} != {root_hash}") return False ``` --- ## Exit Codes Offline verification uses the same exit codes as online: | Code | Meaning | CI/CD Action | |------|---------|--------------| | 0 | Verification passed | Proceed | | 1 | Verification failed | Block | | 2 | System error | Retry/investigate | --- ## Troubleshooting ### Missing Trust Anchor ``` Error: No trust anchor found for keyid "key-2025-prod" ``` **Solution**: Import updated trust anchors from online system. ### Key Not Valid at Time ``` Error: Key "key-2024-prod" was revoked at 2024-12-01, before proof signature at 2025-01-15 ``` **Solution**: This indicates the proof was signed after key revocation. Investigate the signature timestamp. ### Merkle Path Invalid ``` Error: Merkle path verification failed for evidence sha256:e1... ``` **Solution**: The proof bundle may be corrupted. Re-export from online system. --- ## Related Documentation - [Proof Chain API Reference](../api/proofs.md) - [Key Rotation Runbook](../operations/key-rotation-runbook.md) - [Portable Evidence Bundle Verification](portable-evidence-bundle-verification.md) - [Offline Bundle Format](offline-bundle-format.md)