It's the end of the year, so as well as having to do the usual "Tools We Use" post, I also need to renew our code signing certificate.

Code signing has been a funny old journey for me, and one that originally was a total pain - from trying to get the certificate in the first place to self destructing certificates to revocation. For the last couple of years certificates were provided via the use of a hardware dongle. And while I have to admit that simplified things immensely with no need to mess around with OpenSSL and conversion, it also introduced the insane requirement of requiring the token to be unlocked via a GUI dialog. Which naturally is not great when ran via CI tools like Jenkins. The number of failed builds because I forgot to unlock the token or because it had timed out...

A couple of weeks ago I read an interesting post on Scott Hanselman's blog titled "Automatically Signing a Windows EXE with Azure Trusted Signing, dotnet sign, and GitHub Actions". Earlier this week I decided to give this a go and see if I could remove the need for a hardware token, its related software and those <censored> GUI prompts.

This is more an editorial, I don't want to rehash the steps of Scott's post which is useful enough as-is.

Verification

Note that depending on your location, you may not be able to use (Azure) Trusted Signing yet. According to the Azure Portal "Trusted Signing is currently available to organizations in the USA, Canada, European Union & United Kingdom and to individual developers in the USA & Canada.". While I'm based in the UK, I was able to verify as a company. If you're an individual you may be out of luck.

Verification was quite quick - I verify Cyotek using its DUNs number and that seemed to be fine. However, I also needed to verify my own identify in addition to Cyotek itself (Scott's post doesn't mention this as I believe he just did individual). Of course, Microsoft use a completely different verification service to HRMC which use a different service to our accountant. So now there are 3 "trust me guv, we're honest" organisations with copies of private information and none of it by choice. Why can't there be some interoperability...

However, I have to admit the verification was quick and painless and didn't even involve a phone call as is usually the case. It took around 3 hours from start to end, if that.

When creating a certificate make sure you use "Public Trust" unless you know what you're doing.

Signing

Normally I use SignTool.exe with traditional certificates, but here I followed Scott's recommendation to use the dotnet sign tool. This worked fine, and also has the advantage that the exact same command can be used to sign NuGet packages too.

I didn't want to use az login as a) I was using a privilege account, so as soon as I switched back to the normal one signing would break and b) login tokens expire so it would be back to square one with failed builds. So I set up a new App Registration in Azure (with permissions to sign via the Trusted Signing Account), then I could use the AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET environment variables to allow the sign tool to do its work without fuss. It is out of scope of this editorial, how you define them will depend on your CI tool - while Scott's post had instructions for GitHub Actions, I added mine to Jenkins.

And it all just works - I have ran a few dozen builds of internal NuGet packages and full software builds and everything seems fine so far, bar the caveats in the next section.

Of course, the client secret will expire, but it is simple enough to regenerate and update the environment. I'd take that over unlocking a token multiple times a day!

It should be noted that this isn't just for .NET assemblies. I use the same process to sign C++ resource DLLs and setup executables (created with Delphi) just as I did with SignTool.exe without fuss.

Caveats

No support for SHA1

The main caveat is the sign tool doesn't allow the generation of sha1 signatures. This isn't a problem if you build for current platforms, but if you have legacy assemblies for use with .NET Framework 3.5, 4.0 etc and in turn for use with older versions of Windows that don't support sha256, you won't be able to sign with this tool.

Can't dual sign

This goes hand in hand with the first point. For the legacy assemblies that could be used on older versions of Windows, I currently dual sign - first with the sha1, then with sha256. (I honestly can't remember why, I must have read it somewhere it was a good idea). However, I tried to sign a couple of assemblies using the old certificate with sha1 and then with the new and discovered the final assembly only had the sha256 signature. I don't know if this is because the new tool deletes existing signatures, or if you can't sign the same binary with different certificates.

Either way, this is a problem for my open source libraries which target old versions of .NET. Most likely I'll just sign them with the sha256 - means the signature can't be read on older Windows, but I assume such usage is minimal.

Short certificate expiry and NuGet

I mentioned earlier that I could sign NuGet packages via trusted signing as well. This works perfectly well, except you need to load the certificate in your NuGet Account. And the certificates have incredibly short lifetimes - just 3 days.

Currently I release all our open source packages manually, which so far isn't much of a problem but it would be nice to step away from this. I think In future I'll look at using GitHub Actions for this as the source is all hosted there.

In the interim though as per this GitHub Issue, I'm using the Knapcode.CertificateExtractor tool to extract the CER file from the NuGet package and then dropping it into NuGet before pushing. A minor pain, but hopefully there will either be an official solution in future, or I'll switch to something else like the mentioned GitHub Actions.

For reference, this is the command I use to get just the top certificate for loading into NuGet. Obviously you would replace inputs and outputs accordingly!

bat
nuget-cert-extractor --file src\bin\Release\Cyotek.Data.Nbt.4.1.0-beta.nupkg --output src\bin\Release --author --leaf --code-signing

Final Thoughts

It's early days, but I'm holding off renewing my traditional certificate for now and will release some nightly builds using this new process. I will update this post if I come across anything new. For example, I haven't tested signing MSI or VSIX yet - not that I have any plans to use these formats anytime soon.

Cost wise, it will be ever so slightly more expensive than the current certificate - these were via DigiCert reseller, so much cheaper than buying direct and certainly never having to deal with password prompts again is worth the slight increase.


Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Comments