Artem Razin
April 5, 2023

How to obfuscate .NET application in Github workflow?

This tutorial will guide you on how to enable obfuscation if you use GitHub workflow to build and deploy your .NET applications.

What are GitHub Workflows?

Today, GitHub is more than just a storage for your code. With its help you can build and deploy your applications by defining a set of tasks that are executed by the event, for example, when someone pushes changes to the master branch of your repository.

From time to time .NET developers wonder how to integrate obfuscation into this process. Let's create a sample project, set up a repository on GitHub for it, and configure a workflow to build the application. Then we will integrate obfuscation, and also learn how to provide a license key for the .NET obfuscator.

The sample application: password validator.

First, create a new private repository with a .gitignore for Visual Studio:

Create repository

Once the repository is ready, click Code and copy the URL of the repository:

Repository URL

Clone it on the local computer. Then create a skeleton of the console application, and add it to the new solution file:

dotnet new console --use-program-main
dotnet new sln
dotnet sln add PasswordValidator.csproj

The solution file will help us to build the project.

Open Program.cs and edit as shown below:

using System;
using System.Text;
using System.Security.Cryptography;
 
namespace PasswordValidator;
  
class Program
{
    static void Main(string[] args)
    {
        using (var sha256 = SHA256.Create())
        {
            Console.Write("Enter password: ");
            var password = Console.ReadLine();
  
            var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
            Console.WriteLine(string.Format($"Password hash: {Convert.ToBase64String(hash)}"));
        }
    }
}

This code reads the entered string, calculates its hash and outputs it in readable form.

Build it and run. Then enter github (it will be the correct password) and copy the hash value:

dotnet build
bin\Debug\net7.0\PasswordValidator.exe
Enter password: github
Password hash: wLAQnZQ53lf+PPA6vsy8UvTJgXDHMtO2mvXmOVrOV04=

Now we know the hash of the valid password so we can check passwords:

using System;
using System.Text;
using System.Security.Cryptography;
 
namespace PasswordValidator;
  
class Program
{
    static void Main(string[] args)
    {
        using (var sha256 = SHA256.Create())
        {
            Console.Write("Enter password: ");
            var password = Console.ReadLine();
  
            if (Convert.ToBase64String(sha256.ComputeHash(Encoding.UTF8.GetBytes(password))) == "wLAQnZQ53lf+PPA6vsy8UvTJgXDHMtO2mvXmOVrOV04=")
                Console.WriteLine("Valid");
            else
                Console.WriteLine("Not valid");
        }
    }
}

Build it and run to ensure it works as expected:

>dotnet build
>bin\Debug\net7.0\PasswordValidator.exe
Enter password: tratata
Not valid
>bin\Debug\net7.0\PasswordValidator.exe
Enter password: github
Valid

Commit changes to GitHub. Now let's look at how to create a workflow, building the application.

Configure a workflow to build the .NET application.

To add a workflow you need to create a new file in .github/workflows. So create .github, enter it, then create workflows and finally create build.yaml that contains steps required to build the application:

on: [push]
 
jobs:
  build:
 
    runs-on: ubuntu-latest
    strategy:
      matrix:
        dotnet-version: [ '7.0.x' ]
 
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET SDK ${{ matrix.dotnet-version }}
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: ${{ matrix.dotnet-version }}
      - name: Install dependencies
        run: dotnet restore
      - name: Build
        run: dotnet build --configuration Release --no-restore

Commit build.yaml and then navigate to Actions on GitHub. You will see that the application has been built well:

The workflow runs well

Enable obfuscation

It's time to enable obfuscation for the project. Add the following packages:

dotnet add package ArmDot.Client
dotnet add package ArmDot.Engine.MSBuildTasks

ArmDot.Client provides obfuscation attributes that you can use to specify obfuscation options for classes, methods or even the entire assembly. ArmDot.Engine.MSBuildTasks contains the obfuscation task that you enable in the project file.

First, let's specify that we want to virtualize Main. Virtualization is one of the obfuscation approaches that turns original method to a mess of instructions:

using System;
using System.Text;
using System.Security.Cryptography;
 
namespace PasswordValidator;
  
class Program
{
    [ArmDot.Client.VirtualizeCode]
    static void Main(string[] args)
    {
        using (var sha256 = SHA256.Create())
        {
            Console.Write("Enter password: ");
            var password = Console.ReadLine();
  
            if (Convert.ToBase64String(sha256.ComputeHash(Encoding.UTF8.GetBytes(password))) == "wLAQnZQ53lf+PPA6vsy8UvTJgXDHMtO2mvXmOVrOV04=")
                Console.WriteLine("Valid");
            else
                Console.WriteLine("Not valid");
        }
    }
}

It's not enough to add an obfuscation attribute. Also you need to enable the obfuscation task. Modify PasswordValidator.csproj as shown below:

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
 
  <ItemGroup>
    <PackageReference Include="ArmDot.Client" Version="2023.7.0" />
    <PackageReference Include="ArmDot.Engine.MSBuildTasks" Version="2023.7.0" />
  </ItemGroup>
 
