5

Suppose I have the following JWK as the deserialised body of some JWS (RFC7515), where the modulus n is partially omitted for display purposes

{
   "kty": "RSA",
   "e": "AQAB",
   "kid": "KAgNpWbRyy9Mf2rikl498LThMrvkbZWHVSQOBC4VHU4",
   "n": "llWmHF8XA2KNLdmxOP3kxD9OY76p0Sr37j..."
}

And the JWS header specifies both the alg and kid fields required for signature verification.

How do I construct an RSA public key from this JWK so that I can verify the signature? Having looked at some related questions, I have the following Java implementation that attempts to build an RSA public key from the n and e fields within the JWK

public void someMethod(){
    String exjws ="eyJhbGciOiJSUzI1NiIsImtpZCI6IktBZ05wV2JSeXk5TWYycmlrbDQ5OExUaE1ydmtiWldIVlNRT0JDNFZIVTQiL"
        + "CJodG0iOiJwb3N0IiwiaHR1IjoiL3R4IiwidHMiOjE2MDM4MDA3ODN9.eyJjYXBhYmlsaXRpZXMiOltdLCJjbGllbnQiOnsia2V5Ijp7Imp3ayI6eyJrdHkiOiJSU0EiLCJ"
        + "hbGciOiJSUzI1NiIsImUiOiJBUUFCIiwia2lkIjoiS0FnTnBXYlJ5eTlNZjJyaW"
        + "tsNDk4TFRoTXJ2a2JaV0hWU1FPQkM0VkhVNCIsIm4iOiJsbFdtSEY4WEEyS05MZG14T1Aza3hEOU9ZNzZwMFNyMzdqZmh6OTRhOTN4bTJGTnFvU1BjUlpBUGQwbHFEUzhO"
        + "M1VpYTUzZEIyM1o1OU93WTRicE1fVmY4R0p2dnB0TFdueG8xUHlobVByIC0gZWNkU0NSUWRUY19aY01GNGhSVjQ4cXFsdnVEMG1xdGNEYklrU0JEdmNjSm1aSHdmVHBESG"
        + "luVDh0dHZjVlA4VmtBTUFxNGtWYXp4T3BNb0lSc295RXBfZUNlNXBTd3FIbzBkYUNXTktSI"
        + "C0gRXBLbTZOaU90ZWRGNE91bXQ4TkxLVFZqZllnRkhlQkRk"
        + "Q2JyckVUZDR2Qk13RHRBbmpQcjNDVkN3d3gyYkFRVDZTbHhGSjNmajJoaHlJcHE3cGM4clppYjVqTnlYS3dmQnVrVFZZWm96a3NodCAtIExvaHlBU2FLcFlUcDhMdE5aIC0gdyAifSw"
        + "icHJvb2YiOiJqd3MifSwibmFtZSI6Ik15IEZpcnN0IENsaWVu"
        + "dCIsInVyaSI6Imh0dHA6XC9cL2xvY2FsaG9zdFwvY2xpZW50XC9jbGllbnRJRCJ9LCJpbnRlcmFjdCI6eyJzdGFydCI6WyJyZWRpcmVjdCJ"
        + "dLCJmaW5pc2giOnsibWV0aG9kIjoicmVkaXJlY3QiLCJub25jZSI6ImQ5MDIxMzg4NGI4NDA5MjA1MzhiNWM1MSIsInVyaSI6Imh0dHA6XC9cL2xvY2FsaG9zdFwvY2xpZW50"
        + "XC9yZXF1ZXN0LWRvbmUifX0sImFjY2Vzc190b2tlbiI6eyJhY2Nlc3MiOlt7ImFjdGlvbnMiOlsicmVhZCIsInByaW50Il0sImxvY2F0aW9ucyI6WyJodHRwOlwvXC9sb2Nhb"
        + "Ghvc3RcL3Bob3RvcyJdLCJkYXRhdHlwZXMiOlsibWV0YWRhdGEiLCJpbWFnZXMiXSwidHlwZSI6InBob3RvLWFwaSJ9XX0sInN1YmplY3QiOnsic3ViX2lkcyI6WyJpc3Nfc3"
        + "ViIiwiZW1haWwiXX19.LUyZ8_fERmxbYARq8kBYMwzcd8GnCAKAlo2ZSYLRRNAYWPrp2XGLJOvg97WK1idf_LB08OJmLVsCXxCvn9mgaAkYNL_ZjHcusBvY1mNo0E1sdTEr31"
        + "CVKfC-6WrZCscb8YqE4Ayhh0Te8kzSng3OkLdy7xN4xeKuHzpF7yGsM52JZ0cBcTo6WrYEfGdr08AWQJ59ht72n3jTsmYNy9A6I4Wrvfgj3TNxmwYojpBAi"
        + "cfjnzA1UVcNm9F_xiSz1_y2tdH7j5rVqBMQife-k9Ewk95vr3lurthenliYSNiUinVfoW1ybnaIBcTtP1_YCxg_h1y-B5uZEvYNGCuoCqa6IQ";

    String[] parts = exjws.split("\\.");
    String payload = new Base64URL(parts[1]).decodeToString();
    JsonObject jwk  = JsonParser.parseString(payload).getAsJsonObject().get("client")
            .getAsJsonObject().get("key").getAsJsonObject().get("jwk").getAsJsonObject();

    BigInteger modulus = new BigInteger(1, new Base64URL(jwk.get("n").getAsString()).decode());  
    BigInteger exponent = new BigInteger(1, new Base64URL(jwk.get("e").getAsString()).decode());
    byte[] signingInfo = String.join(".",parts[0],parts[1]).getBytes(StandardCharsets.UTF_8);
    byte[] b64DecodedSig = new Base64(parts[2]).decode();
    
    PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent));
    
    Signature verifier = Signature.getInstance("SHA256withRSA");    
    verifier.initVerify(pub);
    verifier.update(signingInfo);
    boolean okay = verifier.verify(b64DecodedSig);
    System.out.println(okay);
}

