Signing Java applets like it’s 2024

Introduction

I was recently tasked with supporting a long-time client, who is still using a particularly old piece of software: a browser-based extension to a product developed at ConSol over 20 years ago. What I’m talking about is, of course, a Java applet.

Hello! I am an applet!

The client isn’t yet able to modernize their setup, which means a) the applet is still in use, and b) needs to be signed again from time to time so as to not trigger any scary warning pop-ups upon execution – like this here specimen:

Code signing gone bad

So with a certificate expiry on the horizon, we set out to obtain a new certificate and use it to sign the applet for the customer. Sounds easy enough, right?

Enter Ballot CSC-13

Well, not really. The usual process – as it had been run through several times before – would have been to just renew the existing certificate with the current provider. This time around however, we found out that the rules had been changed by the CA/Browser Forum (Certification Authority Browser Forum), which is a standards body comprised of several working groups who address the concerns of certificate issuers and consumers. Specifically, the Code Signing Certificate Working Group, has created a ballot to update the „subscribers key protection requirements“ which defines a set of rules how you can store and handle your code signing certificates.

Here’s what they’ve added (as noted by Entrust):

The code signing certificate key pair must be generated and stored in a hardware crypto module that meets or exceeds the requirements of FIPS 140-2 level 2 or Common Criteria EAL 4+. This means the key pair will be generated in a device, where the private key cannot be exported. This will help to minimize the private key compromise. There is flexibility where the code signing certificate subscriber may use a hardware crypto module which is operated by:

  • The subscriber, such as a secure token or a server hardware security module (HSM)
  • A cloud service, such as AWS or Azure
  • A signing service which can be provided by the certification authority (CA) or another trusted service provider

And there you have it: Gone are the days of simply downloading a code signing certificate from your trusted vendor, replacing the old one in your build process and be done with it. This ballot (CSC-13) was passed in April of 2022 but the effective date was pushed back to the 1st of June, 2023 via another ballot (CSC-17) – quite some time ago.

On to safe new havens

To ensure the client would remain able to run the Java applet unhampered, we had to answer three questions:

  • Which of the aforementioned secure storage solutions should be chosen?
  • Which CA / security provider for the certificate should we pick?
  • How can we integrate this newfangled stuff into our legacy build?

All three issues are interlocked, but from a pragmatic point of view, the most important is the last one. In order to get an answer to this question, let’s take a step back and look at the build process.

Build integration

The legacy build is based on ant and uses the <signjar> task to do the actual signing. That looks something like this:

<signjar 
    jar="./dist/applet.jar" 
    keystore="./assets/applet-keystore" 
    alias="applet-cert" 
    storepass="..."
/>
(...)
</signjar>

Ant’s signjar task is really just a thin wrapper around the jarsigner utility, which is part of every Java distribution. Now, in simpler times, you would just order a new certificate for code signing, import that into your Java keystore…

keytool -import -trustcacerts -noprompt -alias applet-cert \
    -keystore ./assets/applet-keystore -storepass ... \
    -file ./codesigning-certificate.crt

…and then proceed to sign your .jar with it:

jarsigner -keystore ./assets/applet-keystore -storepass ... \
    -signedjar ./dist/applet-signed.jar ./dist/applet.jar applet-cert 

This jarsigner invocation translates 1:1 to the signjar task invocation in the ant build. Alas, as we now know, this is not possible anymore. Since the certificate now needs to be stored securely as per ballot CSC-13, this means that the jarsigner tool now needs to talk to an external keystore. The way to do that is to have a custom JCA provider, which you can configure in the JDK and then use to perform a more „contemporary“ signing of code. So whatever other choices we make, as long as we get a robust JCA provider, our chances of succeeding are very good.

Secure Storage

Ballot CSC-13 demands key storage to be a hardware crypto module that satisfies the requirements FIPS 140-2 level 2 or Common Criteria EAL 4+. Such crypto modules are hardened, tamper-resistant hardware devices and colloquially referred to as HSM (Hardware security module).  While you may encounter other names like „secure token“, „security keystore“ or similar, the term „HSM“ is usually the one to look out for: Only if a device is an HSM, or if a service is backed by an HSM, you’ll be able to use them for this purpose. Let’s have a look at our options here:

  • Acquire our own HSM
    • A physical token is only available in one location – to share it you need to send it around.
    • The token also needs to be sent to us, so it will take additional time for the initial setup.
    • Once we have it, we’d need to store it in a secure way, i.e. in a safe or access-restricted area.
    • There are many vendors (and models) to choose from – which means we have less trust in the level of robustness and support when it comes to each of their JCA provider implementations.
  • Use an HSM-backed service by a big cloud provider
    • Since cloud providers are everywhere, it seems like a logical choice to explore such options first.
    • With a cloud service, you’re able to use the certificate independent from your current location.
    • Because nothing needs to be sent around, we should be able to get started with code signing quickly.
    • No concerns about securely storing a physical object.
    • We immediately found quite compelling JCA providers for both Amazon AWS and Microsoft Azure.
  • Use an HSM-backed service by a CA /  security provider
    • This option wasn’t even considered anymore, as it seemed like the big cloud operators could probably offer a more fleshed out and very likely cheaper solution than providers like Thawte or Digicert.

