Building Deterministic
Skater .NET Release Pipelines
Turn ad-hoc GUI obfuscation into a repeatable, auditable release process. Fixed XML profiles, MSBuild determinism, secure key injection, and mandatory rename-map archival keep every protected binary reproducible and supportable.
Executive Takeaway
A deterministic Skater release is achieved by three non-negotiable practices: (1) version-control a single XML settings profile that locks every obfuscation option, (2) compile with MSBuild <Deterministic>true</Deterministic>, and (3) re-sign inside Skater while publishing both the protected binary and its rename map under the same release identifier. Anything less produces unsupportable production binaries.
1 Why Determinism Matters in Protected Releases
Most teams discover the cost of non-deterministic obfuscation only after a production incident. A crash dump arrives with fully scrambled method names; the developer who last ran the GUI is offline; the rename map that would have decoded the stack has already been overwritten by a later local build. The release is effectively unsupportable.
Determinism closes that loop. When the same source commit, the same MSBuild flags, and the same Skater XML profile always produce the identical protected binary and the identical rename map, every release becomes an accountable artifact. Support teams can retrieve the exact map months later; auditors can prove bit-for-bit provenance; hot-fixes can be re-built without introducing new symbol mappings.
- GUI settings drift between developers
- Random naming tokens on every run
- Maps discarded or overwritten locally
- Strong-name signature broken after protection
- Impossible to prove which binary matches which commit
- Single versioned XML profile in Git
- Fixed naming method + options
- Rename map published with every build
- Re-signing performed inside Skater
- SHA-256 of protected binary is stable across agents
2 Concrete Pipeline Recipe
The following patterns are battle-tested on Azure DevOps, GitHub Actions, and Jenkins agents running Windows. They assume Skater is already installed on the build agent (or restored from a tools cache) and that a strong-name key is injected as a secure file or secret.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(TF_BUILD)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
<!-- Post-build Skater protection for Release only -->
<Target Name="SkaterProtect" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
<Exec Command=""$(SkaterPath)\Skater.exe" -XMLFILE:"$(ProjectDir)skater-profile.xml" -SOURCE:"$(TargetPath)" -OUTPUT:"$(TargetDir)Protected" -KEY:"$(StrongNameKeyPath)" -WRITELOG:"$(TargetDir)Protected\skater.log"" />
</Target>
</Project>
# azure-pipelines.yml - deterministic Skater release
trigger:
branches:
include: [ main, release/* ]
pool:
vmImage: 'windows-latest'
steps:
- task: DotNetCoreCLI@2
displayName: 'Deterministic Release Build'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '-c Release -p:Deterministic=true'
- task: DownloadSecureFile@1
name: snk
inputs:
secureFile: 'release.snk'
- script: |
"%ProgramFiles(x86)%\RustemSoft\Skater\Skater.exe" ^
-XMLFILE:"$(Build.SourcesDirectory)\skater-profile.xml" ^
-SOURCE:"$(Build.SourcesDirectory)\bin\Release\net8.0\MyApp.dll" ^
-OUTPUT:"$(Build.ArtifactStagingDirectory)\Protected" ^
-KEY:"$(snk.secureFilePath)" ^
-WRITELOG:"$(Build.ArtifactStagingDirectory)\Protected\skater.log"
displayName: 'Skater protect + re-sign'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/Protected'
ArtifactName: 'skater-protected-$(Build.BuildNumber)'
# Invoke-SkaterRelease.ps1
param(
[string]$Assembly = ".\bin\Release\net8.0\MyApp.dll",
[string]$Profile = ".\skater-profile.xml",
[string]$OutDir = ".\bin\Release\Protected",
[string]$KeyPath = $env:SKATER_SNK_PATH
)
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$skater = "${env:ProgramFiles(x86)}\RustemSoft\Skater\Skater.exe"
& $skater -XMLFILE:$Profile -SOURCE:$Assembly -OUTPUT:$OutDir -KEY:$KeyPath -WRITELOG:(Join-Path $OutDir "skater.log")
if ($LASTEXITCODE -ne 0) { throw "Skater failed with exit code $LASTEXITCODE" }
$hash = (Get-FileHash (Join-Path $OutDir (Split-Path $Assembly -Leaf)) -Algorithm SHA256).Hash
Write-Host "Protected SHA-256: $hash" -ForegroundColor Green
3 Profile Discipline: Treat the XML as Infrastructure
Skater’s preferred automation model is “configure once in the GUI, then replay via XML.” That XML file becomes the single source of truth for every protection decision: which members are renamed, which naming method is used, whether strings are concealed, whether control-flow is applied, and so on.
Export from GUI, commit to Git
After configuring an assembly in the Skater UI, save the settings. The resulting XML (commonly named skater-profile.xml or similar) must live in the repository alongside the project. Pull-request review of profile changes prevents accidental relaxation of protection rules.
Lock the naming method
Choose one of the fixed naming conventions (unreadable characters, alpha-numeric, etc.) and never switch it between releases of the same product line. Changing the method mid-stream produces incompatible rename maps and breaks historical de-obfuscation.
Prefer -XMLFILE over long command lines
While Skater accepts individual switches (-ALLPRIVATE, -CONCEALSTRINGS, -FLOW, etc.), embedding those choices inside a versioned XML profile is far more maintainable and less error-prone in multi-project solutions.
4 Strong-Name Key Handling in CI
Obfuscation rewrites metadata; any prior strong-name signature is destroyed. Skater must therefore re-sign the assembly. The only safe place for the private key material is a secret store that injects the file at job start and guarantees its deletion at job end.
- Store the .snk / .pfx as a secure file or encrypted secret.
- Map it to a temporary path that is never checked into source control.
- Pass that path via the
-KEYargument (or the corresponding XML setting). - Verify the signature after protection with
sn -v. - Wipe the temporary key file in a finally / always block.
5 Rename-Map Archival Is Non-Optional
Skater emits an XML rename map that records every original name and its obfuscated counterpart. Without that exact map, a production stack trace is opaque noise. Treat the map with the same retention policy as the binary itself:
Include the map in the same build artifact (or adjacent artifact) as the protected assemblies, keyed by build number or Git tag.
Retain maps for the entire support lifetime of the release. A symbol server or dedicated artifact feed works well.
Maps reveal original identifiers; restrict download permissions to support and engineering roles.
6 Verification Loop Before Promotion
Never promote a protected package until the following three checks pass on the CI agent:
sn -v Protected\MyApp.dll
must report a valid strong name.
Two consecutive pipeline runs from the identical commit must produce the same SHA-256 for the protected binary.
Execute the integration / smoke suite against the protected assemblies, not the unobfuscated intermediates.
7 Common Failure Modes and Fixes
| Symptom | Likely Cause | Remediation |
|---|---|---|
| FileLoadException / strong-name failure | Signature applied before Skater ran, or -KEY omitted. | Always re-sign inside Skater via -KEY or the XML key setting. |
| Different SHA-256 on successive builds | Deterministic MSBuild flag missing, or profile changed. | Force Deterministic=true and pin the exact XML profile in Git. |
| Unreadable production stack traces | Rename map was never archived or belongs to a different build. | Publish the map under the same artifact name / build number as the binary. |
| Skater exits with missing dependency | Dependent assemblies not on the probe path. | Ensure the full output directory is present or supply additional search paths. |
8 Release Checklist
No ad-hoc GUI settings used for release builds.
Pre-obfuscation IL is identical across agents.
Never present in source control or persistent disk.
Same build number / Git tag, long retention.
sn -v succeeds; SHA-256 matches previous run of same commit.