docs consolidation and others
This commit is contained in:
584
docs/modules/airgap/guides/proof-chain-verification.md
Normal file
584
docs/modules/airgap/guides/proof-chain-verification.md
Normal file
@@ -0,0 +1,584 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user