Generating Public Key from JWK
Asked Answered
N

2

7

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

Nestle answered 26/2, 2021 at 16:3 Comment(9)
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 Base64url 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.Kornher
@Kornher it explains in the question that parts is a String[] consisting of the header, payload and signature. Updated the question to include the libraries.Nestle
Have you tried decoding the signature, i.e. parts[2] with Base64url instead of Base64?Kornher
Yeah, it results in false from verify()Nestle
Can you post a complete example (JWK and JWT)?Kornher
I'll update the question to include the JWS stringNestle
For a repro the public key is still missing.Kornher
n and e are specified within the JWK so I should be able to produce a key from that, no?Nestle
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 jwt.io the verification fails (you can generate the required X.509 key from your key here 8gwifi.org/jwkconvertfunctions.jsp).Kornher
N
4

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.

Nestle answered 12/3, 2021 at 15:34 Comment(0)
K
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();
    }
}
Kenwee answered 26/2, 2021 at 17:3 Comment(3)
This doesn't answer my questionNestle
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 ??Kenwee
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 titleNestle

© 2022 - 2024 — McMap. All rights reserved.