Rustemsoft
Release engineering note

Deterministic .NET Obfuscation: A Release Evidence Playbook

A protected assembly should be a traceable release outcome-not a binary produced by a workstation ritual. This guide treats obfuscation as a controlled pipeline stage with inputs, outputs, signing, and diagnostic evidence that can be recovered months later.

The practical objective: given the same approved inputs, your pipeline should produce the same protected deliverable, preserve the information needed to investigate it, and prove which process created it. Deterministic compilation alone is necessary but not sufficient; the protection configuration, randomization behavior, signing operation, dependencies, and toolchain must also be controlled.

Start with release evidence, not a UI workflow

The usual failure begins after a production incident. The team has an obfuscated method name in a stack trace, but the matching map file is missing; or a binary was protected locally with settings that were never committed. Rebuilding “something similar” does not repair that evidence gap.

Instead, define one release record for every protected package. It should connect the source revision and build identity to the protection settings, protected binary hash, signing result, map file, and execution log. The record changes the question from “who remembers how this was built?” to “which immutable artifacts describe this build?”

Minimum release packet

  • Protected assemblies and package manifest
  • Version-controlled .opq configuration
  • Rename map such as MyApp.map.xml
  • Protection log, input revision, and tool version
  • SHA-256 checksums and strong-name verification result

Build the pipeline around controlled inputs

1. Make compilation repeatable

Build in a clean CI environment and enable deterministic compilation in the project. Also pin the SDK, restore dependencies consistently, and avoid environment-specific build steps. A deterministic compiler cannot compensate for changing package versions or generated source that embeds timestamps.

<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <Configuration Condition="'$(Configuration)' == ''">Release</Configuration>
  <Deterministic>true</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

2. Treat the protection project as source code

Export Opaquer settings to an .opq project file and review it like application code. It captures protection choices-such as exclusions, renaming, string protection, and control-flow settings-that otherwise live only in a GUI session. Where the configuration offers a random seed, fix it for the release path. If changing protection rules is intentional, make it a reviewed configuration change with a new release record.

3. Sign after transformation

Protection modifies IL and metadata. Therefore, a signature applied before protection cannot authenticate the final protected assembly. Supply the signing key to the protected-build stage through secure CI secret storage, re-sign the output, and verify the final file. Never commit an .snk or .pfx to the repository.

4. Publish diagnostics with the binary

The map file is not disposable output. It is part of the incident-response contract for that exact distribution. Store it in restricted artifact storage under the release version or commit identity, together with the log and checksum manifest. Access controls matter: maps can expose relationships between protected and original names.

A CI stage with explicit artifacts

The following shape intentionally keeps paths and artifacts visible. Adapt names and secret mechanisms to the CI system, but do not leave key details implicit.

# Example protected-release stage (Windows runner)
dotnet build MyApp.sln -c Release -p:Deterministic=true

Opaquer.Console.exe ^
  /i:"bin\Release\net8.0\MyApp.dll" ^
  /o:"artifacts\protected\MyApp.dll" ^
  /config:"build\opaquer.opq" ^
  /snk:"%SIGNING_KEY_PATH%" ^
  /map:"artifacts\protected\MyApp.map.xml" ^
  /log:"artifacts\protected\opaquer.log"

sn -v artifacts\protected\MyApp.dll
certutil -hashfile artifacts\protected\MyApp.dll SHA256
# publish artifacts/protected with the release identifier

Verify the release as a product

Verification should look beyond whether the CLI returned success. Run automated tests against the protected binaries, validate strong-name status, and record hashes. When your process claims byte-level reproducibility, run two clean builds from the same controlled inputs and compare output hashes. A mismatch is an investigation signal, not a reason to overwrite the earlier map.

Observed resultLikely boundary to inspectCorrective action
Protected hash changes between equivalent CI runsCompiler inputs, dependency resolution, timestamped generation, or protection seedPin toolchain and dependencies; enable deterministic compilation; make protection settings and seed explicit.
FileLoadException or signature validation failureAssembly was altered after its last valid strong-name signatureRe-sign after protection through the CI-provided key, then verify the protected output.
Stack trace cannot be decodedMap was omitted, overwritten, or belongs to a different releasePublish and retain a uniquely identified map for every release; never substitute a map from another build.
Protection fails on dependenciesResolver cannot locate required assembliesProvide approved dependency search paths and keep dependency inputs in the release record.

Operational model

Think of the release as a chain of custody: source revision produces a deterministic input assembly; an approved protection configuration transforms it; a controlled signing operation authenticates it; and stored artifacts explain it. This model allows a hotfix, audit, or crash investigation to refer to a concrete release packet rather than human memory.

Frequently asked questions

Does a fixed seed guarantee reproducibility by itself?

No. It stabilizes one potential source of variation in the protection stage. Reproducibility also depends on consistent compiler settings, source and dependency inputs, Opaquer configuration, tool versions, and signing behavior.

Should the map file be shipped to customers?

Normally, retain it in protected internal artifact storage instead. It is needed for support and de-obfuscation, but it should remain paired with the exact release it describes.

What is the most useful release identifier?

Use an identifier that can join the Git commit or immutable source revision, CI build number, published package version, and artifact location. Put the same identifier in the build metadata and retention structure.