ADR-020: CGS Merkle Tree Implementation
Formerly
docs/technical/adr/0042-cgs-merkle-tree-implementation.md(legacy ADR 0042).
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2025-12-29 |
| Decision makers | Backend team, Security team, Attestation team (consulted) |
| Review date | 2026-06-29 (re-evaluate whether code duplication warrants a shared library) |
| Related | ADR-021 (Fulcio keyless signing) |
For: engineers working on the Stella Ops verdict pipeline (CGS hashing, evidence packs, attestation). This ADR records why the Canonical Graph Signature (CGS) hash is computed by a small, purpose-built Merkle tree inside VerdictBuilderService rather than reusing the Attestor proof-chain builder.
Context
The CGS (Canonical Graph Signature) system requires deterministic hash computation for verdicts. The decision was whether to:
- Reuse the existing
StellaOps.Attestor.ProofChainMerkle tree builder, or - Build a custom Merkle tree implementation in
VerdictBuilderService.
Requirements
- Determinism: Same evidence must always produce identical CGS hash
- Order Independence: VEX document ordering should not affect hash (sorted internally)
- Cross-Platform: Identical hash on Windows, macOS, Linux (glibc), Linux (musl), BSD
- Leaf Composition: Specific ordering of evidence components (SBOM, VEX sorted, reachability, policy lock)
Existing ProofChain Merkle Builder
Located at: src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/
Pros:
- Already implements Merkle tree construction
- Tested and proven in production
- Handles parent/child attestation chains
Cons:
- Designed for attestation chains, not evidence hashing
- Includes attestation-specific metadata in hash
- Doesn’t support custom leaf ordering required for CGS
- Would require modifications that might break existing attestation behavior
Decision
Build custom Merkle tree implementation in VerdictBuilderService.
Rationale
Separation of Concerns: CGS hash computation has different requirements than attestation chain verification
Full Control Over Determinism: Custom implementation allows:
- Explicit leaf ordering: SBOM → VEX (sorted) → Reachability → PolicyLock
- VEX document sorting by content hash (not insertion order)
- Culture-invariant string comparison (
StringComparer.Ordinal)
Simplicity: ~50 lines of code vs modifying 500+ lines in ProofChain
No Breaking Changes: Doesn’t affect existing attestation infrastructure
Implementation
// VerdictBuilderService.cs
private static string ComputeCgsHash(EvidencePack evidence, PolicyLock policyLock)
{
// Build Merkle tree from evidence components (sorted for determinism)
var leaves = new List<string>
{
ComputeHash(evidence.SbomCanonJson),
ComputeHash(evidence.FeedSnapshotDigest)
};
// Add VEX digests in sorted order (ORDER-CRITICAL for determinism!)
foreach (var vex in evidence.VexCanonJson.OrderBy(v => v, StringComparer.Ordinal))
{
leaves.Add(ComputeHash(vex));
}
// Add reachability if present
if (!string.IsNullOrEmpty(evidence.ReachabilityGraphJson))
{
leaves.Add(ComputeHash(evidence.ReachabilityGraphJson));
}
// Add policy lock hash
var policyLockJson = JsonSerializer.Serialize(policyLock, CanonicalJsonOptions);
leaves.Add(ComputeHash(policyLockJson));
// Build Merkle root
var merkleRoot = BuildMerkleRoot(leaves);
return $"cgs:sha256:{merkleRoot}";
}
private static string BuildMerkleRoot(List<string> leaves)
{
if (leaves.Count == 0)
return ComputeHash("");
if (leaves.Count == 1)
return leaves[0];
var level = leaves.ToList();
while (level.Count > 1)
{
var nextLevel = new List<string>();
for (int i = 0; i < level.Count; i += 2)
{
if (i + 1 < level.Count)
{
// Combine two hashes
var combined = level[i] + level[i + 1];
nextLevel.Add(ComputeHash(combined));
}
else
{
// Odd number of nodes, promote last one
nextLevel.Add(level[i]);
}
}
level = nextLevel;
}
return level[0];
}
private static string ComputeHash(string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
var hashBytes = SHA256.HashData(bytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
Consequences
Positive
- Full control over CGS hash computation logic.
- No risk of breaking existing attestation chains.
- Simple, testable implementation (~50 lines).
- Explicit ordering guarantees determinism.
- Cross-platform verified (Windows, macOS, Linux, Alpine, Debian).
Negative
- Code duplication with ProofChain (minimal — different use case).
- A separate Merkle tree implementation to maintain (low maintenance burden).
Neutral
- The custom implementation is documented and locked in by tests (
CgsDeterminismTests.cs). - Future option: extract shared Merkle tree primitives if a second consumer appears.
Alternatives Considered
Alternative 1: Modify ProofChain Builder
Rejected because:
- Would require adding configuration options to ProofChain
- Risk of breaking existing attestation behavior
- Increased complexity for both use cases
- Tight coupling between verdict and attestation systems
Alternative 2: Use Third-Party Merkle Tree Library
Rejected because:
- External dependency for ~50 lines of code
- Less control over ordering and hash format
- Potential platform-specific issues
- Security review overhead
Alternative 3: Single-Level Hash (No Merkle Tree)
Rejected because:
- Loses incremental verification capability
- Can’t prove individual evidence components without full evidence pack
- Less efficient for large evidence packs (can’t skip unchanged components)
Verification
Test Coverage
File: src/__Tests/Determinism/CgsDeterminismTests.cs
- Golden File Test: Known evidence produces expected hash
- 10-Iteration Stability: Same input produces identical hash 10 times
- VEX Order Independence: VEX document ordering doesn’t affect hash
- Reachability Inclusion: Reachability graph changes hash predictably
- Policy Lock Versioning: Different policy versions produce different hashes
Cross-Platform Verification
CI workflow: .gitea/workflows/cross-platform-determinism.yml
- Windows (Win32 CRT)
- macOS (BSD libc)
- Linux Ubuntu (glibc)
- Linux Alpine (musl libc)
- Linux Debian (glibc)
All platforms produce an identical CGS hash for the same input.
Migration
No migration required - this is a new feature.
References
- ADR index
- Determinism Developer Guide
- Sprint: SPRINT_20251229_001_001_BE_cgs_infrastructure (archived)
- Implementation:
src/__Libraries/StellaOps.Verdict/VerdictBuilderService.cs - Tests:
src/__Tests/Determinism/CgsDeterminismTests.cs - ProofChain:
src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/
