Binary Delta Attestation Flow

Reconciliation note (verified against src/BinaryIndex, src/Attestor, src/Scanner, and the scope catalog). This flow was originally written around an image-version-to-image-version binary diff (scan v1.2.4 against baseline v1.2.3, then attest “what binaries changed between releases”). No such feature exists in the code. The implemented capability is the DeltaSig stack inside the BinaryIndex module: it diffs binaries at the function level (typically a vulnerable binary against a patched binary of the same package), uses those diffs to author CVE patch signatures, and detects backported security fixes that package-version scanning would miss. The “delta attestation” is a DSSE-wrapped function-level diff predicate, not a release-to-release image diff. The document below has been corrected to the implemented behaviour; orphaned image-diff content is annotated inline.

Overview

The Binary Delta Attestation Flow describes how StellaOps produces and attests function-level binary diffs between two builds of a binary — most commonly the vulnerable and patched builds of a package. These signed diffs (DeltaSig predicates) record exactly which functions were added, removed, or modified, with per-function hashes, semantic-similarity scores, and (in v2) symbol provenance. The same machinery drives backport detection: a binary in a scanned image is matched against known vulnerable/patched function signatures so StellaOps can tell whether a fix is present even when the version string lies.

Audience: security engineers authoring CVE patch signatures with the stella deltasig CLI, and platform operators relying on backport detection and the delta-scope gate during scans and releases.

Business Value: Detect backported (or missing) security patches that version-based matching cannot see, prove that a binary patch touches only the declared functions, and gate releases on the scope of a binary change (e.g. “no more than N functions touched”).

Why binary-first? Package version strings can lie — backports, custom/vendored builds, stripped metadata, static linking, distroless images. Binary identity and function-level fingerprints do not. See docs/modules/binary-index/architecture.md §1 (“Binary identity doesn’t lie”).

Actors

ActorTypeRole
CI Pipeline / OperatorSystem / UserAuthors signatures (stella deltasig) or triggers a scan that runs delta-signature matching
Scanner.WorkerServiceRuns DeltaSigAnalyzer during a scan: extracts function hashes and matches them against known signatures (backport detection)
BinaryIndexService / librariesGenerates and matches DeltaSig signatures, resolves binaries to vulnerable/patched status (POST /api/v1/resolve/vuln), builds the function-level diff predicate, and signs/attests it
AttestorServiceReceives the DSSE envelope for transparency-log submission. (Signer and Provenance are consolidated into Attestor — see AGENTS.md §1.4; there is no standalone “Signer” service.)
Symbols serverServiceCatalog of symbol-pack sources (debuginfod/ddeb/buildinfo) guarded by symbols:read/symbols:write. Symbol→source attribution for matches is done by ISymbolProvenanceResolver (GroundTruthProvenanceResolver), not by source-line lookups in the Symbols server.

NOT IMPLEMENTED — orphaned actors. The original table listed a standalone Signer service (consolidated into Attestor) and described Symbols as a service that “resolves debug symbols” / “maps build IDs to source lines”. The Symbols server (src/BinaryIndex/StellaOps.Symbols.Server) is a symbol-source and marketplace catalog (/api/v1/symbols/sources, /api/v1/symbols/marketplace); it does not perform DWARF/PDB source-line resolution in this flow.

Prerequisites

Binary Fingerprinting

BinaryIndex resolves binary identity over three dimensions (BinaryIdentity in StellaOps.BinaryIndex.Core) and three matching tiers (architecture doc §1.2):

IdentifierSource / fieldNotes
File SHA-256FileSha256 — whole fileExact binary match
Build-IDBuildId (+ BuildIdType: gnu-build-id, pe-cv, macho-uuid)ELF .note.gnu.build-id, PE CodeView GUID, or Mach-O UUID — build-specific identity
Text SHA-256TextSha256.text section onlyCode-only hash; ignores data sections
BLAKE3Blake3HashReserved / future-use hash
Function fingerprintbasic-block / CFG / string-ref / constant signals (IFunctionFingerprintExtractor, Tier C)Function-level identity used by DeltaSig matching
TierMethodPrecision
APackage/version range matchingMedium
BBuild-ID / hash catalog (exact binary identity)High
CFunction fingerprints (CFG / basic-block hashes)Very High

