RSASSA-PSS support in Java 11

Issue #129 closed
Travis Spencer created an issue

I saw this OpenJDK issue the other day about adding support for RSASSA-PSS in Java 11. I downloaded a copy and tried this:

public static void main(String[] args) throws JoseException
{
       System.out.printf("Java version = %s\n", System.getProperty("java.version"));

       String[] pssAlgs = new String[]
               {
                       AlgorithmIdentifiers.RSA_PSS_USING_SHA256,
                       AlgorithmIdentifiers.RSA_PSS_USING_SHA384,
                       AlgorithmIdentifiers.RSA_PSS_USING_SHA512
               };

       for (String alg : pssAlgs)
       {
           JsonWebSignature jws = new JsonWebSignature();
           jws.setAlgorithmHeaderValue(alg);
           String payload = "stuff here";
           jws.setPayload(payload);
           jws.setKey(ExampleRsaKeyFromJws.PRIVATE_KEY);

           String cs = jws.getCompactSerialization();

           System.out.println(cs);

           jws = new JsonWebSignature();
           jws.setAlgorithmConstraints(new AlgorithmConstraints(
               AlgorithmConstraints.ConstraintType.WHITELIST, alg));
           jws.setKey(ExampleRsaKeyFromJws.PUBLIC_KEY);
           jws.setCompactSerialization(cs);

           assert jws.verifySignature();
           assert Objects.equals(payload, jws.getPayload());
       }
}

The output wasn't promising 🙁 :

Java version = 11.0.1
Exception in thread "main" org.jose4j.lang.InvalidAlgorithmException: PS256 is an unknown, unsupported or unavailable alg algorithm (not one of [none, HS256, HS384, HS512, ES256, ES384, ES512, RS256, RS384, RS512]).
    at org.jose4j.jwa.AlgorithmFactory.getAlgorithm(AlgorithmFactory.java:51)
    at org.jose4j.jws.JsonWebSignature.getAlgorithm(JsonWebSignature.java:231)
    at org.jose4j.jws.JsonWebSignature.getAlgorithm(JsonWebSignature.java:207)
    at org.jose4j.jws.JsonWebSignature.sign(JsonWebSignature.java:157)
    at org.jose4j.jws.JsonWebSignature.getCompactSerialization(JsonWebSignature.java:132)

I found this strange considering the OpenJDK issue above. So, I wanted to see which signing algorithms were supported:

public static void main(String[] args)
   {
       System.out.printf("Java version = %s\n", System.getProperty("java.version"));

       Set<String> algorithms = Set.of(
               "RSASSA-PSS",
               "SHA1withRSAandMGF1",
               "SHA224withRSAandMGF1",
               "SHA256withRSAandMGF1",
               "SHA384withRSAandMGF1",
               "SHA512withRSAandMGF1",
               "SHA512/224withRSAandMGF1",
               "SHA512/256withRSAandMGF1"
       );

       Arrays.stream(Security.getProviders())
               .flatMap(p -> algorithms.stream()
                       .filter(a -> p.getService("Signature", a) != null)
                       .map(a -> Map.entry(p, a)))
               .forEach(e -> System.out.printf("The %s provider supports %s\n", 
                       e.getKey().getName(), e.getValue()));
   }

Output:

Java version = 11.0.1
The SunRsaSign provider supports RSASSA-PSS

How does RSASSA-PSS relate to the JWA-defined PS256 identifier? That spec says in appendix A that the JCA identifier is SHA256withRSAandMGF1. Maybe it isn't supported in Java 11 after all, I thought 😦

But what is SHA256withRSAandMGF1? It's RSASSA-PSS with SHA-256 as the hashing algorithm (used on the random salt) with MGF1 mask generation, right? If so, then RSASSA-PSS is there in Java 11. SHA-256 hashing is certainly there and MGF1 also seems to exist. Did the Java devs just not get the memo from the JCA spec authors about the identifier they were supposed to use? Is that identifier shorthand for creating a Signature object with the right parameters set?

I wondered without definitive answers, but started to piece together enough understanding to try to verify a JWT using RSASSA-PSS. To do this, I created a program that used BC to produce a signed JWT. Then, I verified it with the SunRsaSign provider:

package com.example;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jose4j.jca.ProviderContext;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jws.JsonWebSignatureAlgorithm;
import org.jose4j.keys.ExampleRsaKeyFromJws;
import org.jose4j.keys.KeyPersuasion;
import org.jose4j.lang.InvalidAlgorithmException;
import org.jose4j.lang.InvalidKeyException;
import org.jose4j.lang.JoseException;

import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.Security;
import java.security.Signature;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.util.Objects;
import java.util.Set;

import static org.jose4j.jwa.AlgorithmConstraints.ConstraintType.WHITELIST;

