Enterprise signing services in Conveyor 14

Conveyor 14 is out and we’ve added a long-awaited feature: support for swapping out the built-in signing engine. For companies that operate in-house signing servers this is often a mandatory requirement, and now it’s easy to integrate Conveyor based releases with those processes. Conveyor 14 also includes numerous bug fixes and usability improvements, driven by user feedback on what they find easy and hard. So let’s take a look at what’s new.

Custom signing scripts

Code signing links a program with the person or organization that made it. Large enterprises have a need to control who can sign things and how, and often have their own internal signing servers. Unfortunately there are no common protocols for these servers, so Conveyor 14 now supports this need by allowing you to replace the built-in signing engine with your own custom script. This script can be anything you like, and Conveyor will pass it the path to the file to be signed and the path to the output file. Here’s an example of how to use it:

app {
  windows {
    sign {
      scripts {
        // Custom script for signing the MSIX package.
        msix = "my-msix-signing-script.bat $MSIX"
        // Custom script for signing the individual binary files.
        binary = "my-binary-signing-script.bat $FILE"

The script should be executable by Windows and will receive the (fully quoted) path to the MSIX or EXE/DLL file to be signed where the $MSIX or $FILE tokens are found. The file should be signed in-place. Conveyor will wait for the script to finish before continuing with the build. A typical script will upload the file to a signing server, sign it, and download it again.

The same support also exists for macOS signing. Here’s an example of how to use it:

app {
  mac {
    sign {
      scripts {
        // Custom script for signing the bundle.
        app = "my-bundle-signing-script.sh $BUNDLE $ENTITLEMENTS"

        // Custom script for signing the individual Mach-O binary files.
        binary = "my-binary-signing-script.sh $FILE $ENTITLEMENTS $IDENTIFIER"

This is similar in concept, but the scripts take more arguments. The $BUNDLE token is the path to the app bundle, the $ENTITLEMENTS token is the path to the entitlements file, and the $IDENTIFIER token is the bundle identifier. The script should sign the file in-place and Conveyor will wait for the script to finish before continuing with the build. The entitlements file will be a .plist file of the form expected by Apple’s codesign tool.

Conveyor relies heavily on its disk cache to accelerate reruns of builds, so the contents of the script will be incorporated into the internal cache key. Signing therefore won’t be rerun unless the file to be signed changed, or the script/program/command line used for signing changes. If you are iterating on the script you can bypass the caching by just adding a version number to a comment in the script header for example, but be aware that dependencies of your script or program aren’t recursively fingerprinted.

Insert root TLS certificates into JVM apps

One of the advantages of distributing desktop apps is that you can control the TLS stack, meaning that backend servers can use self-signed certificates instead of CA certificates. This is faster, cheaper, more secure and more robust than using CA certificates because a custom certificate can’t be revoked by a third party or issued accidentally by some CA you never heard of, and you can manage expiry and replacement explicitly. This protects you from outages caused by unnoticed certificate expiry, a common cause of massive outages even in well run services. And of course such certificates cost you nothing.

JVM apps come with their own TLS root store in a file that’s shipped as part of the JDK. This file is read-only and can’t be changed at runtime, so if you want to use a custom root certificate you have to add it to the file before you ship your app. To meet this need you can now add custom root TLS certificates to your JVM app’s trust store using the app.jvm.additional-ca-certs key.

The feature is easy to use. Just add the following to your conveyor.conf:

app.jvm.additional-ca-certs = {
  my-alias = ./path-to-certificate.cer 

The alias you pick doesn’t really matter but is required for the Java keystore format. Now you may wonder, where do you get the .cer file from? You can get it from the server you want to connect to. For example, if you’re connecting to a server with a self-signed certificate you can use the openssl command line tool to extract the certificate:

openssl s_client -connect myserver.myorg.com:443 -showcerts -servername myserver.myorg.com </dev/null 2>/dev/null | openssl x509 -outform DER > myserver.cer

Let’s print the details of the certificate we just downloaded:

keytool -printcert -file myserver.cer

On Linux a self-signed cert will often be created automatically and placed in /etc/ssl/certs (on Debian/Ubuntu it’s called “snakeoil”). You can copy it from there to your project. If you need to create one from scratch, you can create one without any passphrase like this:

openssl genpkey -algorithm RSA -out private_key.pem
openssl req -new -key private_key.pem -out request.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=example.com"
openssl x509 -req -days 3650 -in request.csr -signkey private_key.pem -outform DER -out certificate.cer

The details provided in the -subj flag doesn’t really matter for self-signed certificates, but the CN (common name) should match the server’s hostname.

Usability improvements

Here’s some of the ways we’ve improved Conveyor to be more helpful based on real user support queries:

  • Remap specs now interpret a rule ending in / as a glob /** meaning “all files under this directory”.
  • If you refer to an unset environment variable the error message now reminds you to use the export keyword.
  • Conveyor now catches attempts to sign Mac apps using a Mac Installer certificate (meant for .pkg files), rather than a standard Developer ID certificate.
  • Changing the site URL for a commercial license now requires an explicit command. The previous approach was a little too frictionless and sometimes users changed the app associated with a license key by accident.

We’ve also fixed various bugs and crashes, so if you’re experiencing trouble with Conveyor, it’s worth trying the latest version. Here’s a few of the things we’ve fixed:

  • Conveyor now handles non-UTF8, non-English Windows systems better.
  • Fixed a crash that could occur if you don’t have npm installed whilst packaging an Electron project or if it’s too old, and detects the case where node_modules directories contain symlinked packages.
  • We fixed another case where exploring the cache directory with the macOS Finder could break things due to hidden .DS_Store files littering the place up, and audited the rest of the caching code to make sure it’s robust against other Finder-related issues.
  • For JVM apps the Gradle plugin now handles the newest style of Compose Multiplatform projects properly.
  • Fixed a bug for server packages that contain Apache config templates.