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.

Contents

  • What are GitHub Workflows?
  • The sample application: password validator.
  • Configure a workflow to build the .NET application.
  • Enable obfuscation
  • How to specify your license for the obfuscator?
  • Conclusion

    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.