public class CheckIfJose4jCanSignWithRsaPssUsingShaStar
{
   public static void main(String[] args) throws JoseException
   {
       System.out.printf("Java version = %s\n", System.getProperty("java.version"));

       Security.addProvider(new BouncyCastleProvider());

       ProviderContext bouncyCastleProviderContext = new ProviderContext();

       bouncyCastleProviderContext.getGeneralProviderContext().setGeneralProvider("BC");

       for (String algorithm : DelegatedJsonWebSignatureAlgorithm.SUPPORTED_ALGORITHM_IDS)
       {
           JsonWebSignature jws = new JsonWebSignature();
           jws.setAlgorithmHeaderValue(algorithm);
           jws.setProviderContext(bouncyCastleProviderContext);
           String payload = "stuff here";
           jws.setPayload(payload);
           jws.setKey(ExampleRsaKeyFromJws.PRIVATE_KEY);

           String jwsCompactSerialization = jws.getCompactSerialization();

           System.out.println(jwsCompactSerialization);

           jws = new JsonWebSignature()
           {
               @Override
               public JsonWebSignatureAlgorithm getAlgorithm() throws InvalidAlgorithmException
               {
                   return new DelegatedJsonWebSignatureAlgorithm(super.getAlgorithm());
               }
           };
           jws.setAlgorithmConstraints(new AlgorithmConstraints(WHITELIST, algorithm));
           jws.setKey(ExampleRsaKeyFromJws.PUBLIC_KEY);
           jws.setCompactSerialization(jwsCompactSerialization);

           if (!jws.verifySignature())
           {
               throw new IllegalStateException("Could not verify signature");
           }

           if (!Objects.equals(payload, jws.getPayload()))
           {
               throw new IllegalStateException("Payloads are not equal");
           }
       }

       System.out.println("Checked all signatures successfully");
   }

   private static class DelegatedJsonWebSignatureAlgorithm implements JsonWebSignatureAlgorithm
   {
       static final Set<String> SUPPORTED_ALGORITHM_IDS = Set.of(
               AlgorithmIdentifiers.RSA_PSS_USING_SHA256,
               AlgorithmIdentifiers.RSA_PSS_USING_SHA384,
               AlgorithmIdentifiers.RSA_PSS_USING_SHA512
       );

       private final JsonWebSignatureAlgorithm delegate;

       DelegatedJsonWebSignatureAlgorithm(JsonWebSignatureAlgorithm delegate)
       {
           this.delegate = delegate;
       }

       @Override
       public boolean verifySignature(byte[] signatureBytes, Key key, byte[] securedInputBytes,
                                      ProviderContext providerContext) throws JoseException
       {
           String algorithmIdentifier = getAlgorithmIdentifier();

           if (SUPPORTED_ALGORITHM_IDS.contains(algorithmIdentifier))
           {
               if (!(key instanceof RSAPublicKey))
               {
                   throw new JoseException("Key is not an RSA public key.");
               }

               try
               {
                   Signature signature = Signature.getInstance("RSASSA-PSS");
                   RSAPublicKey rsaPublicKey = (RSAPublicKey) key;
                   String digestAlgorithm = algorithmIdentifier.replace("PS", "SHA-");
                   int saltLength = Integer.parseInt(algorithmIdentifier.substring(2)) / 8;
                   PSSParameterSpec parameter = new PSSParameterSpec(digestAlgorithm, "MGF1",
                           new MGF1ParameterSpec(digestAlgorithm),
                           saltLength, 1);

                   signature.setParameter(parameter);
                   signature.initVerify(rsaPublicKey);
                   signature.update(securedInputBytes);

                   return signature.verify(signatureBytes);
               }
               catch (GeneralSecurityException e)
               {
                   throw new JoseException("Could not verify signature", e);
               }
           }
           else
           {
               return delegate.verifySignature(signatureBytes, key, securedInputBytes, providerContext);
           }
       }

       @Override
       public byte[] sign(Key key, byte[] securedInputBytes, ProviderContext providerContext) 
           throws JoseException
       {
           return delegate.sign(key, securedInputBytes, providerContext);
       }

       @Override
       public void validateSigningKey(Key key) throws InvalidKeyException
       {
           delegate.validateSigningKey(key);
       }

       @Override
       public void validateVerificationKey(Key key) throws InvalidKeyException
       {
           delegate.validateVerificationKey(key);
       }

       @Override
       public String getJavaAlgorithm()
       {
           return delegate.getJavaAlgorithm();
       }

       @Override
       public String getAlgorithmIdentifier()
       {
           return delegate.getAlgorithmIdentifier();
       }

       @Override
       public KeyPersuasion getKeyPersuasion()
       {
           return delegate.getKeyPersuasion();
       }

       @Override
       public String getKeyType()
       {
           return delegate.getKeyType();
       }

       @Override
       public boolean isAvailable()
       {
           return delegate.isAvailable();
       }
   }
}

The code above is a bit hacky, but the following output shows it's possible to use Java 11 to verify signatures produced with RSASSA-PSS:

