Files
git.stella-ops.org/docs/product-advisories/archived/23-Nov-2025 - Verifying Binary Reachability via DSSE Envelopes.md
2025-11-23 23:44:35 +02:00

28 KiB
Raw Blame History

Heres a crisp idea I think youll like: attested, offlineverifiable call graphs for binaries.

abstract graph with signed edges concept

The gist

  • Goal: Make binary reachability (who calls whom) something an auditor can replay deterministically, even airgapped.

  • How:

    1. Build the call graph for ELF/PE/MachO.
    2. Seal each edge (caller → callee) as its own artifact and sign it in a DSSE (intoto envelope).
    3. Bundle a reachability graph manifest listing all edgeartifacts + hashes of the inputs (binary, debug info, decompiler version, lattice/policy config).
    4. Upload edgeattestations to a transparency log (e.g., Rekor v2).
    5. Anyone can later fetch/verifiy the envelopes and replay the analysis identically (same inputs ⇒ same graph).

Why this matters

  • Deterministic audits: “Prove this edge existed at analysis time.” No handwavy “our tool said so last week.”
  • Granular trust: You can quarantine or dispute just one edge without invalidating the whole graph.
  • Supplychain fit: Edgeartifacts compose nicely with SBOM/VEX; you can say “CVE123 is reachable via these signed edges.”

Minimal vocabulary

  • DSSE: A standard envelope that signs the statement (here: an edge) and its subject (binary, buildID, PURLs).
  • Rekor (v2): An appendonly public log for attestations. Inclusion proofs = tamperevidence.
  • Reachability graph: Nodes are functions/symbols; edges are possible calls; roots are entrypoints (exports, handlers, ctors, etc.).

What “bestinclass” looks like in StellaOps

  • Edge schema (per envelope):

    • subject: binary digest + buildid, container image digest (if relevant)
    • caller: {binaryoffset | symbol | demangled | PURL, version}
    • callee: same structure
    • reason: static pattern (PLT/JMP, thunk), init_array/ctors, EH frames, import table, or dynamic witness (trace sample ID)
    • provenance: tool name + version, pipeline run ID, OS, container digest
    • policy-hash: hash of lattice/policy/rules used
    • evidence: (optional) byte slice, CFG snippet hash, or trace excerpt hash
  • Graph manifest (DSSE too):

    • list of edge envelope digests, roots set, toolchain hashes, input feeds, PURL map (component/function ↔ PURL).
  • Verification flow:

    • Verify envelopes → verify Rekor inclusion → recompute edges from inputs (or check cached proofs) → compare manifest hash.
  • Roots you must include: exports, syscalls, signal handlers, .init_array / .ctors, TLS callbacks, exception trampolines, plugin entrypoints, registered callbacks.

