Code Signing

Overview

Code signing is the process of applying a digital signature to a piece of code so that it can later be verified that the code was written by a particular author and has not been modified. In Niagara, this allows us to verify that modules that are installed were written by a trusted author and helps to prevent malicious code from getting installed.

We plan to roll out third party module signing requirements in phases starting in 4.8. See the main module signing documentation for more information.

Default Signing Profile

Code signing is enabled by default in the Niagara build environment starting in 4.6. Any module you build will automatically be signed with a generic, auto-generated, self-signed certificate. After building a module, you will notice some files in USER_HOME/.tridium/security. The niagara.signing.jks file is the default signing profile keystore, and the niagara.signing.xml file stores settings for the signing profile. This is a good starting point and useful for development, but you will probably want to sign your modules with a different key when releasing, and you may want to use a CA signed certificate in development to make testing easier. To make changes to the signing profile, you can either modify the default signing profile, or create a new one.

Creating a Signing Profile

Note that you will need a working Niagara 4 development environment to run the Gradle commands in this example. If you do not have one, the best place to start is with the New Module Wizard in Workbench.

To create a signing profile, the Gradle task “createProfile” is used:

gradlew :createProfile --profile-path path\to\my_signing_profile.xml

The createProfile task will automatically create both a keystore and a settings file. By default, the signing profile will not have any keys in it. Before you create any keys, you need to modify the default profile so it generates keys for your organization:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>Code Signing Properties</comment>
<entry key="niagara.signing.storetype">JCEKS</entry>
<entry key="niagara.signing.validity">365</entry>
<entry key="niagara.signing.refresh">183</entry>
<entry key="niagara.signing.profileType">
  com.tridium.gradle.plugins.signing.profile.RestrictedSigningProfile
</entry>
<!--Customize dname to your organization. '${alias}' will inject the alias of any created certificate -->
<entry key="niagara.signing.dname">
  C=US,ST=Any State,L=Any City,O=Acme\\, Inc,OU=Engineering,CN=${alias}
</entry>
<entry key="niagara.signing.storepass">…</entry>
<entry key="niagara.signing.keyalg">RSA</entry>
<entry key="niagara.signing.keysize">3072</entry>
</properties>

This should be set to some unique string that identifies your organization. Note that when purchasing commercial certificates, the certificate authority you purchase from may impose additional requirements or restrictions on this field. In addition to changing the default distinguished name, you may also need to change the niagara.signing.keyalg or niagara.signing.keysize fields. The accepted minimum keysize and supported algorithms are subject to change over time.

Once you’ve modified the default profile XML, you can create a self-signed key with the following command:

gradlew :generateCertificate --profile-path path\to\my_signing_profile.xml --alias MyCertificate

This command will create a certificate with the alias “MyCertificate” and store it in your signing profile. You can also import existing certificates to use them with Niagara as outlined below. Note that the keystore password and certificate private key passwords are all managed by the signing profile, but they are stored in plaintext in the profile XML – take care to securely store this file.

For this new signing profile to take effect, we need to configure our Gradle build to load it. This is done by configuring the root ‘build.gradle.kts’ of your build:

...
signingServices {
  // Disable the use of the default profile; this will cause build failures instead
  // of silently falling back to the default
  signingProfileFactory {
    allowDefaultProfile.set(false)
  }
}

niagaraSigning {
  aliases.set(listOf("MyCertificate"))
  signingProfileFile.set(project.layout.projectDirectory.file("path/to/my_signing_profile.xml"))
}
...

Note that we also disable the use of the default profile. This will cause your build to fail instead of silently building with the wrong signing profile.

Specifying Certificate Alias

By default, modules will be signed using the certificate in your signing profile with the alias Niagara4Modules. Since we used a different alias for our certificate, we specified a different alias:

...
niagaraSigning {
  aliases.set(listOf("MyCertificate"))
  ...
}
...

This should be sufficient in most cases, but if you discover you need to sign certain modules with a different certificate, you can specify the alias on a module specific basis by adding the same block to your module’s gradle file.

...
niagaraSigning {
  aliases.set(listOf("MyOtherCertificate"))
}
...

If you find you need to sign any modules with multiple certificates, you can add multiple aliases either by adding them as a list.

aliases.set(listOf("MyCertificate", "MyOtherCertificate"))

Timestamping

Timestamping provides proof that a signature was created at a particular time. This allows us to be sure that a module was signed while the signing certificate was within its validity period. Without timestamping, a signed module cannot be successfully validated after the signing certificate has expired, so we recommend timestamping all modules that will be released for use in production environments.

To enable timestamping, add the following to your signing profile

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment>Code Signing Properties</comment>
  <entry key="niagara.signing.keypass.my-cert">K3yP@ss</entry>
  <entry key="niagara.signing.storepass">St0reP@ss</entry>
  <entry key="niagara.signing.profileType">
    com.tridium.gradle.plugins.signing.profile.RestrictedSigningProfile
  </entry>

  <!--  The lines below will enable timestamping -->
  <entry key="niagara.signing.standardtsa">
    http://timestamp.digicert.com
  </entry>
