239 lines
9.5 KiB
Markdown
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
|