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
.opqconfiguration - 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 result | Likely boundary to inspect | Corrective action |
|---|---|---|
| Protected hash changes between equivalent CI runs | Compiler inputs, dependency resolution, timestamped generation, or protection seed | Pin toolchain and dependencies; enable deterministic compilation; make protection settings and seed explicit. |
FileLoadException or signature validation failure | Assembly was altered after its last valid strong-name signature | Re-sign after protection through the CI-provided key, then verify the protected output. |
| Stack trace cannot be decoded | Map was omitted, overwritten, or belongs to a different release | Publish and retain a uniquely identified map for every release; never substitute a map from another build. |
| Protection fails on dependencies | Resolver cannot locate required assemblies | Provide 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.
Related .NET Obfuscator Resources
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.