In the end we decided to use the Azure „KeyVault“ for the secure storage, which was mainly because their documentation seemed to be superior to the ones for Amazon’s „KMS“ (Key Management Service) and code examples for the JCA provider were also really extensive. The price tag associated is about 5€ per month, which is fair game.

CA / Security Provider

There are quite a few of these providers and there surely exist many articles devoted to comparing them and highlighting the differences between their offerings. In our case, IT already had good experiences with cheapsslsecurity.com and thus encouraged us to review their code signing offerings, to see if they fit. This company is reselling products of big name CAs like Comodo, Digicert or Thawte so there are quite a few options to choose from. What was crucial for us here was that the offer guarantees compatibility with cloud providers. Consequently, we did settle for the FastSSL / Digicert option which did mention cloud compatibility with Azure explicitly and came at a quite reasonable price of just under 130€ per year.

Slowly getting there

With these questions answered, we could actually start doing things.

Setting up KeyVault

Before we could order the certificate, we had to set up the secure storage. Since we already had a Microsoft Azure account for the company, we could basically just follow this tutorial from Microsoft. You only need to make sure you select the „Premium“ pricing tier, as that is needed for the HSM-backed storage type which we require for code signing.

Creating the CSR

The next step is to create a certificate signing request (CSR) in KeyVault. For this, we used the menu on the left to navigate to „Options“ and then „Certificates“. After selecting the „Generate/Import“ tab on the right, we were presented with the „Create a certificate“ form. There’s two things of importance here: The „Type of Certificate Authority (CA)“ needs to be set to „Certificate issued by non-integrated CA“ since an external service is used. Second, is the addition of a „Extended Key Usage“ (EKU), which designates the certificate we’re requesting to be one for code signing:

  • Click on „Advanced Policy Configuration“
  • In the EKU field, add this EKU to the list: 1.3.6.1.5.5.7.3.3
  • Exportable Private Key: No (required by CSC-13)
  • Key Type: RSA-HSM or EC-HSM (required by CSC-13)
  • Key Size: 3072 (minimum, recommended by three letter agencies for years now)

After clicking „create“ we found the certificate in the list with the status set to „disabled“ (it hasn’t been signed yet). On the „Certificate Operation“ tab, we used the „Download CSR“ button to obtain the certificate signing request.

Ordering the certificate

Armed with the CSR we could now order the actual certificate from our chosen security provider. We started by navigating to the FastSSL / Digicert code signing certificate we had decided upon earlier, where we selected:

  • Certificate type: Standard validation (Extended validation is needed e.g. to sign drivers for Windows)
  • Certificate delivery method: Install on Existing HSM (This is the option needed to use Azure KeyVault)

The ordering process then took us through a few forms where we enter details about the company and defined a contact person. The phone number for that contact person is of importance, as the CA will call you for verification. At some point we also provided the CSR that we’ve created in the previous step.

In our case it took around 3 hours until the telephone rang and the CA got back to us. After a validity check and alignments of the certificate parameters (in our case the ampersand „&“ in the company name had to be changed to an „and“ for technical reasons), all was good to go and soon after that we received an email with instructions how to transfer the certificate. To import the signed certificate in KeyVault, we selected the „disabled“ certificate entry from the list (on the same page as before) and used the „Merge Signed Request“ button.

Setting up an Entra ID Application

In order to make use of the certificate, we needed to setup an application in Microsoft’s Entra ID, which is the cloud-based identity and access management (IAM) solution for Azure. They provide a tutorial on how to do this. Once finished, we wrote down the Tenant ID (aka Directory ID) and the Application ID (aka Client ID).

Then we created a new secret for the application by clicking „Certificates & secrets“ in the menu to the left, selecting the „Client secrets“ tab on the right and clicking „New client secret“. We made sure to immediately memorize the displayed secret value, as it can’t be displayed again.

Finally, the application needed to be granted access to our KeyVault, which is done by adding a role assignment in the „Access control (IAM)“ section. The role that needs to be assigned to the Entra ID application is „Key Vault Crypto User“.

