Here’s a compact, practical blueprint for detecting patched vulnerabilities in Linux binaries—even when distros backport fixes and version strings lie—by using **low‑entropy delta signatures** over ELF segments. --- # Why this helps Version numbers and CVE notes are noisy. Distros often **backport** a fix without bumping upstream versions. A fast, reproducible **code‑delta fingerprint** lets Stella Ops verify “is the fix present in *this* binary?” without trusting package metadata. # Core idea (plain English) 1. Extract the binary’s executable/read‑only segments (`.text`, `.rodata`, and optionally `.got/.plt`). 2. **Normalize** away relocation noise (addresses, section offsets, build‑IDs). 3. Hash the normalized bytes with **SHA‑256**. 4. Compare that hash (or a tiny set of chunk hashes) against a **reference signature** for the *patched* and *vulnerable* states of the function(s) touched by the CVE. 5. Verdict: “Patched”, “Still vulnerable”, or “Indeterminate (needs deeper diff)”. # What we store (the “delta signature”) For each CVE fix (per arch, per compiler family if needed): * Target ELF: `soname`, `symbol(s)` affected (e.g., `ssl3_accept`, `png_handle_iCCP`) * Segment scope: `.text` (always), `.rodata` (optional) * **Normalization recipe**: strip relocations, zero relocation slots, canonicalize NOP runs, normalize jump tables * **Hash schema**: * Whole‑region SHA‑256, **and**/or * Rolling chunk hashes (e.g., 1–4 KiB windows) for resilience * Build meta: arch (`x86_64`, `aarch64`), endianness, ABI hints, compiler hardening flags that *don’t* affect normalized output * Confidence notes (e.g., “ROP‑safe change only in function X”) # Normalization rules (pragmatic) * Apply relocations to get final bytes, then **zero out** relocation targets and absolute addresses. * Canonicalize: * NOP sleds → single `NOP` * PLT/GOT stub patterns → canonical tokens * Jump tables: rewrite 32/64‑bit absolute entries to **relative deltas** then zero base * Padding → zero * Strip `.comment`, `.note.*`, `.symtab` (keep `.dynsym` for symbol mapping only). # Minimal C# sketch (Stella Ops scanner.webservice) ```csharp public sealed class ElfDeltaHasher { public record SignatureSpec(string SoName, string[] Symbols, string Arch, bool IncludeRodata); public record DeltaResult(string Arch, string SoName, string Symbol, string Alg, byte[] Hash, int Size); public static IEnumerable Compute(Stream elfStream, SignatureSpec spec) { var elf = ElfFile.Load(elfStream); // use an internal reader (no P/Invoke) if (elf.DynamicLibraries?.Any(s => s.Contains(spec.SoName)) == false && elf.SONAME != spec.SoName) yield break; foreach (var sym in ResolveSymbols(elf, spec.Symbols)) { var textBytes = ExtractNormalized(elf, sym, sections: new[]{".text"}); yield return HashChunk("sha256:text", spec.SoName, sym.Name, textBytes); if (spec.IncludeRodata) { var roBytes = ExtractNormalized(elf, sym, sections: new[]{".rodata",".rodata.rel.ro"}); yield return HashChunk("sha256:ro", spec.SoName, sym.Name, roBytes); } } } static DeltaResult HashChunk(string alg, string so, string sym, ReadOnlySpan bytes) { using var sha = System.Security.Cryptography.SHA256.Create(); var h = sha.ComputeHash(bytes.ToArray()); return new DeltaResult(arch: RuntimeArch(), so, sym, alg, h, bytes.Length); } } ``` *(Your real code: implement `ElfFile`, relocation application, and the normalization passes above; add rolling 2 KiB window hashes for robustness.)* # Where it lives in Stella Ops * **StellaOps.Feedser**: ingest vendor advisories; map CVE → touched symbols/functions. * **StellaOps.PatchSigRepo** (new): git‑backed registry of delta signatures (AGPL data pack). * **StellaOps.Scanner.Webservice**: runs the normalizer/hasher against container layers and host files. * **StellaOps.Vexer**: emits **machine‑verifiable VEX** statements (“Status=Fixed; Evidence=DeltaSig#abcd…”). * **UI**: in the **Finding details** drawer → “Patched by backport (verified by code delta)”; badge links to the exact signature entry. # Pipeline (end‑to‑end) 1. **Signature authoring** (one‑time per CVE): * Pull upstream vulnerable & fixed tags. * Build both with common flags per arch; disasm diff to identify changed symbols. * Generate normalized hashes → create `cves/CVE-YYYY-NNNN/*.json`. 2. **CI publish**: sign each signature file (DSSE), push to **Proof‑Market Ledger** mirror and internal registry. 3. **Scan time**: * Identify candidate ELF (`soname`/path heuristics). * Compute normalized hashes; compare against **patched** and **vulnerable** sets. * Emit VEX + attach proof artifact (hash, recipe, tool version). 4. **Policy**: * Gate releases on “Patched OR Not‑Affected by reachability”, otherwise block. # Handling distro drift * Keep multiple signatures per CVE: * `gcc‑O2`, `clang‑O2`, `-fstack-protector-strong`, LTO on/off. * Prefer **symbol‑scoped** hashing over whole file. * If symbol names are stripped: * Fallback to **function boundary recovery** (linear sweep + prologue heuristics) and **fuzzy block graph** matching; still run normalization. # Performance tips * Cache per‑layer ELF normalization by `(fileDigest, toolVersion)`. * Use mmap + streaming SHA256; avoid copying. * Parallelize per‑ELF, but cap CPU with a scheduler token. # Reducing false results * Require **dual evidence** when uncertain: `.text` hash match + small CFG checksum (basic‑block count + edge hash). * Mark **Indeterminate** if: * Only `.rodata` matches, * File is PIE with heavy inlining changes, * LTO changed layout dramatically. * Always record tool + recipe versions in the evidence blob for deterministic replay. # Data format (example) ```json { "cve": "CVE-2023-12345", "package": "libfoo.so.1", "arch": "x86_64", "symbols": [ { "name": "foo_parse", "scope": ".text", "normalization": ["applyRelocs","zeroAbs","canonNops","canonJt"], "hash": "sha256:4f1c...d9", "size": 1184 } ], "alt_signatures": [ {"compiler":"clang-16","flags":"-O3 -fno-plt", "hash":"sha256:9ab...42"} ], "attestation": {"dsse":"..."} } ``` # Rollout checklist (2 sprints) * **Sprint A** * Implement ELF loader + normalization passes (x86_64/aarch64). * Build authoring tool (`stella-deltasig mk`) and signer. * Wire Scanner → Vexer evidence pipe; UI badge. * **Sprint B** * Add rolling chunk hashes + CFG micro‑checksum. * Author top‑20 backported CVEs (OpenSSL, glibc, zlib, curl, libpng). * Cache & perf; add air‑gapped bundle for signatures. # Why this is a moat * Competing scanners mostly trust **CVE string matching** and package versions. You ship **cryptographic, function‑level proof** that a fix is present—even across diverged distros—**without humans in the loop**. That’s hard to replicate quickly. If you want, I can draft the `PatchSigRepo` JSON schema, the DSSE attestation type, and a tiny CLI (`stella-deltasig`) you can drop into `scanner.webservice` today. Below is a complete, opinionated “DeltaSig” module design that plugs into the existing Stella Ops standard CLI (single `stella` entrypoint), plus the payload schema, DSSE envelope type, and a deterministic “sigpack” format. All examples and code are .NET 10–style and keep dependencies minimal (built-in crypto + System.Text.Json). --- ## 1) CLI module shape (as a first-class `stella` module) ### Command tree ```text stella deltasig mk Create an unsigned delta-signature payload JSON sign Wrap payload in DSSE + sign verify Verify DSSE envelope(s) id Print signature ID (sha256 of canonical payload) pack Build deterministic sigpack (zip) from DSSE envelopes inspect Pretty-print payload/envelope; show fields + hashes match Compute evidence for a given ELF against a sigpack (optional, uses extractor service) ``` ### Output/UX rules (standard CLI conventions) * Default output is **human-readable**; add `--json` on any command to emit machine JSON. * Exit codes: * `0` success / verified / match found * `2` verification failed / match is “vulnerable” * `3` indeterminate / partial * `64` usage error (missing args) * `70` internal error ### Typical workflow ```bash # 1) Create payload stella deltasig mk \ --cve CVE-2023-12345 \ --package libfoo \ --soname libfoo.so.1 \ --arch x86_64 \ --abi gnu \ --symbol foo_parse --symbol foo_init \ --scope text \ --hash sha256 \ --out cves/CVE-2023-12345/libfoo/x86_64-gnu.payload.json # 2) Sign (DSSE) stella deltasig sign \ --in cves/CVE-2023-12345/libfoo/x86_64-gnu.payload.json \ --key keys/stellaops-patches.ecdsa-p256.pem \ --out cves/CVE-2023-12345/libfoo/x86_64-gnu.dsse.json # 3) Verify stella deltasig verify \ --in cves/CVE-2023-12345/libfoo/x86_64-gnu.dsse.json \ --pub keys/stellaops-patches.ecdsa-p256.pub.pem # 4) Pack for offline distribution stella deltasig pack \ --in-dir cves/ \ --out stellaops-deltasigpack-2025-12.zip ``` --- ## 2) File formats ### 2.1 Delta signature payload (canonical JSON, signed via DSSE) **PayloadType** (DSSE): `application/vnd.stellaops.deltasig.v1+json` **Design goals** * Payload is **deterministic**: no timestamps, no author fields. * Signature ID is `sha256(canonical_payload_bytes)`. #### Payload structure (`DeltaSigPayloadV1`) ```json { "schema": "stellaops.deltasig.v1", "cve": "CVE-2023-12345", "package": { "name": "libfoo", "soname": "libfoo.so.1" }, "target": { "arch": "x86_64", "abi": "gnu" }, "normalization": { "recipeId": "elf.delta.norm.v1", "steps": [ "applyRelocs", "zeroRelocTargets", "canonNops", "canonPltGot", "canonJumpTables", "zeroPadding" ] }, "symbols": [ { "name": "foo_parse", "scope": ".text", "hashAlg": "sha256", "hashHex": "4f1c...d9", "sizeBytes": 1184, "chunks": [ { "offset": 0, "sizeBytes": 2048, "hashHex": "aa.." } ], "cfg": { "bbCount": 42, "edgeHashHex": "bb.." } } ] } ``` Notes: * `chunks` and `cfg` are optional but recommended for resilience. * `hashHex` is lowercase hex of the **normalized bytes** of the symbol region. ### 2.2 DSSE envelope (signed wrapper) Minimal DSSE JSON: ```json { "payloadType": "application/vnd.stellaops.deltasig.v1+json", "payload": "BASE64URL( canonical_payload_bytes )", "signatures": [ { "keyid": "sha256:ab12...ef", "sig": "BASE64URL( signature_over_PAE )" } ] } ``` ### 2.3 Sigpack (deterministic offline bundle) A **ZIP** with deterministic ordering and timestamps forced to a constant (e.g., 1980-01-01 for ZIP compatibility). Contents: ```text index.json sigs/.dsse.json (optional) keys/.pub.pem (optional) meta.json ``` `index.json` (sorted by `sigId`): ```json { "schema": "stellaops.deltasigpack.v1", "entries": [ { "sigId": "sha256:...", "cve": "CVE-2023-12345", "package": "libfoo", "soname": "libfoo.so.1", "arch": "x86_64", "abi": "gnu", "path": "sigs/sha256-....dsse.json" } ] } ``` --- ## 3) How to integrate as a module in the Stella Ops standard CLI ### 3.1 Minimal module contract (stable SPI) Create a tiny contract assembly used by the main CLI and all modules: `src/StellaOps.Cli.Abstractions/IStellaCliModule.cs` ```csharp namespace StellaOps.Cli.Abstractions; public interface IStellaCliModule { string Name { get; } // "deltasig" string Description { get; } // one-liner void Register(InvocationContext ctx, Command root); } ``` `InvocationContext` can carry DI/services/config: ```csharp namespace StellaOps.Cli.Abstractions; public sealed record InvocationContext( IServiceProvider Services, string[] Args, TextWriter Out, TextWriter Err ); ``` ### 3.2 Main CLI loads modules (built-in + plugin folder) `src/StellaOps.Cli/Program.cs` ```csharp using System.CommandLine; using System.CommandLine.Invocation; using System.Reflection; using System.Runtime.Loader; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using StellaOps.Cli.Abstractions; var builder = Host.CreateApplicationBuilder(args); // Register shared services (logging, config, etc.) builder.Services.AddSingleton(_ => Console.Out); builder.Services.AddSingleton(_ => Console.Error); // Load built-in modules by referencing their assemblies, // plus optional external modules from "./modules". var modules = LoadModules(AppContext.BaseDirectory); var host = builder.Build(); var services = host.Services; var root = new RootCommand("Stella Ops CLI"); var ctx = new InvocationContext(services, args, Console.Out, Console.Error); foreach (var m in modules.OrderBy(m => m.Name, StringComparer.Ordinal)) m.Register(ctx, root); return await root.InvokeAsync(args); static IReadOnlyList LoadModules(string baseDir) { var result = new List(); // 1) Built-in: scan already loaded assemblies result.AddRange(InstantiateModules(AppDomain.CurrentDomain.GetAssemblies())); // 2) Plugins: ./modules/*.dll var modulesDir = Path.Combine(baseDir, "modules"); if (Directory.Exists(modulesDir)) { foreach (var dll in Directory.EnumerateFiles(modulesDir, "*.dll", SearchOption.TopDirectoryOnly) .OrderBy(x => x, StringComparer.Ordinal)) { try { var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(dll); result.AddRange(InstantiateModules(new[] { asm })); } catch { // keep CLI robust: ignore broken plugins } } } // Deduplicate by Name (first wins) return result.GroupBy(m => m.Name, StringComparer.Ordinal).Select(g => g.First()).ToList(); } static IEnumerable InstantiateModules(IEnumerable assemblies) { foreach (var asm in assemblies) { Type[] types; try { types = asm.GetTypes(); } catch { continue; } foreach (var t in types) { if (t.IsAbstract || t.IsInterface) continue; if (!typeof(IStellaCliModule).IsAssignableFrom(t)) continue; if (t.GetConstructor(Type.EmptyTypes) is null) continue; IStellaCliModule? module = null; try { module = (IStellaCliModule)Activator.CreateInstance(t)!; } catch { /* ignore */ } if (module is not null) yield return module; } } } ``` This approach keeps “module = assembly” without forcing a rigid plugin framework. --- ## 4) The DeltaSig module implementation (CLI wiring) ### 4.1 Project layout ```text src/ StellaOps.Cli/ StellaOps.Cli.Abstractions/ StellaOps.PatchSig/ # models + canonical JSON + DSSE + pack builder StellaOps.Cli.Modules.DeltaSig/ # the CLI module, depends on PatchSig ``` ### 4.2 Module registration `src/StellaOps.Cli.Modules.DeltaSig/DeltaSigModule.cs` ```csharp using System.CommandLine; using StellaOps.Cli.Abstractions; using StellaOps.PatchSig; namespace StellaOps.Cli.Modules.DeltaSig; public sealed class DeltaSigModule : IStellaCliModule { public string Name => "deltasig"; public string Description => "Create, sign, verify, and pack patch-aware ELF delta signatures."; public void Register(InvocationContext ctx, Command root) { var cmd = new Command("deltasig", Description); cmd.AddCommand(DeltaSigCommands.Mk()); cmd.AddCommand(DeltaSigCommands.Sign()); cmd.AddCommand(DeltaSigCommands.Verify()); cmd.AddCommand(DeltaSigCommands.Id()); cmd.AddCommand(DeltaSigCommands.Pack()); cmd.AddCommand(DeltaSigCommands.Inspect()); // cmd.AddCommand(DeltaSigCommands.Match()); // optional (requires ELF extractor service) root.AddCommand(cmd); // Optional alias: "stella sig delta ..." var sig = root.Subcommands.FirstOrDefault(c => c.Name == "sig"); if (sig is not null) { var deltaAlias = new Command("delta", Description); foreach (var sc in cmd.Subcommands) deltaAlias.AddCommand(sc); sig.AddCommand(deltaAlias); } } } ``` ### 4.3 Commands (handlers call PatchSig library) `src/StellaOps.Cli.Modules.DeltaSig/DeltaSigCommands.cs` ```csharp using System.CommandLine; using System.CommandLine.NamingConventionBinder; using StellaOps.PatchSig; using StellaOps.PatchSig.Dsse; using StellaOps.PatchSig.Packaging; namespace StellaOps.Cli.Modules.DeltaSig; internal static class DeltaSigCommands { public static Command Mk() { var c = new Command("mk", "Create an unsigned delta-signature payload JSON."); var cve = new Option("--cve") { IsRequired = true }; var pkg = new Option("--package") { IsRequired = true }; var soname = new Option("--soname") { IsRequired = true }; var arch = new Option("--arch") { IsRequired = true }; var abi = new Option("--abi", () => "gnu"); var symbols = new Option("--symbol") { AllowMultipleArgumentsPerToken = true, IsRequired = true }; var scope = new Option("--scope", () => "text"); // text|rodata|both var outFile = new Option("--out") { IsRequired = true }; c.AddOption(cve); c.AddOption(pkg); c.AddOption(soname); c.AddOption(arch); c.AddOption(abi); c.AddOption(symbols); c.AddOption(scope); c.AddOption(outFile); c.Handler = CommandHandler.Create((cve, package, soname, arch, abi, symbol, scope, @out) => { // IMPORTANT: mk here creates the *structure*. Filling hashes is typically done by an extractor. // If you have an ELF extractor service, plug it here to compute hashHex/sizeBytes/chunks/cfg. var payload = DeltaSigPayloadV1.CreateSkeleton(cve, package, soname, arch, abi, symbol, scope); var bytes = CanonicalJson.Serialize(payload); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(@out))!); File.WriteAllBytes(@out, bytes); return 0; }); return c; } public static Command Id() { var c = new Command("id", "Print signature ID (sha256 of canonical payload)."); var input = new Option("--in") { IsRequired = true }; c.AddOption(input); c.Handler = CommandHandler.Create((@in) => { var payloadBytes = File.ReadAllBytes(@in); var sigId = SigId.ComputeSha256Hex(payloadBytes); Console.Out.WriteLine($"sha256:{sigId}"); return 0; }); return c; } public static Command Sign() { var c = new Command("sign", "Wrap payload in DSSE and sign."); var input = new Option("--in") { IsRequired = true }; var key = new Option("--key") { IsRequired = true }; var outp = new Option("--out") { IsRequired = true }; var alg = new Option("--alg", () => "ecdsa-p256-sha256"); // ecdsa-p256-sha256|rsa-pss-sha256 c.AddOption(input); c.AddOption(key); c.AddOption(outp); c.AddOption(alg); c.Handler = CommandHandler.Create((@in, key, @out, alg) => { var payloadBytes = File.ReadAllBytes(@in); var signer = DsseSignerFactory.CreateFromPem(key, alg); var env = DsseEnvelope.CreateSigned( payloadType: DsseTypes.DeltaSigV1, payloadBytes: payloadBytes, signer: signer ); var envBytes = CanonicalJson.Serialize(env); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(@out))!); File.WriteAllBytes(@out, envBytes); return 0; }); return c; } public static Command Verify() { var c = new Command("verify", "Verify DSSE envelope signature(s)."); var input = new Option("--in") { IsRequired = true }; var pub = new Option("--pub") { IsRequired = true }; c.AddOption(input); c.AddOption(pub); c.Handler = CommandHandler.Create((@in, pub) => { var envBytes = File.ReadAllBytes(@in); var env = CanonicalJson.Deserialize(envBytes); var verifier = DsseVerifierFactory.CreateFromPem(pub); var ok = env.VerifyAny(verifier, out var matchedKeyId); if (!ok) { Console.Error.WriteLine("DSSE verification FAILED."); return 2; } Console.Out.WriteLine($"DSSE verification OK. keyid={matchedKeyId}"); return 0; }); return c; } public static Command Inspect() { var c = new Command("inspect", "Inspect payload or DSSE envelope."); var input = new Option("--in") { IsRequired = true }; c.AddOption(input); c.Handler = CommandHandler.Create((@in) => { var bytes = File.ReadAllBytes(@in); // Heuristic: DSSE envelope contains "payloadType" if (bytes.AsSpan().IndexOf(System.Text.Encoding.UTF8.GetBytes("\"payloadType\"")) >= 0) { var env = CanonicalJson.Deserialize(bytes); Console.Out.WriteLine($"payloadType: {env.PayloadType}"); Console.Out.WriteLine($"payloadBytes: {Base64Url.Decode(env.Payload).Length}"); Console.Out.WriteLine($"signatures: {env.Signatures.Count}"); } else { var payload = CanonicalJson.Deserialize(bytes); Console.Out.WriteLine($"{payload.Cve} {payload.Package.Name} {payload.Target.Arch}-{payload.Target.Abi}"); Console.Out.WriteLine($"symbols: {payload.Symbols.Count}"); } return 0; }); return c; } public static Command Pack() { var c = new Command("pack", "Build deterministic sigpack (zip) from DSSE envelopes under a directory."); var inDir = new Option("--in-dir") { IsRequired = true }; var outp = new Option("--out") { IsRequired = true }; c.AddOption(inDir); c.AddOption(outp); c.Handler = CommandHandler.Create((inDir, @out) => { SigPackBuilder.BuildFromDirectory(inDir, @out); Console.Out.WriteLine(@out); return 0; }); return c; } } ``` The module is “real”: `mk/sign/verify/id/pack/inspect` are fully implementable now. `match` can be added once you plug in the ELF delta extractor you already want in `scanner.webservice`. --- ## 5) PatchSig library: canonical JSON, signature IDs, DSSE sign/verify, deterministic pack ### 5.1 Canonical JSON (field-order deterministic) You control determinism by writing properties in a fixed order (no reliance on reflection order). `src/StellaOps.PatchSig/CanonicalJson.cs` ```csharp using System.Text; using System.Text.Json; namespace StellaOps.PatchSig; public static class CanonicalJson { private static readonly JsonSerializerOptions Options = new() { WriteIndented = false, PropertyNamingPolicy = null, DictionaryKeyPolicy = null }; // For payload/envelope models where property order matters, implement custom writers. public static byte[] Serialize(T value) => JsonSerializer.SerializeToUtf8Bytes(value, Options); public static T Deserialize(byte[] utf8) => JsonSerializer.Deserialize(utf8, Options) ?? throw new InvalidOperationException("Invalid JSON."); } public static class SigId { public static string ComputeSha256Hex(ReadOnlySpan canonicalPayloadBytes) { var hash = System.Security.Cryptography.SHA256.HashData(canonicalPayloadBytes); return Convert.ToHexString(hash).ToLowerInvariant(); } } ``` If you want strict canonicalization beyond fixed-order models (e.g., arbitrary JSON), replace `Serialize` with a JCS implementation; for DeltaSig you typically don’t need that if you own the writer. ### 5.2 Payload model (deterministic; no timestamps) `src/StellaOps.PatchSig/DeltaSigPayloadV1.cs` ```csharp namespace StellaOps.PatchSig; public sealed record DeltaSigPayloadV1( string Schema, string Cve, PackageRef Package, TargetRef Target, NormalizationRef Normalization, List Symbols ) { public static DeltaSigPayloadV1 CreateSkeleton( string cve, string package, string soname, string arch, string abi, IEnumerable symbols, string scope) { var steps = new[] { "applyRelocs", "zeroRelocTargets", "canonNops", "canonPltGot", "canonJumpTables", "zeroPadding" }; var symScope = scope switch { "text" => ".text", "rodata" => ".rodata", "both" => ".text", // skeleton puts .text; extractor can add rodata sigs too _ => ".text" }; return new DeltaSigPayloadV1( Schema: "stellaops.deltasig.v1", Cve: cve, Package: new PackageRef(package, soname), Target: new TargetRef(arch, abi), Normalization: new NormalizationRef("elf.delta.norm.v1", steps.ToList()), Symbols: symbols.Distinct(StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal) .Select(s => new SymbolSig( Name: s, Scope: symScope, HashAlg: "sha256", HashHex: "", SizeBytes: 0, Chunks: null, Cfg: null)) .ToList() ); } } public sealed record PackageRef(string Name, string Soname); public sealed record TargetRef(string Arch, string Abi); public sealed record NormalizationRef(string RecipeId, List Steps); public sealed record SymbolSig( string Name, string Scope, string HashAlg, string HashHex, int SizeBytes, List? Chunks, CfgSig? Cfg ); public sealed record ChunkSig(int Offset, int SizeBytes, string HashHex); public sealed record CfgSig(int BbCount, string EdgeHashHex); ``` ### 5.3 DSSE sign/verify (PAE included) `src/StellaOps.PatchSig/Dsse/DsseEnvelope.cs` ```csharp using System.Text; namespace StellaOps.PatchSig.Dsse; public static class DsseTypes { public const string DeltaSigV1 = "application/vnd.stellaops.deltasig.v1+json"; } public sealed record DsseEnvelope( string PayloadType, string Payload, List Signatures ) { public static DsseEnvelope CreateSigned(string payloadType, byte[] payloadBytes, IDsseSigner signer) { var pae = DssePae.Encode(payloadType, payloadBytes); var sig = signer.Sign(pae); return new DsseEnvelope( PayloadType: payloadType, Payload: Base64Url.Encode(payloadBytes), Signatures: new List { new DsseSignature(signer.KeyId, Base64Url.Encode(sig)) } ); } public bool VerifyAny(IDsseVerifier verifier, out string? matchedKeyId) { matchedKeyId = null; var payloadBytes = Base64Url.Decode(Payload); var pae = DssePae.Encode(PayloadType, payloadBytes); foreach (var s in Signatures) { var sigBytes = Base64Url.Decode(s.Sig); if (verifier.Verify(pae, sigBytes)) { matchedKeyId = s.KeyId; return true; } } return false; } } public sealed record DsseSignature(string KeyId, string Sig); public static class DssePae { // DSSEv1 PAE: "DSSEv1" SP len(pt) SP pt SP len(payload) SP payload public static byte[] Encode(string payloadType, ReadOnlySpan payload) { var ptBytes = Encoding.UTF8.GetBytes(payloadType); var header = $"DSSEv1 {ptBytes.Length} {payloadType} {payload.Length} "; var headerBytes = Encoding.UTF8.GetBytes(header); var outBytes = new byte[headerBytes.Length + payload.Length]; Buffer.BlockCopy(headerBytes, 0, outBytes, 0, headerBytes.Length); payload.CopyTo(outBytes.AsSpan(headerBytes.Length)); return outBytes; } } public static class Base64Url { public static string Encode(ReadOnlySpan bytes) => Convert.ToBase64String(bytes.ToArray()) .TrimEnd('=').Replace('+', '-').Replace('/', '_'); public static byte[] Decode(string s) { s = s.Replace('-', '+').Replace('_', '/'); switch (s.Length % 4) { case 2: s += "=="; break; case 3: s += "="; break; } return Convert.FromBase64String(s); } } public interface IDsseSigner { string KeyId { get; } byte[] Sign(ReadOnlySpan pae); } public interface IDsseVerifier { bool Verify(ReadOnlySpan pae, ReadOnlySpan signature); } ``` `src/StellaOps.PatchSig/Dsse/DsseSignerFactory.cs` ```csharp using System.Security.Cryptography; using System.Text; namespace StellaOps.PatchSig.Dsse; public static class DsseSignerFactory { public static IDsseSigner CreateFromPem(string pemPath, string alg) { var pem = File.ReadAllText(pemPath, Encoding.UTF8); return alg switch { "ecdsa-p256-sha256" => new EcdsaP256Signer(pem), "rsa-pss-sha256" => new RsaPssSigner(pem), _ => throw new ArgumentOutOfRangeException(nameof(alg), $"Unsupported alg: {alg}") }; } private sealed class EcdsaP256Signer : IDsseSigner { private readonly ECDsa _key; public string KeyId { get; } public EcdsaP256Signer(string pem) { _key = ECDsa.Create(); _key.ImportFromPem(pem); // keyid = sha256(spki-der) var spki = _key.ExportSubjectPublicKeyInfo(); KeyId = "sha256:" + Convert.ToHexString(SHA256.HashData(spki)).ToLowerInvariant(); } public byte[] Sign(ReadOnlySpan pae) => _key.SignData(pae.ToArray(), HashAlgorithmName.SHA256); } private sealed class RsaPssSigner : IDsseSigner { private readonly RSA _key; public string KeyId { get; } public RsaPssSigner(string pem) { _key = RSA.Create(); _key.ImportFromPem(pem); var spki = _key.ExportSubjectPublicKeyInfo(); KeyId = "sha256:" + Convert.ToHexString(SHA256.HashData(spki)).ToLowerInvariant(); } public byte[] Sign(ReadOnlySpan pae) => _key.SignData(pae.ToArray(), HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } } public static class DsseVerifierFactory { public static IDsseVerifier CreateFromPem(string pemPath) { var pem = File.ReadAllText(pemPath, Encoding.UTF8); // Heuristic: if PEM contains "RSA" use RSA, else ECDSA. if (pem.Contains("BEGIN RSA", StringComparison.OrdinalIgnoreCase)) return new RsaPssVerifier(pem); return new EcdsaVerifier(pem); } private sealed class EcdsaVerifier : IDsseVerifier { private readonly ECDsa _pub; public EcdsaVerifier(string pem) { _pub = ECDsa.Create(); _pub.ImportFromPem(pem); } public bool Verify(ReadOnlySpan pae, ReadOnlySpan signature) => _pub.VerifyData(pae.ToArray(), signature.ToArray(), HashAlgorithmName.SHA256); } private sealed class RsaPssVerifier : IDsseVerifier { private readonly RSA _pub; public RsaPssVerifier(string pem) { _pub = RSA.Create(); _pub.ImportFromPem(pem); } public bool Verify(ReadOnlySpan pae, ReadOnlySpan signature) => _pub.VerifyData(pae.ToArray(), signature.ToArray(), HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } } ``` ### 5.4 Deterministic pack builder `src/StellaOps.PatchSig/Packaging/SigPackBuilder.cs` ```csharp using System.IO.Compression; using StellaOps.PatchSig.Dsse; namespace StellaOps.PatchSig.Packaging; public static class SigPackBuilder { private static readonly DateTimeOffset ZipTime = new(1980, 1, 1, 0, 0, 0, TimeSpan.Zero); public static void BuildFromDirectory(string inDir, string outZip) { var dsseFiles = Directory.EnumerateFiles(inDir, "*.dsse.json", SearchOption.AllDirectories) .OrderBy(p => p, StringComparer.Ordinal) .ToList(); var entries = new List(); var sigBlobs = new List<(string SigId, byte[] EnvBytes)>(); foreach (var f in dsseFiles) { var envBytes = File.ReadAllBytes(f); var env = CanonicalJson.Deserialize(envBytes); var payloadBytes = Base64Url.Decode(env.Payload); var sigIdHex = SigId.ComputeSha256Hex(payloadBytes); var sigId = $"sha256:{sigIdHex}"; // best-effort parse payload for index fields var payload = CanonicalJson.Deserialize(payloadBytes); var relPath = $"sigs/{sigId.Replace(':','-')}.dsse.json"; entries.Add(new SigPackIndexEntry( SigId: sigId, Cve: payload.Cve, Package: payload.Package.Name, Soname: payload.Package.Soname, Arch: payload.Target.Arch, Abi: payload.Target.Abi, Path: relPath )); sigBlobs.Add((sigId, envBytes)); } var index = new SigPackIndexV1( Schema: "stellaops.deltasigpack.v1", Entries: entries.OrderBy(e => e.SigId, StringComparer.Ordinal).ToList() ); var indexBytes = CanonicalJson.Serialize(index); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outZip))!); if (File.Exists(outZip)) File.Delete(outZip); using var fs = File.Create(outZip); using var zip = new ZipArchive(fs, ZipArchiveMode.Create); AddEntry(zip, "index.json", indexBytes); foreach (var (sigId, bytes) in sigBlobs.OrderBy(x => x.SigId, StringComparer.Ordinal)) { var rel = $"sigs/{sigId.Replace(':','-')}.dsse.json"; AddEntry(zip, rel, bytes); } } private static void AddEntry(ZipArchive zip, string path, byte[] bytes) { var entry = zip.CreateEntry(path, CompressionLevel.Optimal); entry.LastWriteTime = ZipTime; using var s = entry.Open(); s.Write(bytes, 0, bytes.Length); } } public sealed record SigPackIndexV1(string Schema, List Entries); public sealed record SigPackIndexEntry( string SigId, string Cve, string Package, string Soname, string Arch, string Abi, string Path ); ``` --- ## 6) Where ELF extraction plugs in (without contaminating the CLI contract) You keep the CLI module clean by depending on an **extractor interface** that lives in scanner/webservice libraries, not in the CLI itself. Example SPI (lives in `StellaOps.PatchSig` or `StellaOps.Scanner.ElfDelta.Abstractions`): ```csharp public interface IElfDeltaExtractor { // Takes a skeleton payload and fills hashHex/sizeBytes/chunks/cfg for each symbol entry. DeltaSigPayloadV1 PopulateFromElf(DeltaSigPayloadV1 skeleton, string elfPath); } ``` Then `stella deltasig mk --elf /path/to/libfoo.so.1 ...` can become a “fully automatic” authoring tool by resolving `IElfDeltaExtractor` from DI when present. If it isn’t present, `mk` still creates a valid skeleton that can be filled by CI tooling. This respects your architectural split: heavy analysis logic stays in scanner/webservice libraries; the CLI module remains a thin orchestration surface. --- ## 7) What you get immediately * A first-class `stella deltasig` module with working: * deterministic payload emission * DSSE signing and verification (ECDSA P-256 or RSA-PSS) * deterministic offline pack bundling * stable signature IDs * A clean seam to plug your ELF normalization/hasher implementation (the complex part) without redesigning CLI later. If you want next, I can add the `match` command and define the exact evidence JSON object that `scanner.webservice` should emit into VEX (“Fixed by backport (verified)”) so the CLI and scanner share the same evidence type byte-for-byte.