<Target Name="Protect" AfterTargets="AfterCompile" BeforeTargets="BeforePublish">
  <ItemGroup>
     <Assemblies Include="$(ProjectDir)$(IntermediateOutputPath)$(TargetFileName)" />
  </ItemGroup>
  <ArmDot.Engine.MSBuildTasks.ObfuscateTask
    Inputs="@(Assemblies)"
    ReferencePaths="@(_ResolveAssemblyReferenceResolvedFiles->'%(RootDir)%(Directory)')"
    SkipAlreadyObfuscatedAssemblies="true"
  />
</Target>
 
</Project>

The new target Protect contains the obfuscation task that obfuscates the compiled assembly immediately after the compiler places it to the intermediate directory.

Rebuild the project and run it to check if it works well. After that push changes to the repository and look if the job has worked without problems:

Obfuscation works well in the workflow

How to specify your license for the obfuscator?

If you use the full version of the obfuscator, you have a license key. When you develop on the local computer, your license key is just located in the default location. Of course, one can't just place the license key to the repository. The question is where to store it if you want to use the full version of the .NET obfuscator in a GutHub workflow. Encrypted secrets to the rescue!

Navigate your repository on GitHub, then click Settings, expand Secrets and variables, and click Actions, and New repository secret. Name it ARMDOT_LICENSE_KEY and paste your license (it's crucial to put your license in a single line). Then click Add secret:

Add license to secrets

To utilize the license key, you must first update the workflow. Below is a sample for you if you are using Linux:

on: [push]
 
jobs:
  build:
 
    runs-on: ubuntu-latest
    strategy:
      matrix:
        dotnet-version: [ '7.0.x' ]
 
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET SDK ${{ matrix.dotnet-version }}
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: ${{ matrix.dotnet-version }}
      - name: Install dependencies
        run: dotnet restore
      - name: Save license
        run: |
          echo $ARMDOT_LICENSE_KEY >> ${{ runner.temp }}/ArmDotLicenseKey
        shell: bash
        env:
          ARMDOT_LICENSE_KEY : ${{ secrets.ARMDOT_LICENSE_KEY }}
      - name: Build
        run: dotnet build --configuration Release --no-restore
        env:
          ARMDOT_LICENSE_FILE_PATH : ${{ runner.temp }}/ArmDotLicenseKey

If you are using Windows, you need to use cmd as a shell. In this case, it's critical to input your license key on a single line, as cmd does not support values across multiple lines:

on: [push]
 
jobs:
  build:
 
    runs-on: windows-2022
    strategy:
      matrix:
        dotnet-version: [ '7.0.x' ]
 
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET SDK ${{ matrix.dotnet-version }}
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: ${{ matrix.dotnet-version }}
      - name: Install dependencies
        run: dotnet restore
      - name: Save license
        run: |
          echo "%ARMDOT_LICENSE_KEY%" >> ${{ runner.temp }}\ArmDotLicenseKey
        shell: cmd
        env:
          ARMDOT_LICENSE_KEY : ${{ secrets.ARMDOT_LICENSE_KEY }}
      - name: Build
        run: dotnet build --configuration Release --no-restore
        env:
          ARMDOT_LICENSE_FILE_PATH : ${{ runner.temp }}/ArmDotLicenseKey

The trick is to place the license key to a temporary file and then pass the path of the file in the environment variable ARMDOT_LICENSE_FILE_PATH. This approach works with any tool you use to build your project, dotnet build, or MSBuild.

Let's use this variable. Modify PasswordValidator.csproj:

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
 
  <ItemGroup>
    <PackageReference Include="ArmDot.Client" Version="2023.7.0" />
    <PackageReference Include="ArmDot.Engine.MSBuildTasks" Version="2023.7.0" />
  </ItemGroup>
 
<Target Name="Protect" AfterTargets="AfterCompile" BeforeTargets="BeforePublish">
  <ItemGroup>
     <Assemblies Include="$(ProjectDir)$(IntermediateOutputPath)$(TargetFileName)" />
  </ItemGroup>
  <ArmDot.Engine.MSBuildTasks.ObfuscateTask
    Inputs="@(Assemblies)"
    ReferencePaths="@(_ResolveAssemblyReferenceResolvedFiles->'%(RootDir)%(Directory)')"
    SkipAlreadyObfuscatedAssemblies="true"
    LicenseFile="$(ARMDOT_LICENSE_FILE_PATH)"
  />
</Target>
 
</Project>

Push the changes and check if it works well now:

The workflow uses license key

Conclusion

With GitHub workflows, building and deploying .NET applications is extremely easy and efficient. With the help of ArmDot, a modern cross platform obfuscator, you can integrate obfuscation into workflows in a minute.