Revise setting of key use in JWK.load(KeyStore,String,char[]) when the cert key use cannot be mapped 1:1

Issue #461 open
Rafał Gała created an issue

When a key has Digital Signature and Key Encipherment usages, the from method of KeyUse class detects KeyUse.ENCRYPTION. This causes issues when JWKMatcher is build to detect KeyUse.SIGNATURE or null, even though the key has Digital Signature usage and can be used to create signatures.

Shouldn’t KeyUse.SIGNATURE be always returned when a key has signature usage, no matter what other usages are?

Comments (17)

  1. Vladimir Dzhuvinov
    • changed status to open

    Well, in the context of signature handling the KeyUse.SIGNATURE should be the one having priority. We'll need to see how such context aware KeyUse detection may work. If you have ideas feel free to chime in :)

  2. Rafał Gała reporter

    Then maybe this part of KeyUse class should be changed to KeyUse.SIGNATURE? 🙂

            // digitalSignature && keyEncipherment
            // (e.g. RSA TLS certificate for authenticated encryption)
            if (cert.getKeyUsage()[0] && cert.getKeyUsage()[2]) {
                return KeyUse.ENCRYPTION;
            }
    

  3. Vladimir Dzhuvinov

    Would you paste the code where the JWKMatcher is constructed?

    Where does the cert come from?

  4. Rafał Gała reporter

    This is the code, it comes from Spring Security’s NimbusJwtEncoder class.

    return new JWKMatcher.Builder()
                .keyType(KeyType.forAlgorithm(jwsAlgorithm))
                .keyID(headers.getKeyId())
                .keyUses(KeyUse.SIGNATURE, null)
                .algorithms(jwsAlgorithm, null)
                .x509CertSHA256Thumbprint(Base64URL.from(headers.getX509SHA256Thumbprint()))
                .build();
      }
    

    The certificate is issued by my corporate CA.

  5. Rafał Gała reporter

    It is called from method parse(final X509Certificate cert) in com.nimbusds.jose.jwk.RSAKey class:

                return new RSAKey.Builder(publicKey)
                    .keyUse(KeyUse.from(cert))
                    .keyID(cert.getSerialNumber().toString(10))
                    .x509CertChain(Collections.singletonList(Base64.encode(cert.getEncoded())))
                    .x509CertSHA256Thumbprint(Base64URL.encode(sha256.digest(cert.getEncoded())))
                    .build();
    

  6. Rafał Gała reporter

    This is in nimbus-jose-jwt library. It all starts jn JWK.load method called like this:

    JWK jwk = JWK.load(temp, kid, password);
    

    where temp is a keystore containing the certificate and private key, kid is the certificate alias and password is the keystore password.

    If you would like to reproduce this, I can share the keystore with you.

  7. Vladimir Dzhuvinov

    The underlying issue is that JWK “use” can either be signature or encryption, so changing the heuristics to return signature for cert.getKeyUsage()[0] && cert.getKeyUsage()[2] (which implies authenticated encryption) will solve the particular issue you have, but then will break other things. To provide a better mapping of the cert key usage fields the JWK has another parameter called “key_ops”, which enables a JWK to have multiple uses:

    https://datatracker.ietf.org/doc/html/rfc7517#section-4.3

    Do you have a possibility to change the JWK use in your code after it gets loaded from the key store but before the JWKMatcher is built?

  8. Vladimir Dzhuvinov

    It also occurs to be now that perhaps JWK.load shouldn’t set the KeyUse when it’s ambiguous (which can come up with certificates), so only set key_ops.

    Does the JWK.load happen in you app’s code?

  9. Rafał Gała reporter

    I am constructing the JWK with

    JWK jwk = JWK.load(temp, kid, password); 
    

    and then pass it to Spring Security, so I think I can do this, but will need an example.

  10. Vladimir Dzhuvinov
    jwk = new RSAKey.Builder((RSAKay)jwk)
    .keyUse(KeyUse.SIGNATURE)
    .build();
    

    If the loaded JWK is of type RSA. For EC the logic is similar.

  11. Rafał Gała reporter

    Looks like it’s working, I had to copy some code from the load method though:

            Key key = keyStore.getKey(alias, keyPassword);
            X509Certificate xcert = (X509Certificate) temp.getCertificate(kid);
            RSAKey rsaJWK = RSAKey.parse(xcert);
            jwk = new RSAKey.Builder(rsaJWK)
                .keyID(kid)
                .privateKey((RSAPrivateKey)key)
                .keyUse(KeyUse.SIGNATURE)
                .keyStore(temp)
                .build();
    

    It would be nice if you allowed the KeyUse to be passed to load method though.

    Possibility to override the kid attribute would also be nice cause when using KeyCloak as identity provider, it expects the kid attribute to be set to a specific value, calculated like this:

    PublicKey publicKey = keyStore.getCertificate(alias).getPublicKey();
    String kid = Base64Url.encode(MessageDigest.getInstance("SHA-256").digest(publicKey.getEncoded())).toString();
    

    The workaround for this is to prepare a temporary keystore and set the alias to the calculated value and then pass it as alias in JWK.load method.

  12. Vladimir Dzhuvinov

    Re: adding new parse(X509Certificate) method to allow passing of a preferred KeyUse - if the cert key use doesn’t map to a single JWK use value I think it’s best to keep it undefined. The cited JWKMatcher will still work because it’s set to select signature use as well as keys with an undefined use.

  13. Log in to comment