Signed, sealed, and delivered

By Norm Tovey-Walsh on December 21, 2022 at 01:19p.m.

I have been trying to build SaxonCS for .NET such that I could deliver it on MacOS without warning messages for a long time. It has not been an easy or enjoyable adventure. Here are some breadcrumbs for the next poor soul forced to tread down this path.

You can’t do this with .NET 5. That’s probably less important today than it was when I started. I don’t understand the details, but something has been fixed in .NET 6 that isn’t going to be backported to .NET 5. (There’s a comment to that effect in an issue, but I can’t now locate that issue.)

There are several problems that have to be solved. The application has to be built such that it will run when signed. All of the various pieces have to be (correctly) signed. A DMG must be constructed to distribute the application (maybe I don’t have to do this step, but it’s reasonably what users expect). The DMG has to be signed. And the whole thing has to be notarized by Apple so that it will open without warnings.

The objective

A complete, hands off, CI-driven build of a C# application to produce a MacOS DMG file that a user can open and use without any warnings about unsigned code or potentially malicious applications.

Prerequisites

Before you begin, there are some things you have to have setup.

Use the nuget command to install Dotnet.Bundle:

$ nuget install Dotnet.Bundle

This package constructs the “bundle” of files that MacOS expects for an application. (That’s the application.app directory and its descendants.)

Application files

Start with your application. In our case, this complex beast:

using System;

namespace HelloWorld
{
    public class HelloWorld
    {
        public static void Main(string[] arg)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

You will also need a .csproj file. Here’s one that works for me:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
    <PublishReadyToRun>true</PublishReadyToRun>
    <RuntimeIdentifier>osx-x64</RuntimeIdentifier>
    <UseHardenedRuntime>true</UseHardenedRuntime>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
    <IncludeSymbolsInSingleFile>false</IncludeSymbolsInSingleFile>
  </PropertyGroup>

  <PropertyGroup>
    <CFBundleName>HelloWorld</CFBundleName>
    <CFBundleDisplayName>HelloWorld</CFBundleDisplayName>
    <CFBundleIdentifier>com.saxonica.helloworld</CFBundleIdentifier>
    <CFBundleVersion>1.0.0</CFBundleVersion>
    <CFBundleShortVersionString>1.0.0</CFBundleShortVersionString>
    <CFBundleExecutable>HelloWorld</CFBundleExecutable>
    <CFBundleIconFile>HelloWorld.icns</CFBundleIconFile>
    <NSPrincipalClass>NSApplication</NSPrincipalClass>
    <NSHighResolutionCapable>true</NSHighResolutionCapable>
    <NSRequiresAquaSystemAppearance>false</NSRequiresAquaSystemAppearance>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="DotNet.Bundle" Version="0.9.13" />
  </ItemGroup>

</Project>

The first property group specifies properties of the build, the second defines how the application will be bundled, and the last item group is necessary to make the bundler part of the build.

Notes:

  1. You must specify net6 for the framework and create a single file, self-contained application.
  2. You must use the hardened runtime.
  3. You must include native libraries for self extraction.
  4. You must not include symbols in the single file, that’s an option that apparently stopped working in .NET 5.
  5. I created HelloWorld.icns from a PNG with ImageMagick.

Building the application

Add Dotnet.Bundle to the project:

$ dotnet add package Dotnet.Bundle

(You only have to do this once.)

Build the application:

$ dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:Configuration=Release

You can run the bundled application to make sure it works:

$ bin/Release/net6.0/osx-x64/publish/HelloWorld.app/Contents/MacOS/HelloWorld
Hello, World!

Sign the application

Next we have to sign the application. But before we can do that, we have to make an entitlements plist file. I called mine entitlements.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
          "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.disable-executable-page-protection</key>
    <true/>
</dict>
</plist>

Now we can sign it:

$ codesign --force --options runtime --entitlements ./entitlements.plist --deep \
  --sign "Developer ID Application: YOUR DEVELOPER ID GOES HERE" \
  --timestamp bin/Release/net6.0/osx-x64/publish/HelloWorld.app

You must use the entitlements option and the timestamp option. (You probably need all the other options too, but those were the ones that I initially overlooked.)

You can run it again to make sure it still works:

$ bin/Release/net6.0/osx-x64/publish/HelloWorld.app/Contents/MacOS/HelloWorld
Hello, World!

(It didn’t for me for the longest time!)

Construct the DMG

Fire up the DMG Canvas application. (Yes, I know, I said I wanted this to be a hands-off process. I believe DMG Canvas can be automated, but I haven’t tried to figure out exactly how yet.)

The first time you open it up, go to the Preferences dialog and add your Apple ID and a one-time password on the Notarization tab:

Screen capture of the DMG Canvas notarization preferences tab.

This will enable signing and notarizing the DMG later.

On the main screen, add the application to the canvas. On the right hand side, choose the second tab and select “Code Sign and Notarize” in the drop down. You’ll have to specify the certificate you want to use, your Apple ID, and the primary bundle ID. (I have no idea what that means in this context, but you have to put something in there.)

Screen capture of the DMG Canvas main screen with the notarization dialog shown on the right.

Click the “Build” button in the upper right corner, fill in the details,

Screen capture of the DMG Canvas build dialog.

Hit Save and wait (nervously, and for quite a while) for the results!

Screen capture of the DMG Canvas modal dialog while it's building and notarizing the DMG file.

With luck, it all goes smoothly and you get back a signed, notarized DMG file.

There’s obviously more to be done in the DMG: it needs a background image, the standard symlink to /Applications should be present, etc. But I got a working DMG file out of it so, I’m declaring victory for the moment.