CORRECTED. The original table listed “PE Checksum” (PE header) and “Symbol Hash” (exported-symbol count hash) and “Debug ID (DWARF/PDB reference)” as first-class identifiers. The code’s identity model exposes file SHA-256, Build-ID (with a typed kind covering PE CodeView and Mach-O UUID), .text SHA-256, and BLAKE3; function-level identity is the CFG/basic-block fingerprint, not an “exported symbols” hash. Symbol-count deltas are reported per function inside the predicate, not as a top-level identifier.

Flow Diagram

The diagram below shows the authoring + attestation path (stella deltasig), where two builds of a binary (old/vulnerable, new/patched) are diffed at the function level and the resulting predicate is signed and submitted to a transparency log.

┌─────────────────────────────────────────────────────────────────────────────────┐
│                  DeltaSig: function-level binary diff + attestation               │
└─────────────────────────────────────────────────────────────────────────────────┘

┌───────────┐  ┌──────────────────────────────────────────────┐  ┌──────────┐
│  Operator │  │            BinaryIndex (DeltaSig)             │  │ Attestor │
│   / CI    │  │  Disassembly · Normalization · Diff · Predicate│  │  (+ TLog)│
└─────┬─────┘  └───────────────────────┬──────────────────────┘  └────┬─────┘
      │ deltasig author                 │                              │
      │ --vuln old.so --patched new.so  │                              │
      │ --symbols ssl_handshake ...     │                              │
      │────────────────────────────────>│                              │
      │                                 │ Disassemble both binaries    │
      │                                 │───┐                          │
      │                                 │<──┘                          │
      │                                 │ Normalize (recipe) + lift IR │
      │                                 │───┐                          │
      │                                 │<──┘                          │
      │                                 │ Diff per function:           │
      │                                 │ added/removed/modified,      │
      │                                 │ per-fn hashes, sem-similarity│
      │                                 │───┐                          │
      │                                 │<──┘                          │
      │                                 │ (v2) enrich w/ provenance    │
      │                                 │  via GroundTruth resolver    │
      │                                 │───┐                          │
      │                                 │<──┘                          │
      │                                 │ Build DeltaSigPredicate      │
      │                                 │  (subjects old+new)          │
      │                                 │───┐                          │
      │                                 │<──┘                          │
      │ deltasig sign  →  in-toto       │ Build in-toto Statement +    │
      │ Statement + DSSE PAE            │ DSSE envelope (ES256)        │
      │                                 │───┐                          │
      │                                 │<──┘                          │
      │                                 │ Submit DSSE for transparency │
      │                                 │─────────────────────────────>│
      │                                 │                              │ Rekor
      │                                 │                              │ entry +
      │                                 │   AttestationResult          │ inclusion
      │                                 │<─────────────────────────────│ proof
      │ Signed predicate + log index    │                              │
      │<────────────────────────────────│                              │

In-scan backport detection (separate path). During a normal scan, Scanner.Worker’s DeltaSigAnalyzer extracts function hashes from each binary and matches them against the ingested vulnerable/patched signatures. A “patched” match means the fix is present even if the version string suggests vulnerability; a “vulnerable” match (or no patched match) means the fix is missing. Results feed VEX evidence (DeltaSigVexEmitter) and findings — no image-to-image diff is involved.

Step-by-Step

CORRECTED throughout. The original step list described stellaops scan <img> --baseline <img> --binary-delta driving a POST /api/v1/scans { options.binary_delta } request that diffs whole binaries between two image versions. That CLI flag, that scan option, and that whole-binary “added/modified/removed file” delta do not exist. The implemented surfaces are: (a) the stella deltasig CLI group, (b) POST /api/v1/resolve/vuln + /api/v1/resolve/vuln/batch on the BinaryIndex WebService, and © the in-scan DeltaSigAnalyzer. Steps are rewritten accordingly.

