At the moment, one of the hot topics in the world of Development is Zero Trust. This is an easy topic to explain: never trust and always verify. Before doing something (anything), verify that the user / data / etc. is authentic and correct. For example, verify whether a user is allowed to see certain data. Another example is verifying if the data that you received from a third party API is actually of said third party API. One way of doing this is by using signatures.

In one of my latest projects I had to use the Cryptographic Message Syntax (CMS) standard in combination with a Java (Spring-Boot) RESTful application. All responses by the API had to be signed using a X.509 certificate using the CMS standard, so that the receiving party could undeniably establish that the message was from our department and thus valid information.

In this post I will show you how easy it is to sign the response and how you can validate the signature using Java + bouncycastle.

maven coordinates:

<dependency>
   <groupId>org.bouncycastle</groupId>
   <artifactId>bcpkix-jdk15on</artifactId>
   <version>${bouncycastle.version}</version>
</dependency>

Signing

For the API I build we had a simple interface definition:

{
   payload: <base64 encoded message>
   signature: <base64 encoded signature>
}

Every operation that the API implemented had this as its response. So when creating the signature, I first had to create a String (JSON) representation of the response, encode it using Base64 and then create the signature. When creating a CMS Signature you have to first create A CMSSignedDataGenerator, this is the object that will do the actual signing of your payload. The following code will create such an Object.

    private CMSSignedDataGenerator createCmsSignedDataGenerator() throws Exception {
        var certHolder = new X509CertificateHolder(Certificate.getInstance(cert.getEncoded()));
        var cmsSignedDataGenerator = new CMSSignedDataGenerator();
        cmsSignedDataGenerator.addCertificates(keyChain);
        ContentSigner sha1Signer = new JcaContentSignerBuilder(cert.getSigAlgName()).setProvider("BC").build(privateKey);
        cmsSignedDataGenerator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, certHolder));
        return cmsSignedDataGenerator;
    }

If you look closely, I add the full chain and not just the signing certificate. This means that the actual signature will not only contain the intended certificate but the complete chain. This allows for easy verification of the signature because the verifier will only need to verify the root certificate used in the chain.

The creation of the actual signature is relatively easy:

    public String sign(byte[] payload) throws Exception {
        var msg = new CMSProcessableByteArray(payload);
        var cmsSignedDataGenerator = getCmsSignedDataGenerator();
        var signedData = cmsSignedDataGenerator.generate(msg);
        return new String(Base64.getEncoder().encode(signedData.getEncoded("DER")));
    }

Now, the first thing I tried was to optimize the code by creating the DataGenerator in the constructor of my class to maximise reuse and runtime performance. However, after some testing I noticed that my signature became invalid up to the point that my code broke due to nullpointers! A quick search confirmed my suspicions, the CMSSignedDataGenerator is not thread safe and I had to create a new one for each request. This means that the only thing you can “cache” in your object are the certificates themselves (you don’t have to fetch them from disk for each and every call).

So when this code runs you will have a message that has a base64 encoded payload and a base64 encoded signature.

Verifying the signature

Now we know how to make a signature, we want to be able to verify that signature. For that we are first going to look at what is in the signature. In this post you can find a complete explanation of CMS and what is in the signature. For the short version, please continue reading:

A CMS signature normally contains the following items:

  • CMS version
  • Information about the digest algorithm
  • Content info
  • (Optional) the message itself
  • (Optional) Used certificates + certificate revocation lists
  • Signer info:
    • CMS Version
    • Signer ID
    • Digest algoritm
    • Signing time
    • signature algoritm
    • signature

In short, the CMS signature contains all that you require to validate if the message was signed by the provided signature. So a verification consists out of two steps:

  • Verify the content against the signature
  • Verify the used signature

