MacSigner with secret key from AWS cloudHSM not usable.

Issue #520 resolved
Ulrich Winter created an issue

My goal is to create a (detached) jws signature using a symmetric key from an aws cloud hsm.

The MacSigner puts the SecretKey into a SecretKeySpec (see HMAC.java line 94).

This leads to an error within the aws cloudHSM JCE provider, because this needs a key as loaded from the keystore provided for Mac.init().

Library versions used:

  • nimbus-jose-jwt:9.31
  • aws cloudHSM JCE: cloudhsm-jce:5.10.0
  • Java 17: Corretto-17.0.8.8.1 

Kotlin-Code to reproduce this error with Nimbus MacSigner:

            if (Security.getProvider(CloudHsmProvider.PROVIDER_NAME) == null) {
                Security.addProvider(CloudHsmProvider())
            }

            // Load the HSM as a Java crypto provider
            val hsmProvider: Provider = Security.getProvider(CloudHsmProvider.PROVIDER_NAME)

            // Get a handle to the private key for signing
            val hsmKeyStore: KeyStore = KeyStore.getInstance(CloudHsmProvider.CLOUDHSM_KEYSTORE_TYPE, hsmProvider)
            hsmKeyStore.load(null)
            val secretKey = hsmKeyStore.getKey("KEY-ID", "".toCharArray()) as SecretKey

            val signer = MACSigner(secretKey)
            signer.jcaContext.provider = hsmProvider

            // prepare header and payload
            val header = JWSHeader
                .Builder(JWSAlgorithm.HS256)
                .base64URLEncodePayload(false)
                .criticalParams(setOf("b64"))
                .keyID("KEY-ID)
                .build()


            val payload = Payload("This is the signed payload.")
            val jws = JWSObject(header, payload)

            jws.sign(signer)  // <-- Exception
            val isDetached = true
            val token = jws.serialize(isDetached)

The above code snippet throws the following exception, when JWSObject.sign() is called:

Invalid HMAC key: The provided key is an instance of class javax.crypto.spec.SecretKeySpec when it should be of class com.amazonaws.cloudhsm.jce.provider.GenericSecretKey
com.nimbusds.jose.JOSEException: Invalid HMAC key: The provided key is an instance of class javax.crypto.spec.SecretKeySpec when it should be of class com.amazonaws.cloudhsm.jce.provider.GenericSecretKey
    at com.nimbusds.jose.crypto.impl.HMAC.getInitMac(HMAC.java:65)
    at com.nimbusds.jose.crypto.impl.HMAC.compute(HMAC.java:118)
    at com.nimbusds.jose.crypto.impl.HMAC.compute(HMAC.java:94)
    at com.nimbusds.jose.crypto.MACSigner.sign(MACSigner.java:193)
    at com.nimbusds.jose.JWSObject.sign(JWSObject.java:315)

The following code snippet demonstrates the java crypto api calls issued by Nimbus, which lead to this error, and an alternative call sequence which does not wrap the secret inside a SecretKeySpec which works fine with AWS cloudHSM JCE provider:

        val message = "The message.".toByteArray(StandardCharset.UTF_8)
        if (Security.getProvider(CloudHsmProvider.PROVIDER_NAME) == null) {
            Security.addProvider(CloudHsmProvider())
        }
        val hsmProvider: Provider = Security.getProvider(CloudHsmProvider.PROVIDER_NAME)

        val hsmKeyStore: KeyStore = KeyStore.getInstance(CloudHsmProvider.CLOUDHSM_KEYSTORE_TYPE, hsmProvider)
        hsmKeyStore.load(null)

        val secretKey = hsmKeyStore.getKey("KEY-ID", "".toCharArray()) as SecretKey
        val alg = "HMACSHA256"

        // this works:
        val macOk : Mac = Mac.getInstance(alg, hsmProvider)
        macOk.init(secretKey)
        val resultOk = macOk.doFinal();
        println("result = $resultOk")

        // this not:
        val secretKeySpec = javax.crypto.spec.SecretKeySpec(secretKey.getEncoded(), alg)
        val mac : Mac = Mac.getInstance(secretKeySpec.getAlgorithm(), hsmProvider)
        mac.init(secretKeySpec) // <----java.security.InvalidKeyException: The provided key is an instance of class javax.crypto.spec.SecretKeySpec when it should be of class com.amazonaws.cloudhsm.jce.provider.GenericSecretKey
        mac.update(message)
        val result = mac.doFinal();
        println("result = $result")

Is there a way to use a Nimbus MacSigner with a key from AWS cloudHSM?

Is the wrapping of the SecretKey into a SecretKeySpec required?

If not, I suggest that nimbus uses the SecretKey directly without wrapping it when performing a MacSinger operation.

Comments (17)

  1. Vladimir Dzhuvinov
    • changed status to open

    Hi Ulrich,

    The HMAC facility is not designed to work with PKCE#11 JCE providers.

    We'll check what can be done about the that. Hopefully the required changes will be minimal.

    Will you be able to test the code after it's updated? At present we don't have a way to test HMAC in PKCS#11. Only RSA and EC operations.

  2. Ulrich Winter reporter

    Hi Vladimir, thank you for your fast response.
    I currently do not understand your remark on the support of the JCE provider.
    There is this document section, which describes the use of the SunPKCS11 jce provider together with nimbus-jose-jwt:
    https://connect2id.com/products/nimbus-jose-jwt/examples/pkcs11

    AWS cloudHSM provides two different apis for the Java platform:

    • The JCE provider, which I used. This provider from the current sdk v5 is recommended for containerized workloads.
    • The PKCs#11 library, which in turn can be used together with the SunPKCS11 provider (in theory).

    I also tried using the PKCS11 api together with the SunPKCs11 provider instead of the cloudHSM JCE provider, but I did not get it to work.

    So from my point of view, I currently do NOT apply the PKCS#11 api in the above example. I do not know, what the JCE provider does internally.

  3. Vladimir Dzhuvinov

    When the getEncoded() is called on the AWS HSM SecretKey what gets returned / thrown?

    When I wrote about the HMAC facility previously not being PKCS#11 provider capable, I meant “HSM” capable. The underlying JCE provider can be SunPKCs11 or Amazon’s.

  4. Vladimir Dzhuvinov

    Note to self:

    The MACProvider.getSecret() and getSecretString() methods, JavaDocs and tests need to be reworked depending on what the AWS HSM SecretKey.getEncoded() returns or throws.

    The SecretKey.getEncoded() contract says JCE providers that do not or cannot return the key material must return null.

  5. Ulrich Winter reporter

    When the getEncoded() is called on the AWS HSM SecretKey what gets returned / thrown?

    The AWS CloudHSM does not throw any exception but just returns null

    One can enable to extract keys from the HSM, but this is not intended, as a purpose of the HSM is to keep the secret keys save.

    I suggest that the nimbus lib never calls getEncoded() for any SecretKey, because otherwise it still cannot be used with a key from an HSM.

    The drawback is, that then the size of the secret cannot be determined.

    I think, that the nimbus lib should just assume that the key has the required size and that the Mac operation throws an error, if the key is too small for the algorithm chosen.

  6. Vladimir Dzhuvinov

    Updated the MACProvider and MACSigner classes: 24c99d94913e7b4233ba1ca4e2851ac0c19c1d76

    Thanks for verifying the SecretKey.getEncoded output. I reintroduced the key length checking when the key material is available. This is to give developers clear guidance when the key material is available and not compliant, rather than ending up asking questions here 🙂

  7. Ulrich Winter reporter

    Thanks very much for this quick solution.
    Let me know, when there is a (SNAPSHOT) release to be tested against the hsm.

  8. Vladimir Dzhuvinov

    The update was just pushed out into Maven Central as

    version 9.33 (2023-09-14)
        * Updates the MACSigner to support JCE providers, such as PKCS#11 providers
          or Amazon's CloudHSM, that don't expose the underlying SecretKey
          material (iss #520).
        * Refactors the HMAC class to support PKCS#11 providers.
        * Adds new MACProvider.MACProvider(SecretKey,Set<JWSAlgorithm>)
          constructor.
        * The MACProvider.getSecret and getSecretString methods will return null
          when the provider was constructed with a SecretKey that doesn't expose
          its key material.
    

    I want to complete the MACVerifier next. Would you be able to test the HSxxx verification with the Amazon CloudHSM was well?

  9. Ulrich Winter reporter

    Using

     nimbus-jose-jwt:9.33 
    

    both MACSigner and MACVerifier are working fine with a 256 bit long generic secret from the AWS CloudHSM but only as long as the HSM is configured to allow extraction of the encoded secrets.

    When extracting secrets is disabled, I get the following exception:

    Cannot read the array length because the return value of "com.nimbusds.jose.crypto.MACSigner.getSecret()" is null
    com.nimbusds.jose.JOSEException: Cannot read the array length because the return value of "com.nimbusds.jose.crypto.MACSigner.getSecret()" is null
        at com.nimbusds.jose.JWSObject.sign(JWSObject.java:346)
        ... 
    Caused by: java.lang.NullPointerException: Cannot read the array length because the return value of "com.nimbusds.jose.crypto.MACSigner.getSecret()" is null
        at com.nimbusds.jose.crypto.MACSigner.sign(MACSigner.java:203)
        at com.nimbusds.jose.JWSObject.sign(JWSObject.java:315)
        ... 424 more
    

    Sorry that I did not check this before.

    My patch has the same error.

  10. Vladimir Dzhuvinov

    It’s not a useful HSM if the secret extraction has to be enabled 🙂

    The bug revealed in your testing of the MACSigner was located and fixed: c695b11

    The MACVerifier should also be capable now of dealing with SecretKey instances that don't expose their key material: 45f15d1

    Let me know how the tests work out with this new release:

    version 9.34 (2023-09-14)
        * Updates the MACVerifier to support JCE providers, such as PKCS#11
          providers or Amazon's CloudHSM, that don't expose the underlying
          SecretKey material (iss #520).
        * Fixes the MACSigner.sign method for SecretKey instances that don't expose
          their key material (iss #520).
        * Deprecates HMAC.compute(String,byte[],byte[],Provider), use
          HMAC.compute(String,SecretKey,byte[],Provider) instead.
    

  11. Log in to comment