1. Author / extract delta signatures

An operator (or CI) authors a function-level diff between the vulnerable and patched builds of a binary using the stella deltasig CLI group (src/Cli/StellaOps.Cli/Commands/DeltaSig/DeltaSigCommandGroup.cs):

# Extract normalized function signatures from a single binary
# (binary is positional; --symbols/-s is required on extract)
stella deltasig extract ./libssl.so.3 --symbols ssl_handshake --arch x86_64 [--semantic]

# Author a vulnerable↔patched signature pair (CVE patch signature).
# NOTE: `author` does NOT take --symbols; it diffs the two builds and is keyed
# by --cve/--package, with --arch (+ optional --soname/--abi) and --out <dir>.
stella deltasig author --vuln ./libssl-vuln.so --patched ./libssl-fixed.so \
  --cve CVE-2024-XXXX --package openssl --arch x86_64 --out ./sigs [--semantic]

# Sign / verify use named options (not positional args):
stella deltasig sign   --in <signature.json> --key <priv.pem> --out <envelope.dsse.json> [--alg ecdsa-p256-sha256]
stella deltasig verify --in <envelope.dsse.json> --pub <pub.pem>

# Match a binary (positional) against a signature pack/dir via --sigpack/-s:
stella deltasig match  <binary> --sigpack <pack-or-dir> [--cve CVE-...] [--json] [--semantic]

# Pack reads a directory of *.dsse.json and writes a ZIP:
stella deltasig pack   --in-dir <dir> --out <pack.zip> [--pack-id <id>]

# Inspect takes a positional payload/envelope file:
stella deltasig inspect <signature-or-envelope> [--json]

The extract/author/match --semantic flag (IR-level semantic fingerprints) requires a live BinaryIndex service connection.

CORRECTED. The original examples showed author --symbols ..., positional sign <file> / verify <file>, a two-positional match <binary> <pack>, and a variadic pack <signatures...>. None of those match DeltaSigCommandGroup.cs: author has no --symbols option (only extract/match/author differ — author is keyed by --vuln/--patched/--cve/--package/--arch/--soname/--abi/--out); sign takes --in/--key/--out/--alg; verify takes --in/--pub; match takes a positional binary plus --sigpack/-s; pack takes --in-dir/--out/--pack-id. Only extract (positional binary) and inspect (positional file) use positional arguments.

2. Resolve a binary’s vulnerability status (REST)

To ask BinaryIndex whether a binary is vulnerable or patched, call the resolution API on the BinaryIndex WebService (ResolutionController, route api/v1/resolve). At least one identifier (build_id, fingerprint, or one of the hashes) is required:

POST /api/v1/resolve/vuln HTTP/1.1
Content-Type: application/json

{
  "package": "pkg:deb/debian/openssl@3.0.7",
  "build_id": "abc123def456789...",
  "hashes": {
    "file_sha256": "sha256:e3b0c44...",
    "text_sha256": "sha256:abc123..."
  },
  "distro_release": "debian:bookworm",
  "cve_id": "CVE-2024-XXXX"   // optional; omit to check all known CVEs
}

Batch form (POST /api/v1/resolve/vuln/batch) carries an items[] array plus options.bypass_cache / options.include_dsse_attestation. The single endpoint instead takes bypassCache as a query parameter. The batch is processed up to ResolutionServiceOptions.MaxBatchSize (default 500); items beyond the cap are silently truncated (see Error Handling). The response (VulnResolutionResponse) returns a status (ResolutionStatus: Fixed, Vulnerable, NotAffected, Unknown), optional fixedVersion, and supporting evidence (ResolutionEvidence) — optionally including a DSSE attestation (attestationDsse, Base64-encoded JSON) when include_dsse_attestation is set.