The result of verify() is currently returning false.

I have tried generating RSA key pairs, signing and verifying using the generate keys and that worked. I suspect that my issue is that the constructed RSA key in the code above is wrong somehow. Any help appreciated.

EDIT

JSON library is Gson

JWK library is nismus-jose-jwt which provides Base64 and Base64URl

Community
  • 1
  • 1
Scott
  • 1,863
  • 2
  • 24
  • 43
  • 1
    The code is difficult to check because the references to `Base64`, `Base64URL` and your JWK/JSON library are missing and also `parts` is not explained. Provided the Base64url decoding of `n` and `e` are correct, the proper key will be generated. Presumably `parts[0]`, `parts[1]` and `parts[2]` contain the header, payload and signature, each Base64url encoded. The signature would need to be Base64**url** decoded for the `verify()` call, but seems to only be Base64 decoded, so possibly that's the problem (though I would expect an exception here). Please add the missing information. – Topaco Feb 26 '21 at 18:14
  • @Topaco it explains in the question that `parts` is a `String[]` consisting of the header, payload and signature. Updated the question to include the libraries. – Scott Feb 26 '21 at 19:42
  • Have you tried decoding the signature, i.e. `parts[2]` with Base64url instead of Base64? – Topaco Feb 26 '21 at 19:46
  • Yeah, it results in `false` from `verify()` – Scott Feb 26 '21 at 19:47
  • Can you post a complete example (JWK and JWT)? – Topaco Feb 26 '21 at 19:48
  • I'll update the question to include the JWS string – Scott Feb 26 '21 at 19:58
  • For a repro the public key is still missing. – Topaco Feb 26 '21 at 20:15
  • `n` and `e` are specified within the `JWK` so I should be able to produce a key from that, no? – Scott Feb 26 '21 at 20:20
  • 1
    Modulus and exponent determined in the code correspond to those in the token and the PublicKey is also correctly derived from them. It just seems that the public key in the JWT is not the one needed for verification. Also on https://jwt.io/ the verification fails (you can generate the required X.509 key from your key here https://8gwifi.org/jwkconvertfunctions.jsp). – Topaco Feb 26 '21 at 20:54

2 Answers2

3

From the discussion with @Topaco in the comments:

The code in the question does successfully construct the RSA public key from the modulus n and the exponent e.

However, the Signature.verify() invocation returns false because the public key specified within the JWK in the request does not match the key used to sign the request.

Scott
  • 1,863
  • 2
  • 24
  • 43
1

I'm using a JWT library for this task (I know it's a bit of overkill, but it's working...): https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/

They provide an easy-to-use interface and below you find a sample code that generates an RSA key pair and print out the public key, then convert this public key to JWK format (print out as well) followed by the "final" conversion from JWK format to Java's RSAPublicKey format - the original public key is identical to the "double converted" new public key:

RSA converting between Java keys and JWK keys
rsaPublicKey:
Sun RSA public key, 2048 bits
  params: null
  modulus: 26357316308141920246706187189832816184464669247574329274151600014987833498613081305553907167489639882497901020042668019684731733203493602029515963993706600847721534104752032126130361576446376774646308346723928903197131878300000630951097323650413651136361398382777541880437222482333326912353931641531474275115618239345544686220478026629436520040030688170796270228708165193211856330191604982765859609032534442818720461696078063893165568447273933782242398761845509532495844704423556107073518195030616464416564865911759432179943444938978123330642161124144169230685337930276039065398676755273689018037129036026967769360801
  public exponent: 65537

jwkRsaPublicKey (JWK-Format)
{"kty":"RSA","e":"AQAB","n":"0MpIJE0koFXx5sZOOI-XsEMMQfvwHkizj1jaYGATZEz0YTdf-WUDrO2JeELP1UvwHRbD5Mt0y0IvSYEjG4btVoZWjoJwEIz-bT7rtJNnZ9bjY8vMYloCUM81nTLve0sVRqkjw3S7IFXsTXx05vkY7oV25Z9YeZH2f5b1ph3JGcTrQF8d3XZy6XAM_KaWWOPTwzoNtr3JQQzUJ2vS_BGCJyiVU1cEB0RlRu1Gd9EPqDcMGAN2nMoHUuQw0qNTd-ms0Du0RGnktRDpcm3SXLsUt2J4adbPp02eXjn-TDTISzR6FywC0sAL6ED0EqWhOgqEf7EftctSJGGdgLOkmL4poQ"}

rsaPublicKeyFromJwk:
Sun RSA public key, 2048 bits
  params: null
  modulus: 26357316308141920246706187189832816184464669247574329274151600014987833498613081305553907167489639882497901020042668019684731733203493602029515963993706600847721534104752032126130361576446376774646308346723928903197131878300000630951097323650413651136361398382777541880437222482333326912353931641531474275115618239345544686220478026629436520040030688170796270228708165193211856330191604982765859609032534442818720461696078063893165568447273933782242398761845509532495844704423556107073518195030616464416564865911759432179943444938978123330642161124144169230685337930276039065398676755273689018037129036026967769360801
  public exponent: 65537

This code has no exception handling and is for educational purpose only:

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.RSAKey;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.interfaces.RSAPublicKey;

public class ConvertRsaKeysJavaJwk {
    public static void main(String[] args) throws NoSuchAlgorithmException, JOSEException {
        System.out.println("RSA converting between Java keys and JWK keys");
        // generate a RSA key pair
        KeyPair rsaKeyPair = generateRsaKeyPair(2048);
        RSAPublicKey rsaPublicKey = (RSAPublicKey) rsaKeyPair.getPublic();
        System.out.println("rsaPublicKey:\n" + rsaPublicKey);
        // import the ecdsaPublicKey to JWK
        // usage of nimbus-jose-jwt
        // https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/
        RSAKey jwkRsaPublicKey = new RSAKey.Builder(rsaPublicKey).build();
        System.out.println("\njwkRsaPublicKey (JWK-Format)\n" + jwkRsaPublicKey);
        // convert jwk to java
        RSAPublicKey rsaPublicKeyFromJwk = jwkRsaPublicKey.toRSAPublicKey();
        System.out.println("\nrsaPublicKeyFromJwk:\n" + rsaPublicKeyFromJwk);
    }

    public static KeyPair generateRsaKeyPair(int keylengthInt) throws NoSuchAlgorithmException {
        KeyPairGenerator keypairGenerator = KeyPairGenerator.getInstance("RSA");
        keypairGenerator.initialize(keylengthInt, new SecureRandom());
        return keypairGenerator.generateKeyPair();
    }
}
Michael Fehr
  • 5,827
  • 2
  • 19
  • 40
  • This doesn't answer my question – Scott Feb 27 '21 at 22:36
  • I'm sorry that I missunderstood your question "*How do I construct an RSA public key from this JWK so that I can verify the signature?*" The answer has to generate a new JWK as your example one is shortened, takes the JWK and converts it to a RSA public key ?? – Michael Fehr Feb 28 '21 at 07:58
  • You'll see from the question and the comments that I wasn't exclusively asking about the programmatic construction of RSA keys from JWKs, but specifically why `Signature.verify()` returned false for my specific scenario. Your answer is derived purely from the question title – Scott Mar 12 '21 at 15:39