</properties>

The niagara.signing.standardtsa property can be set to any publicly available RFC 3161 compliant SHA-256 time stamp authority server. The DigiCert TSA has worked well for us, so there should be no reason to change this unless the server goes down or there is a connectivity problem from your network.

Enabling timestamping will require an internet connection when building modules, but the signature and timestamp can be verified in Niagara even when running offline.

Establishing Trust

When we eventually require code signing for all third party modules, the certificate that a module is signed with must be trusted for it to run in a Niagara installation. There are a few ways this can be accomplished. The first is to purchase a signed certificate from a trusted certificate authority. This is the easiest method because the certificate will automatically be trusted by all Niagara installations, so there is no need to install additional certificates when installing the signed module. This is also the most expensive method since a new certificate must be purchased each time it expires. If this cost is prohibitive, one of the following alternatives can be used.

The second option is to use a certificate that is signed by an internal certificate authority, then install the CA certificate into the user trust store of all Niagara installations where the module will be installed. Multiple certificates can be signed by the same internal CA, and any Niagara installation with the CA certificate installed will trust any module signed with any certificate issued by the CA. To import the internal CA certificate into your Niagara user trust store, you must first obtain the CA certificate in PEM format. If it is not available in PEM format, it can be converted using OpenSSL or a similar tool. You can then import the certificate using Niagara’s Certificate Management.

The third option is to sign modules with a self-signed certificate and install that certificate into the user trust store of any Niagara installation where the module will be installed. This is the least scalable approach, but can be useful for small scale operations or during development. Self-signed certificates are not allowed when the module verification mode is set to “high”, which will be the default in a future version, so running modules signed with a self-signed certificate will require the module verification mode to be set back to “medium” in the future. To import the certificate into your Niagara user trust store, you will have to first export it from your keystore.

gradlew :exportCertificate --profile-path path\to\my_signing_profile.xml --alias MyCertificate --pem-file my-cert.pem

You can then import the certificate using Niagara’s Certificate Management. There is also a provisioning job available to install a certificate into the user trust store of multiple hosts at once to make things easier for larger installations.

Using Signed Certificates

The first two approaches described above will require you to get your certificate signed by a CA and import it back into your keystore. To do this, you first need to generate a certificate signing request with the following command

gradlew :exportCertificate --profile-path path\to\my_signing_profile.xml --alias MyCertificate --csr-only --pem-file my-cert.csr

Note that if you know you will be generating a certificate to be signed by a CA, you can generate a new certificate and export the CSR in one command:

gradlew :generateCertificate --profile-path path\to\my_signing_profile.xml --alias MyCertificate --csr-file my-cert.csr

Then you should take the my-cert.csr file and send it to your CA to be signed, and they will send you back the signed certificate, which you will import back into your keystore with the command

gradlew :importCertificate --profile-path path\to\my_signing_profile.xml --alias MyCertificate --pem-file my-cert.pem

You may be prompted that the certificate is not trusted. Enter “yes” to install the reply anyway.

Note that the signed certificate file’s extension may differ depending on your CA. If you signed the certificate yourself using Workbench, the extension will be .pem. Your CA may send you multiple certificate files; you must use a PEM or PKCS encoded file with the full certificate chain. If your CA does not provide a file with the full certificate chain, you will have to construct one by concatenating the individual PEM files.

Key Management

It is important to properly manage your code signing keys and protect the keys you use for release because if your keys are exposed, someone could impersonate you by signing code with your signing key, defeating the purpose of code signing.

We suggest having a separate signing certificate for signing release modules and storing that on a separate machine where all release builds will be done. Access to this machine and the signing certificate should be limited to a few trusted developers, and only modules that are intended to be released to customers or used in a production environment should be signed with this certificate.

During development and testing, individual developers can use the default generic self-signed certificate and install it in the user trust store of Niagara test installations to establish a chain of trust as described above. Another option for development certificates is to have all developer certificates signed by an internal development CA, then that CA certificate can be installed in the user trust store of Niagara test installations organization wide. This allows developers to share modules for testing without having to install every developers certificate in their test environment.

Permissions Requiring Signing

There are currently two Niagara permission groups Niagara permission groups that require signed modules due to their sensitivity. These are REFLECTION, and HSM_SIGNING. Any module requesting either of these permissions must be validly signed, and must be trusted by the Niagara installation in which it is installed as described in the Establishing Trust section above. Any module requesting either of these permissions that is not signed and trusted will cause the Station or Workbench to halt when the module is loaded.

Code Signing using Hardware Security Modules (HSMs)