Java version = 11.0.1
eyJhbGciOiJQUzM4NCJ9.c3R1ZmYgaGVyZQ.ibWxuHl7Cdc8xWYwuYNzX7Lj05IBkfNx11nP3-Gdj2M_xj5IhrdhPvDziVMnQH9pHBCWZCPZ6z0F-y5vfAYFxYhs6ZJOuAY--4Jm0MMeNICV0e2VC4-8QIJNf3qde7aubBsh60WfiCIb9ba61DKrQ5BYqQ4F5po5jC6HjgHhxEHKRzMXMWy36AkaNUmNlTkY2COyPyoGv-tU_rjT4N00jmZnzedrEbKIWKDtfcg79I7fV5QATDc_SwTcJQt5xzDRfpTI6p4GoU3mCaAQEcXVCeP9RBmbaJuBM10kMIs-lcFWWAhTWnuBiY1tM3E6YW5PM8MZb1YzXghL5cmw49wFfg
eyJhbGciOiJQUzI1NiJ9.c3R1ZmYgaGVyZQ.SRl_GA7qeooKXYjP2DZEIWnOUQzUrylGBLUhDpviH-UvSdFXirALNEorscfFjuoUhY9pdkRIBt4iF6jtFUydtGzhXhChFo9GY7VNn26TW9X_Xja_iA8_9Cd2l5icEottkKieurBIXZ5xQA28yOUMo7UoC7kpQlFF4P7Z6cW5cKA4xubs3N5qxiYVrG15yNoCRZeHMd5fuNO-e0umyBz-2d2nkfWO1IgGyfk2fajp8yEbSeOqHrZpJWi4bLw3Hg9_bC1HC-npbqPeJpgRLkbhMUUH71uIScfrbsLj7P80r_9_gx4_OSBHb2rFnX8Z8TN3XgWi2D5H5Fvzp6EcVHoMWQ
eyJhbGciOiJQUzUxMiJ9.c3R1ZmYgaGVyZQ.ZBdqcpz3dOjf_eMUMKfu7yqicMk7RyRgpe5vpA7x5zdz2By2_dpkOWNx0AeDcTvWpXIGAQEWlZOmsPnXIBVLbTtR8S3BkQX4YIuVP9LPxkyqSNS-6d48PeXeZFE9Q2g-wyDPuEc4TEoe3Azl01G67De7aS6ztqwNmwYRdr2DTbFzw6ZqTQTMUbvWkhSKn0w0q9SkOv68CnNjFMIiy0AD6Gb5JzIAbhp0aRSwyJw_PWkAy3IWhF3u8hHu_rpotW5KJlQ6SRXv0TjkuAmnYZZHX5bOnsjbk9N41FWnI1lpwFkMjkdoLXKutjV50r2JBD7NWOmn0vI8p0xl8TpRIVt13w
Checked all signatures successfully

(I think it's possible to create signatures as well, but I didn't test that yet.)

The above code is hacky because it uses the delegated JsonWebSignatureAlgorithm. It does this because creating a Java Provider is a BIG deal. It seems that you have to sign the JAR with some key that Oracle has signed with the CA's key, etc. etc. So, adding a provider was a no go.

So, now, after all this, the issue:

Support RSASSA-PSS in Java 11 which seems to work

How though is the question. Should Jose4j wait till the Java devs get the JWA memo about the signing algorithm needing to be named SHA256withRSAandMGF1 and add it? I hope not; otherwise, we'll all have the hacky code above in our code bases. Instead, I think that Jose4j should try to add a convenience layer or helper API to make it easy to verify signatures in Java if RSASSA-PSS is found to be a supported signing algorithm.

Comments (8)

  1. Brian Campbell repo owner

    Well, isn't that fun? :/ I'd been naively thinking that things would just work when PSS support got added with Java 11. Thank you for showing me the error of my ways.

    I'll need to spend some time looking at this in more detail. But on the surface it looks like maybe just some divergence in the naming of things. Note that those entires in the JWA appendix came from this suggestion based on the JCA standard names and the bouncy castle names at the time: https://www.ietf.org/mail-archive/web/jose/current/msg03561.html

    I think that a somewhat less invasive workaround than a delegated JWS alg, that would work for signing and verification, would be to implement the 3 PSS algs and register them with the JWS AlgorithmFactory AlgorithmFactoryFactory.getInstance().getJwsAlgorithmFactory() and registerAlgorithm(...). I believe the PSS JWS alg implementations could be simple ones like those at https://bitbucket.org/b_c/jose4j/src/7f9624414a1baf752adbc61d4a1be16253eeec23/src/main/java/org/jose4j/jws/RsaUsingShaAlgorithm.java#lines-52 but just using the "RSASSA-PSS" name instead. This kinda suggests a way the library could support PSS with java 11 while still working with existing BC providers too.

  2. Brian Campbell repo owner

    Hrm... forking should be allowed. No matter though. Thanks for the link with your work. I'm gonna do an old fashioned style PR using copy/paste and tweak. Will get this rolled out in a new release soonish.

  3. Brian Campbell repo owner

    421e4d4 has the changes for this. I think it's all good but some testing or verification from you can't hurt before I cut a new release.

  4. Brian Campbell repo owner

    committed 421e4d4

    update to look for and use the RSASSA-PSS support that is new in Java 11 (Issue #129) Java 11 adds support for RSA PSS signatures (largely because of TLS 1.3) but used 'RSASSA-PSS' + PSSParameterSpec where as Bouncy Castle was using 'SHAXXXwithRSAandMGF1'

  5. Log in to comment