Quick implementation plan (C#/.NET 10, fits your stack)

  1. Parsers: ELF/PE/MachO loaders (SymbolTable, DynSym, Reloc/Relr, Import/Export, Sections, BuildID), plus DWARF/PDB stubs when present.

  2. Normalizer: stable symbol IDs (image base + RVA) and PURL resolver (package → function namespace).

  3. Edge extractors (pluggable):

    • Static: import thunks, PLT/JMP, reloctargets, vtable patterns, .init_array, EH tables, jump tables.
    • Dynamic (optional): eBPF/ETW/Perf trace ingester → produce witness edges.
  4. Edge attestation: one DSSE per edge + signer (FIPS/SM/GOST/EIDAS as needed).

  5. Manifest builder: emit graph manifest + policy/lattice hash; store in your Ledger.

  6. Transparency client: Rekor v2 submit/query; cache inclusion proofs for offline bundles.

  7. Verifier: deterministic replay runner; diff engine (edgeset, roots, policy changes).

  8. UI: “Edge provenance” panel; click an edge → see DSSE, Rekor proof, extraction reason.

Practical guardrails

  • Idempotence: Edge IDs = hash(callerID, calleeID, reason, tool-version). Reruns dont duplicate.
  • Explainability: Every edge must say why it exists (pattern or witness).
  • Stripped binaries: fall back to pattern heuristics + patch oracles; mark edges probabilistic with separate attestation type.
  • Hybrid truth: Keep static and dynamic edges distinct; policies can require both for “reachable”.

How this helps your daytoday

  • Compliance: Ship an SBOM/VEX plus a proof pack; auditors can verify offline.
  • Triage: For a CVE, show the exact signed path from entrypoint → vulnerable function; suppresses noisy “maybereachable” claims.
  • Vendor claims: Accept thirdparty edges only if they come with DSSE + Rekor inclusion.

If you want, I can draft the DSSE edge schema (JSON), the manifest format, and the .NET 10 interfaces (IEdgeExtractor, IAttestor, IReplayer, ITransparencyClient) so your midlevel dev can start coding today. Heres a concrete, “give this to a midlevel .NET dev” implementation plan for the attested, offlineverifiable call graph.

Ill assume:

  • Recent .NET (your “.NET 10”)
  • C#
  • You can add NuGet packages
  • You already have (or will have) an “Authority Signer” for DSSE signatures (file key, KMS, etc.)

0. Solution layout (what projects to create)

Create a new solution, e.g. StellaOps.CallGraph.sln with:

  1. StellaOps.CallGraph.Core (Class Library)

    • Domain models (functions, edges, manifests)
    • Interfaces (IBinaryParser, IEdgeExtractor, IAttestor, IRekorClient, etc.)
    • DSSE envelope and helpers
  2. StellaOps.CallGraph.BinaryParsers (Class Library)

    • Implementations of IBinaryParser for:

      • PE/.NET assemblies using System.Reflection.Metadata / PEReader(NuGet)
      • Optionally native PE / ELF using Microsoft.Binary.Parsers(NuGet) or ELFSharp(NuGet)
  3. StellaOps.CallGraph.EdgeExtraction (Class Library)

    • Callgraph builder / edge extractors (import table, IL call instructions, .ctors, etc.)
  4. StellaOps.CallGraph.Attestation (Class Library)

    • DSSE helpers
    • Attestation logic for edges + graph manifest
    • Transparency log (Rekor) client
  5. StellaOps.CallGraph.Cli (Console app)

    • Developer entrypoint: callgraph analyze <binary>

    • Outputs:

      • Edge DSSE envelopes (one per edge, or batched)
      • Graph manifest DSSE
      • Humanreadable summary
  6. StellaOps.CallGraph.Tests (xUnit / NUnit)

    • Unit tests per layer

1. Define the core domain (Core project)

1.1 Records and enums

Create these in StellaOps.CallGraph.Core:

public sealed record BinaryIdentity(
    string LogicalId,          // e.g. build-id or image digest
    string Path,               // local path used during analysis
    string? BuildId,
    string? ImageDigest,       // e.g. OCI digest
    IReadOnlyDictionary<string, string> Digests // sha256, sha512, etc.
);

public sealed record FunctionRef(
    string BinaryLogicalId,    // link to BinaryIdentity.LogicalId
    ulong Rva,                 // Relative virtual address (for native) or metadata token for managed
    string? SymbolName,        // raw symbol if available
    string? DisplayName,       // demangled, user-facing
    string? Purl               // optional: pkg/function mapping
);

public enum EdgeReasonKind
{
    ImportTable,
    StaticCall,         // direct call instruction
    VirtualDispatch,    // via vtable / callvirt
    InitArrayOrCtor,
    ExceptionHandler,
    DynamicWitness      // from traces
}

public sealed record EdgeReason(
    EdgeReasonKind Kind,
    string Detail        // e.g. ".text: call 0x401234", "import: kernel32!CreateFileW"
);

public sealed record ReachabilityEdge(
    FunctionRef Caller,
    FunctionRef Callee,
    EdgeReason Reason,
    string ToolVersion,
    string PolicyHash,           // hash of lattice/policy
    string EvidenceHash          // hash of raw evidence blob (CFG snippet, trace, etc.)
);

Graph manifest:

public sealed record CallGraphManifest(
    string SchemaVersion,
    BinaryIdentity Binary,
    IReadOnlyList<FunctionRef> Roots,
    IReadOnlyList<string> EdgeEnvelopeDigests,   // sha256 of DSSE envelopes
    string PolicyHash,
    IReadOnlyDictionary<string, string> ToolMetadata
);

1.2 Core interfaces

public interface IBinaryParser
{
    BinaryIdentity Identify(string path);
    IReadOnlyList<FunctionRef> GetFunctions(BinaryIdentity binary);
    IReadOnlyList<FunctionRef> GetRoots(BinaryIdentity binary); // exports, entrypoint, handlers, etc.
    BinaryCodeRegion GetCodeRegion(BinaryIdentity binary);      // raw bytes + mappings, see below
}

public sealed record BinaryCodeRegion(
    byte[] Bytes,
    ulong ImageBase,
    IReadOnlyList<SectionInfo> Sections
);

public sealed record SectionInfo(
    string Name,
    ulong Rva,
    uint Size
);

public interface IEdgeExtractor
{
    IReadOnlyList<ReachabilityEdge> Extract(
        BinaryIdentity binary,
        IReadOnlyList<FunctionRef> functions,
        BinaryCodeRegion code);
}

public interface IAttestor
{
    Task<DsseEnvelope> SignEdgeAsync(
        ReachabilityEdge edge,
        BinaryIdentity binary,
        CancellationToken ct = default);

    Task<DsseEnvelope> SignManifestAsync(
        CallGraphManifest manifest,
        CancellationToken ct = default);
}

public interface IRekorClient
{
    Task<RekorEntryRef> UploadAsync(DsseEnvelope envelope, CancellationToken ct = default);
}

public sealed record RekorEntryRef(string LogId, long Index, string Uuid);

(Well define DsseEnvelope in section 3.)


2. Implement minimal PE parser (BinaryParsers project)

Start with PE/.NET only; expand later.

2.1 Add NuGet packages

  • System.Reflection.Metadata (if youre not already on a shared framework that has it)(NuGet)
  • Optionally Microsoft.Binary.Parsers for native PE & ELF; it already knows how to parse PE headers and ELF.(NuGet)

2.2 Implement PeBinaryParser (managed assemblies)

In StellaOps.CallGraph.BinaryParsers:

  • BinaryIdentity Identify(string path)

    • Open file, compute SHA256 (streaming).

    • Use PEReader and MetadataReader to pull:

      • MVID (ModuleDefinition).
      • Assembly name, version.
    • Derive LogicalId, e.g. "dotnet:<AssemblyName>/<Mvid>".

  • IReadOnlyList<FunctionRef> GetFunctions(...)

    • Use PEReaderGetMetadataReader() to enumerate methods:

      • reader.TypeDefinitions → methods in each type.

      • For each MethodDefinition, compute:

        • BinaryLogicalId = binary.LogicalId
        • Rva = methodDef.RelativeVirtualAddress
        • SymbolName = reader.GetString(methodDef.Name)
        • DisplayName = typeFullName + "::" + methodName + signature
        • Purl optional mapping (you can fill later from SBOM).
  • IReadOnlyList<FunctionRef> GetRoots(...)

    • Roots for .NET:

      • Main methods in entry assembly.
      • Public exported API if you want (public methods in public types).
      • Static constructors (.cctor) for public types (init roots).
    • Keep it simple for v1: treat Main as only root.

  • BinaryCodeRegion GetCodeRegion(...)

    • For managed assemblies, you only need IL for now:

      • Use PEReader.GetMethodBody(rva) to get MethodBodyBlock.(Microsoft Learn)
      • For v1, you can assemble permethod IL as you go in the extractor instead of prebuilding a whole region.

Implementation trick: have PeBinaryParser expose a helper:

public MethodBodyBlock? TryGetMethodBody(BinaryIdentity binary, uint rva);

Youll pass this down to the edge extractor.

2.3 (Optional) native PE/ELF

Once managed assemblies work:

  • Add Microsoft.Binary.Parsers for PE + ELF.(NuGet)
  • Or ELFSharp if you prefer.(NuGet)

You can then:

  • Parse import table → edges from “import stub” → imported function.
  • Parse export table → roots (exports).
  • Parse .pdata, .xdata → exception handlers.
  • Parse .init_array (ELF) / TLS callbacks, C runtime init functions.

For an “average dev” first iteration, you can skip native and get a lot of value from .NET assemblies only.


3. DSSE attestation primitives (Attestation project)

You already use DSSE elsewhere, but heres a selfcontained minimal version.

3.1 Envelope models

public sealed record DsseSignature(
    string KeyId,
    string Sig   // base64 signature
);

public sealed record DsseEnvelope(
    string PayloadType,                   // e.g. "application/vnd.stella.call-edge+json"
    string Payload,                       // base64-encoded JSON statement
    IReadOnlyList<DsseSignature> Signatures
);

Statement for a single edge:

public sealed record EdgeStatement(
    string _type,                         // e.g. "https://stella.ops/Statement/CallEdge/v1"
    object subject,                       // Binary info + maybe PURLs
    ReachabilityEdge edge
);

You can loosely follow the DSSE / intoto style: Googles Grafeas Envelope type also matches DSSEs envelope.proto.(Google Cloud)

3.2 Preauthentication encoding (PAE)

Implement DSSE PAE once:

public static class Dsse
{
    public static byte[] PreAuthEncode(string payloadType, byte[] payload)
    {
        static byte[] Cat(params byte[][] parts)
        {
            var total = parts.Sum(p => p.Length);
            var buf = new byte[total];
            var offset = 0;
            foreach (var part in parts)
            {
                Buffer.BlockCopy(part, 0, buf, offset, part.Length);
                offset += part.Length;
            }
            return buf;
        }

        static byte[] Utf8(string s) => Encoding.UTF8.GetBytes(s);

        var header      = Utf8("DSSEv1");
        var pt          = Utf8(payloadType);
        var lenPt       = Utf8(pt.Length.ToString(CultureInfo.InvariantCulture));
        var lenPayload  = Utf8(payload.Length.ToString(CultureInfo.InvariantCulture));
        var space       = Utf8(" ");

        return Cat(header, space, lenPt, space, pt, space, lenPayload, space, payload);
    }
}

3.3 Implement IAttestor

Assume you already have some IAuthoritySigner that can sign arbitrary byte arrays (Ed25519, RSA, etc.).

public sealed class DsseAttestor : IAttestor
{
    private readonly IAuthoritySigner _signer;
    private readonly JsonSerializerOptions _jsonOptions = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    public DsseAttestor(IAuthoritySigner signer) => _signer = signer;

    public async Task<DsseEnvelope> SignEdgeAsync(
        ReachabilityEdge edge,
        BinaryIdentity binary,
        CancellationToken ct = default)
    {
        var stmt = new EdgeStatement(
            _type: "https://stella.ops/Statement/CallEdge/v1",
            subject: new
            {
                type   = "file",
                name   = binary.Path,
                digest = binary.Digests
            },
            edge: edge
        );

        return await SignStatementAsync(
            stmt,
            payloadType: "application/vnd.stella.call-edge+json",
            ct);
    }

    public async Task<DsseEnvelope> SignManifestAsync(
        CallGraphManifest manifest,
        CancellationToken ct = default)
    {
        var stmt = new
        {
            _type   = "https://stella.ops/Statement/CallGraphManifest/v1",
            subject = new
            {
                type   = "file",
                name   = manifest.Binary.Path,
                digest = manifest.Binary.Digests
            },
            manifest
        };

        return await SignStatementAsync(
            stmt,
            payloadType: "application/vnd.stella.call-manifest+json",
            ct);
    }

    private async Task<DsseEnvelope> SignStatementAsync(
        object statement,
        string payloadType,
        CancellationToken ct)
    {
        var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(statement, _jsonOptions);
        var pae          = Dsse.PreAuthEncode(payloadType, payloadBytes);

        var signatureBytes = await _signer.SignAsync(pae, ct).ConfigureAwait(false);
        var keyId          = await _signer.GetKeyIdAsync(ct).ConfigureAwait(false);

        return new DsseEnvelope(
            PayloadType: payloadType,
            Payload: Convert.ToBase64String(payloadBytes),
            Signatures: new[]
            {
                new DsseSignature(keyId, Convert.ToBase64String(signatureBytes))
            });
    }
}

You can plug in:

  • IAuthoritySigner using System.Security.Cryptography.Ed25519 on .NET (or BouncyCastle) for signatures.(Stack Overflow)

4. Edge extraction (EdgeExtraction project)

4.1 Choose strategy per binary type

For managed .NET assemblies the easiest route is to use Mono.Cecil to read IL opcodes.(NuGet)

Add package: Mono.Cecil.

public sealed class ManagedIlEdgeExtractor : IEdgeExtractor
{
    public IReadOnlyList<ReachabilityEdge> Extract(
        BinaryIdentity binary,
        IReadOnlyList<FunctionRef> functions,
        BinaryCodeRegion code)
    {
        // For managed we won't use BinaryCodeRegion; well re-open file with Cecil.
        var result = new List<ReachabilityEdge>();
        var filePath = binary.Path;

        var module = ModuleDefinition.ReadModule(filePath, new ReaderParameters
        {
            ReadSymbols = false
        });

        foreach (var type in module.Types)
        foreach (var method in type.Methods.Where(m => m.HasBody))
        {
            var callerRef = ToFunctionRef(binary, method);

            foreach (var instr in method.Body.Instructions)
            {
                if (instr.OpCode.FlowControl != FlowControl.Call)
                    continue;

                if (instr.Operand is not MethodReference calleeMethod)
                    continue;

                var calleeRef = ToFunctionRef(binary, calleeMethod);

                var edge = new ReachabilityEdge(
                    Caller: callerRef,
                    Callee: calleeRef,
                    Reason: new EdgeReason(
                        EdgeReasonKind.StaticCall,
                        Detail: $"IL {instr.OpCode} {calleeMethod.FullName}"
                    ),
                    ToolVersion: "stella-callgraph/0.1.0",
                    PolicyHash: "TODO",
                    EvidenceHash: "TODO" // later: hash of snippet
                );

                result.Add(edge);
            }
        }

        return result;
    }

    private static FunctionRef ToFunctionRef(BinaryIdentity binary, MethodReference method)
    {
        var displayName = $"{method.DeclaringType.FullName}::{method.Name}";
        return new FunctionRef(
            BinaryLogicalId: binary.LogicalId,
            Rva: (ulong)method.MetadataToken.ToInt32(),
            SymbolName: method.FullName,
            DisplayName: displayName,
            Purl: null
        );
    }
}

Later, you can add:

  • Import table edges (EdgeReasonKind.ImportTable).
  • Virtual dispatch edges, heuristics, etc.
  • Dynamic edges from trace logs (EdgeReasonKind.DynamicWitness).

4.2 Callgraph builder

Add a thin orchestration service:

public sealed class CallGraphBuilder
{
    private readonly IBinaryParser _parser;
    private readonly IReadOnlyList<IEdgeExtractor> _extractors;

    public CallGraphBuilder(
        IBinaryParser parser,
        IEnumerable<IEdgeExtractor> extractors)
    {
        _parser = parser;
        _extractors = extractors.ToList();
    }

    public (BinaryIdentity binary,
            IReadOnlyList<FunctionRef> functions,
            IReadOnlyList<FunctionRef> roots,
            IReadOnlyList<ReachabilityEdge> edges) Build(string path)
    {
        var binary    = _parser.Identify(path);
        var functions = _parser.GetFunctions(binary);
        var roots     = _parser.GetRoots(binary);

        // Optionally, pack code region if needed
        var code = new BinaryCodeRegion(Array.Empty<byte>(), 0, Array.Empty<SectionInfo>());

        var edges = _extractors
            .SelectMany(e => e.Extract(binary, functions, code))
            .ToList();

        return (binary, functions, roots, edges);
    }
}

5. Edge→DSSE and manifest→DSSE wiring

In StellaOps.CallGraph.Attestation, create a coordinator:

public sealed class CallGraphAttestationService
{
    private readonly CallGraphBuilder _builder;
    private readonly IAttestor _attestor;
    private readonly IRekorClient _rekor;

    public CallGraphAttestationService(
        CallGraphBuilder builder,
        IAttestor attestor,
        IRekorClient rekor)
    {
        _builder  = builder;
        _attestor = attestor;
        _rekor    = rekor;
    }

    public async Task<CallGraphAttestationResult> AnalyzeAndAttestAsync(
        string path,
        CancellationToken ct = default)
    {
        var (binary, functions, roots, edges) = _builder.Build(path);

        // 1) Sign each edge
        var edgeEnvelopes = new List<DsseEnvelope>();
        foreach (var edge in edges)
        {
            var env = await _attestor.SignEdgeAsync(edge, binary, ct);
            edgeEnvelopes.Add(env);
        }

        // 2) Compute digests for manifest
        var edgeEnvelopeDigests = edgeEnvelopes
            .Select(e => Crypto.HashSha256(JsonSerializer.SerializeToUtf8Bytes(e)))
            .ToList();

        var manifest = new CallGraphManifest(
            SchemaVersion: "1.0",
            Binary: binary,
            Roots: roots,
            EdgeEnvelopeDigests: edgeEnvelopeDigests,
            PolicyHash: edges.FirstOrDefault()?.PolicyHash ?? "",
            ToolMetadata: new Dictionary<string, string>
            {
                ["builder"]    = "stella-callgraph/0.1.0",
                ["created-at"] = DateTimeOffset.UtcNow.ToString("O")
            });

        var manifestEnvelope = await _attestor.SignManifestAsync(manifest, ct);

        // 3) Publish DSSE envelopes to Rekor (if configured)
        var rekorRefs = new List<RekorEntryRef>();
        foreach (var env in edgeEnvelopes.Append(manifestEnvelope))
        {
            var entry = await _rekor.UploadAsync(env, ct);
            rekorRefs.Add(entry);
        }

        return new CallGraphAttestationResult(
            Manifest: manifest,
            ManifestEnvelope: manifestEnvelope,
            EdgeEnvelopes: edgeEnvelopes,
            RekorEntries: rekorRefs);
    }
}

public sealed record CallGraphAttestationResult(
    CallGraphManifest Manifest,
    DsseEnvelope ManifestEnvelope,
    IReadOnlyList<DsseEnvelope> EdgeEnvelopes,
    IReadOnlyList<RekorEntryRef> RekorEntries);

6. Rekor v2 client (transparency log)

Rekor is a RESTbased transparency log (part of Sigstore).(Sigstore)

For an average dev, keep it simple:

  1. Add HttpClientbased RekorClient:

    • UploadAsync(DsseEnvelope):

      • POST to your Rekor servers /api/v1/log/entries (v1 today; v2 is under active development, but the pattern is similar).
      • Store returned logID, logIndex, uuid in RekorEntryRef.
  2. For offline replay youll want to store:

    • The DSSE envelopes.
    • Rekor entry references (and ideally inclusion proofs, but that can come later).

You dont need to fully implement Merkle tree verification in v1; you can add that when you harden the verifier.


7. CLI for developers (Cli project)

A simple console app gives you fast feedback:

stella-callgraph analyze myapp.dll \
  --output-dir artifacts/callgraph

Implementation sketch:

static async Task<int> Main(string[] args)
{
    var input = args[1]; // TODO: proper parser

    var services = Bootstrap(); // DI container

    var svc = services.GetRequiredService<CallGraphAttestationService>();
    var result = await svc.AnalyzeAndAttestAsync(input);

    // Write DSSE envelopes & manifest as JSON files
    var outDir = Path.Combine("artifacts", "callgraph");
    Directory.CreateDirectory(outDir);

    await File.WriteAllTextAsync(
        Path.Combine(outDir, "manifest.dsse.json"),
        JsonSerializer.Serialize(result.ManifestEnvelope, new JsonSerializerOptions { WriteIndented = true }));

    for (var i = 0; i < result.EdgeEnvelopes.Count; i++)
    {
        var path = Path.Combine(outDir, $"edge-{i:D6}.dsse.json");
        await File.WriteAllTextAsync(path,
            JsonSerializer.Serialize(result.EdgeEnvelopes[i], new JsonSerializerOptions { WriteIndented = true }));
    }

    return 0;
}

8. Verifier (same libraries, different flow)

Later (or in parallel), add a verification mode:

  1. Inputs:

    • Binary file.
    • Manifest DSSE file.
    • Edge DSSE files.
    • (Optionally) Rekor log inclusion proof bundle.
  2. Steps (same dev can implement):

    • Verify DSSE signatures for manifest and edges (using IAuthoritySigner.VerifyAsync).

    • Check:

      • Manifests binary digest matches the current file.
      • Manifests edgeenvelope digests match hashes of the provided DSSE edge files.
    • Rebuild call graph using the same tool & policy version and diff against attested edges:

      • For deterministic replay, their differences should be zero.
    • Optionally:

      • Ask Rekor for current log info and verify inclusion proof (advanced).

9. Order of work for a midlevel .NET dev

If you hand this as a sequence of tasks:

  1. Core models & interfaces

    • Add domain records (BinaryIdentity, FunctionRef, ReachabilityEdge, CallGraphManifest).
    • Add IBinaryParser, IEdgeExtractor, IAttestor, IRekorClient.
  2. Managed PE parser

    • Implement PeBinaryParser using System.Reflection.Metadata (PEReader, MetadataReader).(NuGet)
    • Return BinaryIdentity, a list of methods as FunctionRef, and roots (Main).
  3. IL edge extractor

    • Add Mono.Cecil.

    • Implement ManagedIlEdgeExtractor that:

      • Iterates methods and IL instructions.
      • Emits edges for call and callvirt.
  4. CallGraphBuilder

    • Wire .Build(path) to use PeBinaryParser + ManagedIlEdgeExtractor.
  5. DSSE library

    • Add DsseEnvelope, DsseSignature, Dsse.PreAuthEncode.
    • Implement DsseAttestor that wraps ReachabilityEdge and CallGraphManifest into DSSE envelopes using an IAuthoritySigner.
  6. Rekor client (stub, then real)

    • First: DummyRekorClient that just returns fake IDs.
    • Then: HttpRekorClient that POSTs to your Rekor server.
  7. CallGraphAttestationService + CLI

    • Implement CallGraphAttestationService.

    • CLI command to:

      • Run analysis.
      • Write DSSE files + a human readable summary.
  8. Verifier

    • Implement basic “offline verify” command:

      • Verify DSSE signatures on manifest + edges.
      • Verify manifest ↔ edge digest linkage.
      • (Later) compare reanalyzed graph with attested one.

If you want, I can next:

  • Propose the exact JSON schema for EdgeStatement and CallGraphManifest (with sample instances).
  • Or help turn this into a Jira/Linear ticket breakdown ready for your team.