Auth. These endpoints require an authenticated caller (fallback policy RequireAuthenticatedUser); there is no dedicated binary-index scope in the catalog. The Symbols server endpoints are the only ones in this module that enforce a specific scope (symbols:read / symbols:write).

3. Disassemble, normalize, and diff at the function level

For each target symbol/function, BinaryIndex disassembles both binaries (Iced for x86/x86-64, B2R2 for ARM/MIPS/RISC-V), applies a versioned normalization recipe for deterministic hashing, and computes a per-function diff (StellaOps.BinaryIndex.Diff, StellaOps.BinaryIndex.DeltaSig). Each function is classified added, removed, or modified, with old/new byte hashes, sizes, optional IR-level diff, and a semantic-similarity score for modified functions.

4. (v2) Enrich with symbol provenance

The v2 generator (DeltaSigServiceV2) enriches each function match with provenance from ground-truth corpus sources via ISymbolProvenanceResolver (GroundTruthProvenanceResolver). Preferred sources, in priority order, are debuginfod-fedora, debuginfod-ubuntu, ddeb-ubuntu, buildinfo-debian (ProvenanceResolutionOptions.PreferredSources, default). Each match gains a symbolProvenance block (SymbolProvenanceV2): sourceId, observationId (format groundtruth:{source_id}:{debug_id}:{revision}), fetchedAt, signatureState (verified/unverified/expired), plus optional packageName/packageVersion/distro/distroVersion/debugId.

CORRECTED. The original “Symbol Resolution” step claimed the Symbols service maps build IDs to source repo/commit/compiler-flags and to exact src/...c:NN lines. The implemented provenance is corpus-source attribution (which debuginfod/ddeb/buildinfo source provided the symbol), not repository/commit/line resolution.

5. Build the delta predicate

The delta is captured as a DeltaSig predicate, not an image-diff document.

v1 — function-level diff (DeltaSigPredicate, StellaOps.BinaryIndex.DeltaSig.Attestation):

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    { "name": "oci://registry/openssl@sha256:old...", "digest": {"sha256": "old..."} },
    { "name": "oci://registry/openssl@sha256:new...", "digest": {"sha256": "new..."} }
  ],
  "predicateType": "https://stellaops.dev/delta-sig/v1",
  "predicate": {
    "schemaVersion": "1.0.0",
    "subject": [
      { "uri": "oci://registry/openssl@sha256:old...", "digest": {"sha256": "old..."}, "arch": "linux-amd64", "role": "old" },
      { "uri": "oci://registry/openssl@sha256:new...", "digest": {"sha256": "new..."}, "arch": "linux-amd64", "role": "new" }
    ],
    "delta": [
      {
        "functionId": "ssl_handshake",
        "address": 4194304,
        "oldHash": "sha256:abc...",
        "newHash": "sha256:def...",
        "oldSize": 512,
        "newSize": 544,
        "changeType": "modified",
        "semanticSimilarity": 0.92,
        "section": ".text"
      }
    ],
    "summary": {
      "totalFunctions": 150,
      "functionsAdded": 0,
      "functionsRemoved": 0,
      "functionsModified": 1,
      "functionsUnchanged": 149,
      "totalBytesChanged": 32,
      "minSemanticSimilarity": 0.92,
      "avgSemanticSimilarity": 0.92,
      "maxSemanticSimilarity": 0.92
    },
    "tooling": {
      "lifter": "b2r2",
      "lifterVersion": "0.x",
      "canonicalIr": "b2r2-lowuir",
      "diffAlgorithm": "ir-semantic",
      "hashAlgorithm": "sha256"
    },
    "cveIds": ["CVE-2024-XXXX"],
    "computedAt": "2026-01-19T12:00:00Z"
  }
}

v2 — provenance + verdict (DeltaSigPredicateV2, see docs/modules/binary-index/deltasig-v2-schema.md):

CORRECTED. The original predicate type https://stellaops.io/attestation/binary-delta/v1 does not exist. The original predicate body (baseline image, delta_summary of file-level modified/added/removed counts, binary_changes[] keyed by container path with commit provenance, and a verification block of all_changes_have_provenance/all_binaries_signed/no_unexpected_modifications) is not produced anywhere in the code. The implemented predicate is function-keyed with old/new subjects (v1) or a single subject + verdict (v2).