Installing the JCA provider

The final preparation is to install the JCA provider. For that we headed to the jre/lib/security in our Java installation and opened the file java.security, searching for a block that defines the providers. It looks like this:

security.provider.1=sun.security.provider.Sun
security.provider.2=sun.security.rsa.SunRsaSign
security.provider.3=sun.security.ec.SunEC
(...)

At the end of that block of key-value pairs, we added a new entry, increasing the counter in the key on the left side. The value needs to be the package name the Azure KeyVault JCA provider, which is com.azure.security.keyvault.jca.KeyVaultJcaProvider.

(...)
security.provider.9=sun.security.smartcardio.SunPCSC
security.provider.10=sun.security.mscapi.SunMSCAPI
# next line is added by us
security.provider.11=com.azure.security.keyvault.jca.KeyVaultJcaProvider

As a last step we downloaded the JCA provider library via MVNRepository and dropped the .jar into the jre/lib/ext subfolder of our Java installation.

I can’t believe it’s signing code!

Now we were finally ready to sign some code – we had all the necessary pieces of information:

  • The KeyVault URL
  • The certificate’s name
  • The Directory ID (also known as Tenant ID)
  • The Application ID (also known as Client ID)
  • The Client Secret value

Additionally, we needed a TSA (timestamp authority), which is used to record the date and time when a signature took place. As we’ve effectively bought a DigiCert certificate, we went with theirs (http://timestamp.digicert.com).

For testing purposes, we set up a small script – Below UUIDs and secret are randomly generated ones, of course 😉

#!/bin/bash

KEYVAULT_URL=https://example.vault.azure.net
CERT_NAME=applet-cert
DIRECTORY_ID=66c98f3e-6cba-4775-bbe6-4cfc258062a3
APPLICATION_ID=b3b4419d-acf9-4781-abea-b42c7c62c283
CLIENT_SECRET=M2H8Q~~YqRS8EcuXBk196sd71taEwvVdO2-ewrXY

jarsigner -keystore NONE -storetype AzureKeyVault -storepass "" -verbose \
    -tsa http://timestamp.digicert.com -providerName AzureKeyVault \
    -providerClass com.azure.security.keyvault.jca.KeyVaultJcaProvider \
    -J-Dazure.keyvault.uri=$KEYVAULT_URL \
    -J-Dazure.keyvault.tenant-id=$DIRECTORY_ID \
    -J-Dazure.keyvault.client-id=$APPLICATION_ID \
    -J-Dazure.keyvault.client-secret=$CLIENT_SECRET \
    -signedjar ./dist/applet-signed.jar ./dist/applet.jar applet-cert

Executing that script gave us the following:

Nov 05, 2024 12:18:19 PM com.azure.security.keyvault.jca.implementation.KeyVaultClient <init>
INFO: Using Azure Key Vault: ...
Nov 05, 2024 12:18:19 PM com.azure.security.keyvault.jca.implementation.utils.AccessTokenUtil getLoginUri
INFO: Getting login URI using: ...

(...)

>>> Signer
    X.509, CN=ConSol Consulting and Solutions Software GmbH, O=ConSol Consulting and Solutions Software GmbH, L=München, ST=Bayern, C=DE
    [
    Signature algorithm: SHA256withRSA, 3072-bit key
    [trusted certificate]
>>> TSA
    X.509, CN=DigiCert Timestamp 2024, O=DigiCert, C=US

(...)

jar signed.

The timestamp will expire on 2031-11-10.

This looks pretty good! Let’s see if it holds up if we verify it:

$ jarsigner -verify -certs ./dist/applet-signed.jar

(...)

- Signed by "CN=ConSol Consulting and Solutions Software GmbH, O=ConSol Consulting and Solutions Software GmbH, L=München, ST=Bayern, C=DE"
    Digest algorithm: SHA-256
    Signature algorithm: SHA256withRSA, 3072-bit key
  Timestamped by "CN=DigiCert Timestamp 2024, O=DigiCert, C=US" on Tue Nov 05 12:18:21 UTC 2024
    Timestamp digest algorithm: SHA-256
    Timestamp signature algorithm: SHA256withRSA, 4096-bit key

jar verified.

Warning:
This jar contains entries whose certificate chain is invalid. Reason: PKIX path building failed: 
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

The signer certificate will expire on 2027-11-03.
The timestamp will expire on 2031-11-10.

Whoops. So from Java’s point of view, the certificate chain up to the root is not complete or in other words: at least one intermediate along the chain is missing. In order to get said intermediaries, we headed back to our CA’s download page and looked for the download option for the certificate chain. Once we had that, we could add it to jarsigner invocation with the -certchain argument:

jarsigner -keystore NONE -storetype AzureKeyVault -storepass "" -verbose \
    -tsa http://timestamp.digicert.com -providerName AzureKeyVault \
    -providerClass com.azure.security.keyvault.jca.KeyVaultJcaProvider \
    -J-Dazure.keyvault.uri=$KEYVAULT_URL \
    -J-Dazure.keyvault.tenant-id=$DIRECTORY_ID \
    -J-Dazure.keyvault.client-id=$APPLICATION_ID \
    -J-Dazure.keyvault.client-secret=$CLIENT_SECRET \
    -certchain ./assets/fullchain.pem \
    -signedjar ./dist/applet-signed.jar ./dist/applet.jar applet-cert

Now we could pass the verification without warnings:

$ jarsigner -verify -verbose -certs ./dist/applet-signed.jar

(...)
- Signed by "CN=ConSol Consulting and Solutions Software GmbH, O=ConSol Consulting and Solutions Software GmbH, L=München, ST=Bayern, C=DE"
    Digest algorithm: SHA-256
    Signature algorithm: SHA256withRSA, 3072-bit key
  Timestamped by "CN=DigiCert Timestamp 2024, O=DigiCert, C=US" on Tue Nov 05 14:39:35 UTC 2024
    Timestamp digest algorithm: SHA-256
    Timestamp signature algorithm: SHA256withRSA, 4096-bit key

jar verified.

The signer certificate will expire on 2027-11-03.
The timestamp will expire on 2031-11-10.

Finishing touches

If you’ve made it this far, you may wonder: Aren’t they still missing the final step, which is integrating the code signing into the legacy build? And you’re right – So let’s have look at how this works in ant with the <signjar> task:

<property name="azure.keyvault-uri" value="https://example.vault.azure.net" />
<property name="azure.directory-id" value="66c98f3e-6cba-4775-bbe6-4cfc258062a3" />
<property name="azure.application-id" value="b3b4419d-acf9-4781-abea-b42c7c62c283" />
<property name="azure.client-secret" value="M2H8Q~~YqRS8EcuXBk196sd71taEwvVdO2-ewrXY" />

<signjar jar="./dist/applet.jar"
        tsaurl="http://timestamp.digicert.com"
        alias="applet-cert"
        keystore="NONE"
        storetype="AzureKeyVault"
        storepass="...">
    <sysproperty key="azure.keyvault.uri" value="${azure.keyvault-uri}"/>
    <sysproperty key="azure.keyvault.tenant-id" value="${azure.directory-id}"/>
    <sysproperty key="azure.keyvault.client-id" value="${azure.application-id}"/>
    <sysproperty key="azure.keyvault.client-secret" value="${azure.client-secret}"/>
</signjar>

Now while this works in principle, unfortunately it does not allow us to add the certificate chain. This is because the signjar task does not implement support for the -certchain argument of the jarsigner tool. Consequently, when we use this method for code signing and run the applet at the client, it results in almost exactly the warning window seen at the very beginning the article. To get around this, we leveraged the good old <exec> task to call jarsigner manually:

<exec executable="${env.JAVA_HOME}/bin/jarsigner">
   <arg value="-keystore" />
   <arg value="NONE" />
   <arg value="-storetype" />
   <arg value="AzureKeyVault" />
   <arg value="-storepass" />
   <arg value="''" />
   <arg value="-verbose" />
   <arg value="-tsa" />
   <arg value="http://timestamp.digicert.com" />
   <arg value="-certchain" />
   <arg value="fullchain.pem" />
   <arg value="-providerName" />
   <arg value="AzureKeyVault" />
   <arg value="-providerClass" />
   <arg value="com.azure.security.keyvault.jca.KeyVaultJcaProvider" />
   <arg value="-J-Dazure.keyvault.uri=${azure.keyvault-uri}" />
   <arg value="-J-Dazure.keyvault.tenant-id=${azure.directory-id}" />
   <arg value="-J-Dazure.keyvault.client-id=${azure.application-id}" />
   <arg value="-J-Dazure.keyvault.client-secret=${azure.client-secret}" />
   <arg value="-signedjar" />
   <arg value="./dist/applet-signed.jar" />
   <arg value="./dist/applet.jar" />
   <arg value="applet-cert" />
</exec>

And with that we had successfully integrated code signing as per ballot CSC-13 into the legacy build!

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Nach oben scrollen
WordPress Cookie Hinweis von Real Cookie Banner