Files
git.stella-ops.org/docs/modules/attestor/graph-root-attestation.md
2025-12-24 21:45:46 +02:00

239 lines
9.5 KiB
Markdown

# Graph Root Attestation
## Overview
Graph root attestation is a mechanism for creating cryptographically signed, content-addressed proofs of graph state. It enables offline verification that replayed graphs match the original attested state by computing a Merkle root from sorted node/edge IDs and input digests, then wrapping it in a DSSE envelope with an in-toto statement.
## Purpose
Graph root attestations solve the problem of proving graph authenticity without reconstructing the entire proof chain. They enable:
- **Offline Verification**: Download an attestation, recompute the root from stored nodes/edges, compare
- **Audit Snapshots**: Point-in-time proof of graph state for compliance
- **Evidence Linking**: Reference attested roots (not transient IDs) in evidence chains
- **Transparency**: Optional Rekor publication for public auditability
## Architecture
### Components
```
┌─────────────────────────────────────────────────────────────────┐
│ GraphRootAttestor │
├─────────────────────────────────────────────────────────────────┤
│ AttestAsync(request) → GraphRootAttestationResult │
│ VerifyAsync(envelope, nodes, edges) → VerificationResult │
└─────────────────┬───────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ IMerkleRootComputer │
├─────────────────────────────────────────────────────────────────┤
│ ComputeRoot(leaves) → byte[] │
│ Algorithm → "sha256" │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ EnvelopeSignatureService │
├─────────────────────────────────────────────────────────────────┤
│ Sign(payload, key) → EnvelopeSignature │
└─────────────────────────────────────────────────────────────────┘
```
### Data Flow
1. **Request** → Sorted node/edge IDs + input digests
2. **Merkle Tree** → Compute SHA-256 root from leaves
3. **In-Toto Statement** → Build attestation with predicate
4. **Canonicalize** → JCS (RFC 8785) with version marker
5. **Sign** → DSSE envelope with Ed25519/ECDSA
6. **Store/Publish** → CAS storage + optional Rekor
## Graph Types
The `GraphType` enum identifies what kind of graph is being attested:
| GraphType | Description |
|-----------|-------------|
| `Unknown` | Unspecified graph type |
| `CallGraph` | Function/method call relationships |
| `DependencyGraph` | Package/library dependencies (SBOM) |
| `SbomGraph` | SBOM component graph |
| `EvidenceGraph` | Linked evidence records |
| `PolicyGraph` | Policy decision trees |
| `ProofSpine` | Proof chain spine segments |
| `ReachabilityGraph` | Code reachability analysis |
| `VexLinkageGraph` | VEX statement linkages |
| `Custom` | Application-specific graph |
## Models
### GraphRootAttestationRequest
Input to the attestation service:
```csharp
public sealed record GraphRootAttestationRequest
{
public required GraphType GraphType { get; init; }
public required IReadOnlyList<string> NodeIds { get; init; }
public required IReadOnlyList<string> EdgeIds { get; init; }
public required string PolicyDigest { get; init; }
public required string FeedsDigest { get; init; }
public required string ToolchainDigest { get; init; }
public required string ParamsDigest { get; init; }
public required string ArtifactDigest { get; init; }
public IReadOnlyList<string> EvidenceIds { get; init; } = [];
public bool PublishToRekor { get; init; } = false;
public string? SigningKeyId { get; init; }
}
```
### GraphRootAttestation (In-Toto Statement)
The attestation follows the in-toto Statement/v1 format:
```json
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "sha256:abc123...",
"digest": { "sha256": "abc123..." }
},
{
"name": "sha256:artifact...",
"digest": { "sha256": "artifact..." }
}
],
"predicateType": "https://stella-ops.org/attestation/graph-root/v1",
"predicate": {
"graphType": "DependencyGraph",
"rootHash": "sha256:abc123...",
"rootAlgorithm": "sha256",
"nodeCount": 1247,
"edgeCount": 3891,
"nodeIds": ["node-a", "node-b", ...],
"edgeIds": ["edge-1", "edge-2", ...],
"inputs": {
"policyDigest": "sha256:policy...",
"feedsDigest": "sha256:feeds...",
"toolchainDigest": "sha256:tools...",
"paramsDigest": "sha256:params..."
},
"evidenceIds": ["ev-1", "ev-2"],
"canonVersion": "stella:canon:v1",
"computedAt": "2025-12-26T10:30:00Z",
"computedBy": "stellaops/attestor/graph-root",
"computedByVersion": "1.0.0"
}
}
```
## Merkle Root Computation
The root is computed from leaves in this deterministic order:
1. **Sorted node IDs** (lexicographic, ordinal)
2. **Sorted edge IDs** (lexicographic, ordinal)
3. **Policy digest**
4. **Feeds digest**
5. **Toolchain digest**
6. **Params digest**
Each leaf is SHA-256 hashed, then combined pairwise until a single root remains.
```
ROOT
/ \
H(L12) H(R12)
/ \ / \
H(n1) H(n2) H(e1) H(policy)
```
## Usage
### Creating an Attestation
```csharp
var services = new ServiceCollection();
services.AddGraphRootAttestation(sp => keyId => GetSigningKey(keyId));
var provider = services.BuildServiceProvider();
var attestor = provider.GetRequiredService<IGraphRootAttestor>();
var request = new GraphRootAttestationRequest
{
GraphType = GraphType.DependencyGraph,
NodeIds = graph.Nodes.Select(n => n.Id).ToList(),
EdgeIds = graph.Edges.Select(e => e.Id).ToList(),
PolicyDigest = "sha256:abc...",
FeedsDigest = "sha256:def...",
ToolchainDigest = "sha256:ghi...",
ParamsDigest = "sha256:jkl...",
ArtifactDigest = imageDigest
};
var result = await attestor.AttestAsync(request);
Console.WriteLine($"Root: {result.RootHash}");
Console.WriteLine($"Nodes: {result.NodeCount}");
Console.WriteLine($"Edges: {result.EdgeCount}");
```
### Verifying an Attestation
```csharp
var envelope = LoadEnvelope("attestation.dsse.json");
var nodes = LoadNodes("nodes.ndjson");
var edges = LoadEdges("edges.ndjson");
var result = await attestor.VerifyAsync(envelope, nodes, edges);
if (result.IsValid)
{
Console.WriteLine($"✓ Verified: {result.ComputedRoot}");
Console.WriteLine($" Nodes: {result.NodeCount}");
Console.WriteLine($" Edges: {result.EdgeCount}");
}
else
{
Console.WriteLine($"✗ Failed: {result.FailureReason}");
Console.WriteLine($" Expected: {result.ExpectedRoot}");
Console.WriteLine($" Computed: {result.ComputedRoot}");
}
```
## Offline Verification Workflow
1. **Obtain attestation**: Download DSSE envelope from storage or transparency log
2. **Verify signature**: Check envelope signature against trusted public keys
3. **Extract predicate**: Parse `GraphRootPredicate` from the payload
4. **Fetch graph data**: Download nodes and edges by ID from CAS
5. **Recompute root**: Apply Merkle tree algorithm to node/edge IDs + input digests
6. **Compare**: Computed root must match `predicate.RootHash`
## Determinism Guarantees
Graph root attestations are fully deterministic:
- **Sorting**: All IDs sorted lexicographically (ordinal comparison)
- **Canonicalization**: RFC 8785 JCS with `stella:canon:v1` version marker
- **Hashing**: SHA-256 only
- **Timestamps**: UTC ISO-8601 (not included in root computation)
Same inputs always produce the same root hash, enabling replay verification.
## Security Considerations
- **Signature verification**: Always verify DSSE envelope signatures before trusting attestations
- **Key management**: Use short-lived signing keys; rotate regularly
- **Transparency**: Publish to Rekor for tamper-evident audit trail
- **Input validation**: Validate all digests are properly formatted before attestation
## Related Documentation
- [DSSE Envelopes](./dsse-envelopes.md) - Envelope format and signing
- [Proof Chain](./proof-chain.md) - Overall proof chain architecture
- [Canonical JSON](../../modules/platform/canonical-json.md) - Canonicalization scheme