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

9.5 KiB

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:

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:

{
  "_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

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

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