6. Sign as a DSSE envelope and attest

DeltaSigEnvelopeBuilder wraps the predicate in an in-toto v1 Statement (payloadType application/vnd.in-toto+json), computes the DSSE PAE, and produces a DsseEnvelope. IDeltaSigAttestorService.AttestAsync signs (default algorithm ES256) and submits to a transparency log, returning a DeltaSigAttestationResult with rekorEntryId, logIndex, integratedTime, and an optional stored inclusion proof for offline verification.

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "base64:statement-json...",
  "signatures": [
    {
      "keyid": "<signing-key-id>",
      "sig": "base64:signature..."
    }
  ]
}

CORRECTED. Signing is performed within the DeltaSig/Attestor integration (IDeltaSigAttestorService), not by a standalone “Signer” service. The DSSE signature object has keyid + sig (DsseSignature); there is no cert field on the signature record in the code.

Flagged — offline-first posture. DeltaSigAttestationOptions.RekorUrl defaults to https://rekor.sigstore.dev, an external transparency-log endpoint. For air-gapped deployments this default must be overridden to an on-prem/offline log; it should not be treated as the canonical default in a self-hosted install.

Verification Policy — Delta Scope Gate

A DeltaSig predicate can be gated by the Delta Scope Gate (DeltaScopePolicyGate, StellaOps.BinaryIndex.DeltaSig.Policy). The gate evaluates a predicate against thresholds on the function-level change scope and tooling — it enforces “this binary patch only touches a bounded set of functions”, not “which container files changed between releases”.

Configuration section BinaryIndex:DeltaScopeGate (DeltaScopeGateOptions); defaults shown:

BinaryIndex:
  DeltaScopeGate:
    maxModifiedFunctions: 10        # error if exceeded
    maxAddedFunctions: 5            # error if exceeded
    maxRemovedFunctions: 2          # error if exceeded
    maxBytesChanged: 10000          # error if exceeded
    minSemanticSimilarity: 0.8      # error if min sim below this
    warnAvgSemanticSimilarity: 0.9  # warning (does not fail the gate)
    requiredLifters: []             # e.g. ["ghidra"] for high-assurance
    requiredDiffAlgorithm: null     # e.g. "ir-semantic"
    forbiddenFunctionPatterns: []   # regex; matched against functionId
    allowApprovalBypass: false

The gate returns a DeltaScopeGateResult with passed, a list of violations (each carrying a rule, severity Error/Warning, actual vs threshold values, and optional functionId), and a summary. The gate fails only when at least one Error-severity violation is present; Warning-severity issues are reported without failing.

NOT IMPLEMENTED — orphaned policy. The original binary_delta_policy block with mode: strict/permissive and rules such as all_changes_must_have_provenance, allow_added_binaries, allow_removed_binaries, require_signed_builds, max_modified_binaries, allow_unsigned_binaries, warn_on_missing_provenance, block_known_malware_hashes has no counterpart in the code. There is no strict/permissive mode and no image-binary allow/deny rules; the implemented gate is function-scope based as shown above.

Data Contracts

Vulnerability Resolution Request (VulnResolutionRequest)

interface VulnResolutionRequest {
  package: string;                 // PURL or CPE (required)
  filePath?: string;
  buildId?: string;                // ELF Build-ID, PE CodeView GUID, or Mach-O UUID
  hashes?: {
    fileSha256?: string;
    textSha256?: string;
    blake3?: string;
  };
  fingerprint?: string;            // Base64
  fingerprintAlgorithm?: string;   // e.g. "combined", "tlsh", "ssdeep"
  cveId?: string;                  // optional; omit to check all known CVEs
  distroRelease?: string;          // e.g. "debian:bookworm"
  // Validation: at least one of buildId, fingerprint, or a hash is required.
}