As of mid-2023, all commercial certificate vendors require the use of Hardware Security Modules (HSMs) to protect the private key of any code signing certificate. Starting with Niagara 4.14, the development toolchain includes limited support for signing modules using physical HSM devices that support the PKCS11 standard. Any HSM that can be used with the standard jarsigner command provided by the JDK will also likely work, as HSM support was added by adding support for calling jarsigner with arbitrary arguments to perform the actual signing work.

Creating a JarSignerSigningProfile

To support signing with an HSM, a JarSignerSigningProfile is available. This profile is a bit different from the XML-based signing profiles outlined above: it is a Java properties file, and not XML; and it configures the command and arguments used to run jarsigner rather than any Gradle-specific configuration. A full example file is shown at the end of this section.

Before using the JarSignerSigningProfile, you will need to ensure that any drivers necessary to use your HSM are installed and working. Your certificate vendor should be able to provide you directions on how to do this; it will vary depending on the type of device offered.

This should work on both Linux and Windows – once the correct drivers have been installed. Any recent version of jarsigner should work, though best compatibility will be with a Java 8 JDK. Much existing documentation claims that you must use a old, 32-bit JDK, but Tridium testing has shown that modern 64-bit JDKs work perfectly fine as long as they include PKCS11 native support. Consult your JDK vendor for more information.

This file sets up the command to be run with two variables: jarsigner.cmd, which is the path to a jarsigner executable; and jarsigner.args, which are the arguments to it. jarsigner.args maps to a list of arguments; you must specify each argument on a separate line using the syntax jarsigner.args+=-my-argument; this appends a new argument to the list that will be passed to jarsigner when run. Do not set more than one argument per line: jarsigner.args+=-foo bar will not work.

When constructing arguments, a handful of variables are made available to you:

Additionally, to securely store passwords or other sensitive data, you may place them in a file in the same directory as this file, with a ‘.credentials’ extension. For example, if your profile is named my_signing_profile.properties, a credentials file in the same folder named my_signing_profile.credentials will be loaded. You may specify any property in this file and it will be made available for expansion, as long as it begins with jarsigner. The example assumes you use the property name jarsigner.storepass for the password for your HSM. Credentials must be escaped as per the escaping rules for Java properties files; in particular, " must be escaped as \".

Note that when specifying any paths on Windows, you must escape the file separator ’' as ‘\’. Additionally, on Windows, any arguments containing spaces must be wrapped in "": any paths, as well as the alias specification.

The example below contains TODOs for any places you may need to change to get this working for your HSM.

The alias specification in build.gradle.kts must also be correct. Your HSM should have a mechanism for listing the keys and their aliases; again, consult your certificate vendor’s documentation for further information.

niagara.signing.profileType=com.tridium.gradle.plugins.signing.profile.JarSignerSigningProfile

# TODO If needed, specify a credentials file here.
# jarsigner.credentialsFile=

# TODO If the jarsigner on your PATH does not support PKCS11 signing, set the full
# path to a working jarsigner here.
jarsigner.cmd=jarsigner

# TODO Update with the TSA recommended by your certificate vendor and uncomment
# these lines
# jarsigner.args+=-tsa
# jarsigner.args+=http://timestamp.digicert.com

# jarsigner.args+=-tsadigestalg
# jarsigner.args+=SHA256

jarsigner.args+=-keystore
jarsigner.args+=NONE

jarsigner.args+=-storetype
jarsigner.args+=PKCS11

jarsigner.args+=-storepass
# TODO Update this if your credentials file specifies a different variable
jarsigner.args+=${jarsigner.storepass}

jarsigner.args+=-providerClass
jarsigner.args+=sun.security.pkcs11.SunPKCS11

# TODO Newer jarsigner versions may require the use of 'addProvider' instead of
# 'providerClass'; this may be the case if you get odd errors like
# 'java.security.NoSuchAlgorithmException: SHA256 MessageDigest not available'
# jarsigner.args+=-addProvider
# jarsigner.args+=SunPKCS11

jarsigner.args+=-providerArg
# TODO Update with the absolute path to your HSM .cfg file
jarsigner.args+=${profile.path}${/}eToken.cfg

jarsigner.args+=-signedjar
jarsigner.args+=${destfile}
jarsigner.args+=${srcfile}

# TODO Ensure the alias in build.gradle.kts matches what's registered in your HSM
# Again, only on Windows: If your alias contains spaces, wrap this in "": "${alias}"
jarsigner.args+=${alias}

The example above can be copied to a my_signing_profile.properties file, and can be loaded in the same manner as any other profile in your build.gradle.kts:

niagaraSigning {
  aliases.set(listOf("MyCertificate"))
  signingProfileFile.set(project.layout.projectDirectory.file("path/to/my_signing_profile.properties"))
}

Signing with JarSignerSigningProfile

Once you have configured your profile and set it as the default in your build.gradle.kts, the standard gradlew jar command will sign modules using the HSM. If this fails, you can run gradlew jar --info for more details. This will also print the full jarsigner command that was invoked. Ensure that command runs outside of Gradle as a first troubleshooting step.