Merely verifying the signature with the provided message and certificate is not sufficient. Someone could just include their own certificate and make a valid signature. So verifying the used certificates is also required when verifying the signature.

    public void verifySignature(EventResponse eventResponse) throws CMSException, CertificateException, OperatorCreationException {
        log.info("Starting verification payload CMS Signature");
        ASN1Primitive primitive = createPrimitive(eventResponse);
        var contentInfo = ContentInfo.getInstance(primitive);
        var cmsSignedData = new CMSSignedData(new CMSProcessableByteArray(Base64.getDecoder().decode(eventResponse.getPayload())), contentInfo);

        SignerInformationStore signers = cmsSignedData.getSignerInfos();
        for (SignerInformation signer : signers.getSigners()) {
            log.debug("Verifying {}", signer.getSID());
            X509CertificateHolder certHolder = ((Collection<X509CertificateHolder>) cmsSignedData.getCertificates().getMatches(signer.getSID())).iterator().next();
            verifyCertificateChain(certHolder, cmsSignedData.getCertificates());
            SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder().build(certHolder);

            if (signer.verify(verifier)) {
                log.info("CMS Signature verified with cert: {}", signer.getSID().getIssuer());
            } else {
                log.info("Unable to verify signature with cert: {}", signer.getSID().getIssuer());
                throw new ConfigurationException(String.format("Unable to verify signature with cert: %s", signer.getSID().getIssuer()));
            }
        }
    }

This code is responsible for verifying the signature (and it does a call to a method that verifies the certificates) but first the signature. First we need to create the ASN1Primitive, a representation of the signature as a Java Object. Next we create the CMSSignedData based using the Signature Object and the actual message.

Now that we have those two objects, we can verify the signature(s) – as a CMS can contain more than one signer. This is done by verifying the signature against the certificate. By doing this for each signer you can verify the complete signature.

The next step is verifying the certificate itself.

    private void verifyCertificateChain(X509CertificateHolder certificateHolder, Store<X509CertificateHolder> intermediateCertificates) {
        log.debug("Verifying certificate chain for: {}", certificateHolder.getSubject());
        try {
            var selector = new X509CertSelector();
            selector.setCertificate(createCertificate(certificateHolder));

            Set<TrustAnchor> trustAnchors = new HashSet<>();
            Iterator<String> aliasIterator = this.store.aliases().asIterator();
            log.debug("Adding trust anchors");
            while (aliasIterator.hasNext()) {
                trustAnchors.add(new TrustAnchor((X509Certificate) this.store.getCertificate(aliasIterator.next()), null));
            }
            var pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
        
            log.debug("Adding trust intermediates");
            var intermediateCertStore = CertStore.getInstance("Collection",
                    new CollectionCertStoreParameters(intermediateCertificates.getMatches(null)), "BC");
            pkixParams.addCertStore(intermediateCertStore);

            var builder = CertPathBuilder.getInstance("PKIX", "BC");
            log.debug("Verifying chain");
            builder.build(pkixParams);
            log.info("Verified certificate chain");

        } catch (Exception e) {
            throw new ConfigurationException("Unable to verify the certificate chain", e);
        }

    }

The above code verifies the certificates against known / approved certificates. First a trust anchor is created. This is done by retrieving all certificates from a keystore and adding them to a list of trusted certificates. These are combined in the PKIXBuilder params. Because it is possible that the certificate that has been used is part of a chain of certificates, we will add the complete certificate chain that we found in the signature. This means that a certificate is valid as long as one or more certificates in that chain are trusted.

The next step is to add the intermediate store to the params. Once we have all three, we can verify the actual chain, to be absolutely sure everything is in order!

Conclusion

CMS Signatures are a very powerful tool when it comes to adding an extra layer of security in your chain. It is independent of communication protocol and as long as the message is received by a party you can verify the source of the information and whether or not it has been changed. It will take some setting up, however once it is in place and it works you have added an extra layer in your Zero Trust framework.

Want to learn more about Zero Trust Coding? Be part of the codefest on September 21st!