DeltaSig v1 predicate (DeltaSigPredicate) — authoritative delta shape

interface DeltaSigPredicate {
  schemaVersion: string;           // "1.0.0"
  subject: DeltaSigSubject[];      // old + new binaries (role: "old" | "new")
  delta: FunctionDelta[];          // per-function changes
  summary: DeltaSummary;           // function counts + similarity stats
  tooling: DeltaTooling;           // lifter, IR, diff algorithm
  computedAt: string;              // RFC 3339
  cveIds?: string[];
  advisories?: string[];
  ecosystem?: string;
  packageName?: string;
  versionRange?: { oldVersion: string; newVersion: string; constraint?: string };
  sbomDigest?: string;
  largeBlobs?: LargeBlobReference[];
  hybridDiff?: HybridDiffEvidence;
}

(v2 — DeltaSigPredicateV2 — replaces the old/new subject pair with a single subject plus verdict/confidence and per-function matchState; see docs/modules/binary-index/deltasig-v2-schema.md.)

NOT IMPLEMENTED — orphaned contracts. The original BinaryDeltaRequest (options.binary_delta.{enabled,baseline,...} + attestation) and BinaryDeltaResponse (scan_id, baseline/current ImageReference, file-level summary, changes[], policy_verdict.{verdict,violations}) do not exist. There is no binary_delta scan option and no scan_id-keyed delta response. The implemented contracts are the resolution request/response (api/v1/resolve) and the DeltaSig predicates.

Error Handling

ErrorRecovery (as implemented)
Missing identifier on resolution request400 Bad Request (InvalidRequest/MissingPackage); request validation requires package + at least one of build_id / fingerprint / hash
Binary not found in index404 Not Found is declared on ResolutionController (ProducesResponseType 404); the controller itself returns 200 with a VulnResolutionResponse carrying status (e.g. Unknown) rather than a hard 404 for an unmatched binary
Empty batch400 Bad Request (EmptyBatch) when items[] is empty/missing
Oversized batchNo 400. The batch is silently truncated to ResolutionServiceOptions.MaxBatchSize (default 500) with a logged warning (ResolutionService.ResolveBatchAsync); excess items are dropped, not rejected
Resolution failure500 Internal Server Error (ResolutionError single / BatchResolutionError batch); request is logged
Attestation failureDeltaSigAttestationResult.Failed(error) returned with success=false and an errorMessage

CORRECTED. The original table referenced “Baseline not found → suggest scanning baseline first” and “Symbol resolution failed → hash-only comparison”; neither maps to an implemented code path (no baseline-scan workflow; symbol provenance is best-effort enrichment, not a fallback toggle). Also corrected: an oversized batch is truncated, not rejected with 400; and a binary that is not matched comes back as a 200 response with an Unknown/NotAffected status rather than a 404 (the 404 is only a declared possible response).

Observability

Metrics (BinaryIndex resolution telemetry)

Meter StellaOps.BinaryIndex.Resolution (ResolutionTelemetry):

MetricType
binaryindex.resolution.requests.totalCounter
binaryindex.resolution.resolutions.totalCounter
binaryindex.resolution.cache.hits.total / ...cache.misses.totalCounter
binaryindex.resolution.errors.totalCounter
binaryindex.resolution.rate_limited.totalCounter
binaryindex.resolution.request.duration.msHistogram
binaryindex.resolution.cache.latency.msHistogram
binaryindex.resolution.fingerprint_match.duration.msHistogram
binaryindex.resolution.batch.sizeHistogram
binaryindex.resolution.confidenceHistogram
binaryindex.resolution.requests.in_progressUpDownCounter

NOT IMPLEMENTED — orphaned metrics. The original binary_delta_scans_total, binary_delta_changes_total, binary_delta_duration_seconds, and binary_index_size_bytes metrics do not exist. The real instruments are the resolution counters/histograms above.

Key log events. The original binary.delta.* event names are illustrative only and are not emitted as named events in the code; treat them as a roadmap sketch, not a contract.

Source References