From c05d1a64c600e9a96831f9b56f12daa916867a9e Mon Sep 17 00:00:00 2001 From: Fabrice Benhamouda Date: Wed, 7 Aug 2024 19:14:50 +0000 Subject: [PATCH] Support for HMAC Precomputed Key This commit adds support for HMAC Precomputed Keys in ACCP. See https://github.com/aws/aws-lc/pull/1574 This commit uses branch `hmac-precomputed-key-size-define` from https://github.com/fabrice102/aws-lc and thus cannot be merged as is. It can only be merged once the above branch is merged to AWS-LC. --- .gitmodules | 2 +- README.md | 25 ++ aws-lc | 2 +- build.gradle | 12 +- csrc/hmac.cpp | 123 +++++++- .../amazon/corretto/crypto/examples/Hmac.kt | 37 +++ .../crypto/examples/HmacWithPrecomputedKey.kt | 53 ++++ .../AmazonCorrettoCryptoProvider.java | 54 +++- .../corretto/crypto/provider/EvpHmac.java | 275 +++++++++++++++--- .../provider/HkdfSecretKeyFactorySpi.java | 9 +- .../HmacWithPrecomputedKeyKeyFactorySpi.java | 107 +++++++ .../crypto/provider/test/HmacTest.java | 173 ++++++++++- 12 files changed, 797 insertions(+), 75 deletions(-) create mode 100644 examples/gradle-kt-dsl/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/Hmac.kt create mode 100644 examples/gradle-kt-dsl/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/HmacWithPrecomputedKey.kt create mode 100644 src/com/amazon/corretto/crypto/provider/HmacWithPrecomputedKeyKeyFactorySpi.java diff --git a/.gitmodules b/.gitmodules index 43d73ea6..7f583a64 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "aws-lc"] path = aws-lc - url = https://github.com/awslabs/aws-lc + url = https://github.com/fabrice102/aws-lc diff --git a/README.md b/README.md index cd9ce154..20e3ccff 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,12 @@ KeyFactory: AlgorithmParameters: * EC. Please refer to [system properties](https://github.com/corretto/amazon-corretto-crypto-provider#other-system-properties) for more information. +Mac algorithms with precomputed key and associated secret key factories (expert use only, refer to [HMAC with Precomputed Key](https://github.com/corretto/amazon-corretto-crypto-provider#HMAC-with-Precomputed-Key) for more information): +* HmacSHA512WithPrecomputedKey +* HmacSHA384WithPrecomputedKey +* HmacSHA256WithPrecomputedKey +* HmacSHA1WithPrecomputedKey +* HmacMD5WithPrecomputedKey # Notes on ACCP-FIPS ACCP-FIPS is a variation of ACCP which uses AWS-LC-FIPS 2.x as its cryptographic module. This version of AWS-LC-FIPS has completed FIPS validation testing by an accredited lab and has been submitted to NIST for certification. Refer to the [NIST Cryptographic Module Validation Program's Modules In Progress List](https://csrc.nist.gov/Projects/cryptographic-module-validation-program/modules-in-process/Modules-In-Process-List) for the latest status of the AWS-LC Cryptographic Module. We will also update our release notes and documentation to reflect any changes in FIPS certification status. We provide ACCP-FIPS for experimentation and performance testing in the interim. @@ -370,6 +376,25 @@ Thus, these should all be set on the JVM command line using `-D`. Allows one to set the temporary directory used by ACCP when loading native libraries. If this system property is not defined, the system property `java.io.tmpdir` is used. +# Additional information + +## HMAC with Precomputed Key + +EXPERT use only. Most users of ACCP just need normal `HmaxXXX` algorithms and not their `WithPrecomputedKey` variants. + +The non-standard-JCA/JCE algorithms `HmacXXXWithPrecomputedKey` (where `XXX` is the digest name, e.g., `SHA384`) implement an optimization of HMAC described in NIST-FIPS-198-1 (Section 6) and in RFC2104 (Section 4). +They allow to generate a precomputed key for a given original key and a given HMAC algorithm, +and then to use this precomputed key to compute HMAC (instead of the original key). +Only use these algorithms if you know you absolutely need them. + +In more detail, the secret key factories `HmacXXXWithPrecomputedKey` allow to generate a precomputed key from a normal HMAC key. +The mac algorithms `HmacXXXWithPrecomputedKey` take a precomputed key instead of a normal HMAC key. +Precomputed keys must implement `SecretKeySpec` with format `RAW` and algorithm `HmacXXXWithPrecomputedKey`. + +Implementation uses AWS-LC functions `HMAC_set_precomputed_key_export`, `HMAC_get_precomputed_key`, and `HMAC_Init_from_precomputed_key`. + +See [example HmacWithPrecomputedKey](./examples/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/HmacWithPrecomputedKey.kt). + # License This library is licensed under the Apache 2.0 license although portions of this product include software licensed under the [dual OpenSSL and SSLeay diff --git a/aws-lc b/aws-lc index 4368aaa6..079c8676 160000 --- a/aws-lc +++ b/aws-lc @@ -1 +1 @@ -Subproject commit 4368aaa6975ba41bd76d3bb12fac54c4680247fb +Subproject commit 079c867611bba8a1f7626da00868e3def18d2c80 diff --git a/build.gradle b/build.gradle index 8a49bad6..9b507d85 100644 --- a/build.gradle +++ b/build.gradle @@ -16,9 +16,9 @@ group = 'software.amazon.cryptools' version = '2.4.1' ext.isFips = Boolean.getBoolean('FIPS') if (ext.isFips) { - ext.awsLcGitVersionId = 'AWS-LC-FIPS-2.0.13' + ext.awsLcGitVersionId = 'hmac-precomputed-key-size-define' // FIXME: needed for version w/ precomputed keys } else { - ext.awsLcGitVersionId = 'v1.30.1' + ext.awsLcGitVersionId = 'hmac-precomputed-key-size-define' // FIXME: needed for version w/ precomputed keys } // Check for user inputted git version ID. @@ -197,10 +197,10 @@ task buildAwsLc { workingDir awslcSrcPath commandLine "git", "fetch", "--tags" } - exec { - workingDir awslcSrcPath - commandLine "git", "checkout", awsLcGitVersionId - } +// exec { +// workingDir awslcSrcPath +// commandLine "git", "checkout", awsLcGitVersionId +// } } mkdir "${buildDir}/awslc" mkdir sharedObjectOutDir diff --git a/csrc/hmac.cpp b/csrc/hmac.cpp index 38e41e18..ea8c1095 100644 --- a/csrc/hmac.cpp +++ b/csrc/hmac.cpp @@ -14,7 +14,7 @@ using namespace AmazonCorrettoCryptoProvider; // For the smaller data-sizes we're using, avoiding GetPrimitiveArrayCritical is worth it. namespace { -void maybe_init_ctx(raii_env& env, HMAC_CTX* ctx, jbyteArray& keyArr, jlong evpMd) +void maybe_init_ctx(raii_env& env, HMAC_CTX* ctx, jbyteArray& keyArr, jlong evpMd, jboolean usePrecomputedKey) { if (DO_NOT_INIT == evpMd) { return; @@ -33,13 +33,22 @@ void maybe_init_ctx(raii_env& env, HMAC_CTX* ctx, jbyteArray& keyArr, jlong evpM // of wrapping it in a java_buffer when we don't need it. java_buffer keyBuf = java_buffer::from_array(env, keyArr); jni_borrow key(env, keyBuf, "key"); - if (unlikely( - HMAC_Init_ex(ctx, key.data(), key.len(), reinterpret_cast(evpMd), nullptr /* ENGINE */) - != 1)) { - throw_openssl("Unable to initialize HMAC_CTX"); + if (unlikely(usePrecomputedKey)) { + if (unlikely( + HMAC_Init_from_precomputed_key(ctx, key.data(), key.len(), reinterpret_cast(evpMd)) + != 1)) { + throw_openssl("Unable to initialize HMAC_CTX using precomputed key"); + } + } else { + if (unlikely(HMAC_Init_ex( + ctx, key.data(), key.len(), reinterpret_cast(evpMd), nullptr /* ENGINE */) + != 1)) { + throw_openssl("Unable to initialize HMAC_CTX"); + } } } } +} void update_ctx(raii_env& env, HMAC_CTX* ctx, jni_borrow& input) { @@ -59,6 +68,26 @@ void calculate_mac(raii_env& env, HMAC_CTX* ctx, java_buffer& result) // it can be faster to use put_bytes rather than convert it into a jni_borrow. result.put_bytes(env, scratch, 0, macSize); } + +jint get_precomputed_key_size(raii_env& env, jstring digestName) +{ + jni_string name(env, digestName); + if (!strcmp("md5", name)) { + return HMAC_MD5_PRECOMPUTED_KEY_SIZE; + } else if (!strcmp("sha1", name)) { + return HMAC_SHA1_PRECOMPUTED_KEY_SIZE; + } else if (!strcmp("sha256", name)) { + return HMAC_SHA256_PRECOMPUTED_KEY_SIZE; + } else if (!strcmp("sha384", name)) { + return HMAC_SHA384_PRECOMPUTED_KEY_SIZE; + } else if (!strcmp("sha512", name)) { + return HMAC_SHA512_PRECOMPUTED_KEY_SIZE; + } else { + // This should not happen: this function should only be called with valid digest names by the Java code + throw_java_ex( + EX_ERROR, "THIS SHOULD NOT BE REACHABLE. Invalid digest name provided to get_precomputed_key_size."); + } + return 0; // just to please the static verifier, since throw_java_ex always throws an exception } #ifdef __cplusplus @@ -77,10 +106,17 @@ JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_getConte /* * Class: com_amazon_corretto_crypto_provider_EvpHmac * Method: updateCtxArray - * Signature: ([B[BJ[BII)V + * Signature: ([B[BJ[BIIZ)V */ -JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_updateCtxArray( - JNIEnv* pEnv, jclass, jbyteArray ctxArr, jbyteArray keyArr, jlong evpMd, jbyteArray inputArr, jint offset, jint len) +JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_updateCtxArray(JNIEnv* pEnv, + jclass, + jbyteArray ctxArr, + jbyteArray keyArr, + jlong evpMd, + jbyteArray inputArr, + jint offset, + jint len, + jboolean usePrecomputedKey) { try { raii_env env(pEnv); @@ -88,7 +124,7 @@ JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_updateCt java_buffer inputBuf = java_buffer::from_array(env, inputArr, offset, len); - maybe_init_ctx(env, ctx, keyArr, evpMd); + maybe_init_ctx(env, ctx, keyArr, evpMd, usePrecomputedKey); jni_borrow input(env, inputBuf, "input"); update_ctx(env, ctx, input); @@ -119,17 +155,18 @@ JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_doFinal( /* * Class: com_amazon_corretto_crypto_provider_EvpHmac * Method: fastHmac - * Signature: ([B[BJ[BII[B)V + * Signature: ([B[BJ[BII[BZ)V */ JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_fastHmac(JNIEnv* pEnv, - jclass clazz, + jclass, jbyteArray ctxArr, jbyteArray keyArr, jlong evpMd, jbyteArray inputArr, jint offset, jint len, - jbyteArray resultArr) + jbyteArray resultArr, + jboolean usePrecomputedKey) { // We do not depend on the other methods because it results in more use to JNI than we want and lower performance try { @@ -138,7 +175,7 @@ JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_fastHmac java_buffer inputBuf = java_buffer::from_array(env, inputArr, offset, len); java_buffer resultBuf = java_buffer::from_array(env, resultArr); - maybe_init_ctx(env, ctx, keyArr, evpMd); + maybe_init_ctx(env, ctx, keyArr, evpMd, usePrecomputedKey); { jni_borrow input(env, inputBuf, "input"); @@ -153,6 +190,66 @@ JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_fastHmac } } +/* + * Class: Java_com_amazon_corretto_crypto_provider_EvpHmac + * Method: getPrecomputedKeyLength + * Signature: (Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_EvpHmac_getPrecomputedKeyLength( + JNIEnv* pEnv, jclass, jstring digestName) +{ + try { + raii_env env(pEnv); + return get_precomputed_key_size(env, digestName); + } catch (java_ex& ex) { + ex.throw_to_java(pEnv); + } + return 0; +} + +/* + * Class: com_amazon_corretto_crypto_provider_HmacWithPrecomputedKeyKeyFactorySpi + * Method: getPrecomputedKey + * Signature: ([BI[BIJ)V + */ +JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_HmacWithPrecomputedKeyKeyFactorySpi_getPrecomputedKey( + JNIEnv* pEnv, jclass, jbyteArray jOutput, jint outputLen, jbyteArray jKey, jint keyLen, jlong evpMd) +{ + try { + JBinaryBlob result(pEnv, nullptr, jOutput); + JBinaryBlob key(pEnv, nullptr, jKey); + + bssl::ScopedHMAC_CTX ctx; + + if (unlikely(HMAC_Init_ex(ctx.get(), + key.get(), // key + keyLen, // keyLen + reinterpret_cast(evpMd), // EVP_MD + nullptr /* ENGINE */) + != 1)) { + throw_openssl("Unable to initialize HMAC_CTX"); + } + + if (unlikely(HMAC_set_precomputed_key_export(ctx.get()) != 1)) { + throw_openssl("Unable to call HMAC_set_precomputed_key_export"); + } + + // HMAC_get_precomputed_key takes as input the length of the buffer + // and update it to the actual length of the precomputed key. + // The Java caller always selects the right buffer size, so we should not have any error. + // But we do a sanity check that this is the case. + size_t actualOutputLen = outputLen; + if (unlikely(HMAC_get_precomputed_key(ctx.get(), result.get(), &actualOutputLen) != 1)) { + throw_openssl("Unable to call HMAC_get_precomputed_key"); + } + if (unlikely(outputLen < 0 || (size_t)outputLen != actualOutputLen)) { + throw_java_ex(EX_ERROR, "THIS SHOULD NOT BE REACHABLE. invalid output precomputed key length."); + } + } catch (java_ex& ex) { + ex.throw_to_java(pEnv); + } +} + #ifdef __cplusplus } #endif diff --git a/examples/gradle-kt-dsl/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/Hmac.kt b/examples/gradle-kt-dsl/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/Hmac.kt new file mode 100644 index 00000000..5e37e378 --- /dev/null +++ b/examples/gradle-kt-dsl/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/Hmac.kt @@ -0,0 +1,37 @@ +package com.amazon.corretto.crypto.examples + +import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider +import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class Hmac { + @Test + fun hmacTest() { + val accpProviderName = "AmazonCorrettoCryptoProvider" + AmazonCorrettoCryptoProvider.install() + + val mac = Mac.getInstance("HmacSHA384") + assertEquals(accpProviderName, mac.provider.name) + + // An arbitrary 32-bytes key in base64 for the example + val keyBase64 = "62lKZjLXnX4yGvNyd3/M3q+T6yfREHgbIoJidXCEzGw=" + val key = Base64.getDecoder().decode(keyBase64) + val keySpec = SecretKeySpec(key, "Generic") + + val message = "Hello, this is just an example." + + // Compute the MAC + mac.init(keySpec); + val macResult = mac.doFinal(message.toByteArray()) + + // Verify the result matches what we expect + val expectedResultBase64 = + "w72DBgWvjTDqlv+EzOc1/R+K9Qq1jrNCHCQewXXhaOQ8Joi2jPPQdAT+HDc65KMM" + val expectedResult = Base64.getDecoder().decode(expectedResultBase64) + assertContentEquals(expectedResult, macResult) + } +} \ No newline at end of file diff --git a/examples/gradle-kt-dsl/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/HmacWithPrecomputedKey.kt b/examples/gradle-kt-dsl/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/HmacWithPrecomputedKey.kt new file mode 100644 index 00000000..a3f569cb --- /dev/null +++ b/examples/gradle-kt-dsl/lib/src/test/kotlin/com/amazon/corretto/crypto/examples/HmacWithPrecomputedKey.kt @@ -0,0 +1,53 @@ +package com.amazon.corretto.crypto.examples + +import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider +import java.security.SecureRandom +import java.util.* +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class HmacWithPrecomputedKey { + @Test + fun hmacWithPrecomputedKeyTest() { + // EXPERT-ONLY use + // This example is most likely NOT what you want to use. + // If you need to use Hmac, see the Hmac.kt example. + // This example shows how to use precomputed keys, which is not standard in JCA/JCE. + // See ACCP README.md for details. + + val accpProviderName = "AmazonCorrettoCryptoProvider" + AmazonCorrettoCryptoProvider.install() + + val mac = Mac.getInstance("HmacSHA384WithPrecomputedKey") + assertEquals(accpProviderName, mac.provider.name) + + val skf = SecretKeyFactory.getInstance("HmacSHA384WithPrecomputedKey") + assertEquals(accpProviderName, skf.provider.name) + + // An arbitrary 32-bytes key in base64 for the example + val keyBase64 = "62lKZjLXnX4yGvNyd3/M3q+T6yfREHgbIoJidXCEzGw="; + val key = Base64.getDecoder().decode(keyBase64); + val keySpec = SecretKeySpec(key, "Generic"); + + val message = "Hello, this is just an example." + + // Compute the HMAC precomputed key + val precomputedKey = skf.generateSecret(keySpec) + + // Compute the HMAC using the precomputed key + mac.init(precomputedKey); + val macResult = mac.doFinal(message.toByteArray()) + + // Verify the result matches what we expect + val expectedResultBase64 = + "w72DBgWvjTDqlv+EzOc1/R+K9Qq1jrNCHCQewXXhaOQ8Joi2jPPQdAT+HDc65KMM" + val expectedResult = Base64.getDecoder().decode(expectedResultBase64) + assertContentEquals(expectedResult, macResult) + } +} \ No newline at end of file diff --git a/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java b/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java index eb646098..a29f1833 100644 --- a/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java +++ b/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java @@ -5,6 +5,14 @@ import static com.amazon.corretto.crypto.provider.AesCbcSpi.AES_CBC_ISO10126_PADDING_NAMES; import static com.amazon.corretto.crypto.provider.AesCbcSpi.AES_CBC_NO_PADDING_NAMES; import static com.amazon.corretto.crypto.provider.AesCbcSpi.AES_CBC_PKCS7_PADDING_NAMES; +import static com.amazon.corretto.crypto.provider.EvpHmac.HMAC_MD5_WITH_PRECOMPUTED_KEY; +import static com.amazon.corretto.crypto.provider.EvpHmac.HMAC_PREFIX; +import static com.amazon.corretto.crypto.provider.EvpHmac.HMAC_SHA1_WITH_PRECOMPUTED_KEY; +import static com.amazon.corretto.crypto.provider.EvpHmac.HMAC_SHA256_WITH_PRECOMPUTED_KEY; +import static com.amazon.corretto.crypto.provider.EvpHmac.HMAC_SHA384_WITH_PRECOMPUTED_KEY; +import static com.amazon.corretto.crypto.provider.EvpHmac.HMAC_SHA512_WITH_PRECOMPUTED_KEY; +import static com.amazon.corretto.crypto.provider.EvpHmac.WITH_PRECOMPUTED_KEY; +import static com.amazon.corretto.crypto.provider.HkdfSecretKeyFactorySpi.HKDF_PREFIX; import static com.amazon.corretto.crypto.provider.HkdfSecretKeyFactorySpi.HKDF_WITH_SHA1; import static com.amazon.corretto.crypto.provider.HkdfSecretKeyFactorySpi.HKDF_WITH_SHA256; import static com.amazon.corretto.crypto.provider.HkdfSecretKeyFactorySpi.HKDF_WITH_SHA384; @@ -124,6 +132,38 @@ private void buildServiceMap() { addService("Mac", "Hmac" + hash, "EvpHmac$" + hash); } + for (String hash : new String[] {"MD5", "SHA1", "SHA256", "SHA384", "SHA512"}) { + addService( + "Mac", "Hmac" + hash + WITH_PRECOMPUTED_KEY, "EvpHmac$" + hash + WITH_PRECOMPUTED_KEY); + } + + final String hmacWithPrecomputedKeyKeyFactorySpi = "HmacWithPrecomputedKeyKeyFactorySpi"; + addService( + "SecretKeyFactory", + HMAC_MD5_WITH_PRECOMPUTED_KEY, + hmacWithPrecomputedKeyKeyFactorySpi, + false); + addService( + "SecretKeyFactory", + HMAC_SHA1_WITH_PRECOMPUTED_KEY, + hmacWithPrecomputedKeyKeyFactorySpi, + false); + addService( + "SecretKeyFactory", + HMAC_SHA256_WITH_PRECOMPUTED_KEY, + hmacWithPrecomputedKeyKeyFactorySpi, + false); + addService( + "SecretKeyFactory", + HMAC_SHA384_WITH_PRECOMPUTED_KEY, + hmacWithPrecomputedKeyKeyFactorySpi, + false); + addService( + "SecretKeyFactory", + HMAC_SHA512_WITH_PRECOMPUTED_KEY, + hmacWithPrecomputedKeyKeyFactorySpi, + false); + addService( "KeyAgreement", "ECDH", @@ -302,7 +342,8 @@ public Object newInstance(final Object constructorParameter) throws NoSuchAlgori final String type = getType(); final String algo = getAlgorithm(); - if ("SecretKeyFactory".equalsIgnoreCase(type)) { + if ("SecretKeyFactory".equalsIgnoreCase(type) + && algo.toUpperCase().startsWith(HKDF_PREFIX.toUpperCase())) { final HkdfSecretKeyFactorySpi spi = HkdfSecretKeyFactorySpi.INSTANCES.get( HkdfSecretKeyFactorySpi.getSpiFactoryForAlgName(algo)); @@ -311,6 +352,17 @@ public Object newInstance(final Object constructorParameter) throws NoSuchAlgori } } + if ("SecretKeyFactory".equalsIgnoreCase(type) + && algo.toUpperCase().startsWith(HMAC_PREFIX.toUpperCase()) + && algo.toUpperCase().endsWith(WITH_PRECOMPUTED_KEY.toUpperCase())) { + final HmacWithPrecomputedKeyKeyFactorySpi spi = + HmacWithPrecomputedKeyKeyFactorySpi.INSTANCES.get( + HmacWithPrecomputedKeyKeyFactorySpi.getSpiFactoryForAlgName(algo)); + if (spi != null) { + return spi; + } + } + if ("KeyGenerator".equalsIgnoreCase(type) && "AES".equalsIgnoreCase(algo)) { return SecretKeyGenerator.createAesKeyGeneratorSpi(); } diff --git a/src/com/amazon/corretto/crypto/provider/EvpHmac.java b/src/com/amazon/corretto/crypto/provider/EvpHmac.java index a7c5b487..1a3b659b 100644 --- a/src/com/amazon/corretto/crypto/provider/EvpHmac.java +++ b/src/com/amazon/corretto/crypto/provider/EvpHmac.java @@ -21,6 +21,18 @@ import javax.crypto.spec.SecretKeySpec; class EvpHmac extends MacSpi implements Cloneable { + static final String HMAC_PREFIX = "Hmac"; + static final String WITH_PRECOMPUTED_KEY = "WithPrecomputedKey"; + + static final String HMAC_SHA512_WITH_PRECOMPUTED_KEY = + HMAC_PREFIX + "SHA512" + WITH_PRECOMPUTED_KEY; + static final String HMAC_SHA384_WITH_PRECOMPUTED_KEY = + HMAC_PREFIX + "SHA384" + WITH_PRECOMPUTED_KEY; + static final String HMAC_SHA256_WITH_PRECOMPUTED_KEY = + HMAC_PREFIX + "SHA256" + WITH_PRECOMPUTED_KEY; + static final String HMAC_SHA1_WITH_PRECOMPUTED_KEY = HMAC_PREFIX + "SHA1" + WITH_PRECOMPUTED_KEY; + static final String HMAC_MD5_WITH_PRECOMPUTED_KEY = HMAC_PREFIX + "MD5" + WITH_PRECOMPUTED_KEY; + /** When passed to {@code evpMd} indicates that the native code should not call HMAC_Init_ex. */ private static long DO_NOT_INIT = -1; /** @@ -33,21 +45,44 @@ class EvpHmac extends MacSpi implements Cloneable { private static native int getContextSize(); /** - * Calls {@code HMAC_Update} with {@code input}, possibly calling {@code HMAC_Init_ex} first (if - * {@code evpMd} is any value except {@link #DO_NOT_INIT}). This method should only be used via - * {@link #synchronizedUpdateCtxArray(byte[], byte[], long, byte[], int, int)}. + * Returns the length of the precomputed key for the HMAC for the hash function with name + * digestName + * + * @param digestName name of the digest (md5,sha1,sha256,sha384,sha512) + * @return the length of the precomputed key, in bytes + */ + static native int getPrecomputedKeyLength(String digestName); + + /** + * Calls {@code HMAC_Update} with {@code input}, possibly calling {@code HMAC_Init_ex} or {@code + * HMAC_Init_from_precomputed_key} first (if {@code evpMd} is any value except {@link + * #DO_NOT_INIT}). This method should only be used via {@link #synchronizedUpdateCtxArray(byte[], + * byte[], long, byte[], int, int, boolean)}. * * @param ctx opaque array containing native context */ private static native void updateCtxArray( - byte[] ctx, byte[] key, long evpMd, byte[] input, int offset, int length); + byte[] ctx, + byte[] key, + long evpMd, + byte[] input, + int offset, + int length, + boolean usePrecomputedKey); + /** - * @see {@link #updateCtxArray(byte[], byte[], long, byte[], int, int)} + * @see {@link #updateCtxArray(byte[], byte[], long, byte[], int, int, boolean)} */ private static void synchronizedUpdateCtxArray( - byte[] ctx, byte[] key, long evpMd, byte[] input, int offset, int length) { + byte[] ctx, + byte[] key, + long evpMd, + byte[] input, + int offset, + int length, + boolean usePrecomputedKey) { synchronized (ctx) { - updateCtxArray(ctx, key, evpMd, input, offset, length); + updateCtxArray(ctx, key, evpMd, input, offset, length, usePrecomputedKey); } } @@ -71,19 +106,33 @@ private static void synchronizedDoFinal(byte[] ctx, byte[] result) { /** * Calls {@code HMAC_Init_ex}, {@code HMAC_Update}, and {@code HMAC_Final} with {@code input}. * This method should only be used via {@link #synchronizedFastHmac(byte[], byte[], long, byte[], - * int, int, byte[])}. + * int, int, byte[], boolean)}. * * @param ctx opaque array containing native context */ private static native void fastHmac( - byte[] ctx, byte[] key, long evpMd, byte[] input, int offset, int length, byte[] result); + byte[] ctx, + byte[] key, + long evpMd, + byte[] input, + int offset, + int length, + byte[] result, + boolean usePrecomputedKey); /** - * @see {@link #fastHmac(byte[], byte[], long, byte[], int, int, byte[])} + * @see {@link #fastHmac(byte[], byte[], long, byte[], int, int, byte[], boolean)} */ private static void synchronizedFastHmac( - byte[] ctx, byte[] key, long evpMd, byte[] input, int offset, int length, byte[] result) { + byte[] ctx, + byte[] key, + long evpMd, + byte[] input, + int offset, + int length, + byte[] result, + boolean usePrecomputedKey) { synchronized (ctx) { - fastHmac(ctx, key, evpMd, input, offset, length, result); + fastHmac(ctx, key, evpMd, input, offset, length, result, usePrecomputedKey); } } @@ -93,12 +142,29 @@ private static void synchronizedFastHmac( private HmacState state; private InputBuffer buffer; - EvpHmac(long evpMd, int digestLength) { + private static final String WITH_PRECOMPUTE_KEY = "WithPrecomputedKey"; + + /** + * @param digestName is the name of the digest in lowercase (e.g., "sha256", "md5") + * @param baseAlgorithm the base name of the algorithm without "WithPrecomputedKey" (e.g., + * "HmacMd5") + * @param usePrecomputedKey true is using precomputed keys instead of normal keys + */ + EvpHmac(String digestName, final String baseAlgorithm, final boolean usePrecomputedKey) { + final long evpMd = Utils.getEvpMdFromName(digestName); + final int digestLength = Utils.getDigestLength(evpMd); + final int precomputedKeyLength = getPrecomputedKeyLength(digestName); + if (evpMd == DO_NOT_INIT || evpMd == DO_NOT_REKEY) { throw new AssertionError( "Unexpected value for evpMd conflicting with reserved negative value: " + evpMd); } - this.state = new HmacState(evpMd, digestLength); + String algorithm = baseAlgorithm; + if (usePrecomputedKey) { + algorithm += WITH_PRECOMPUTE_KEY; + } + this.state = + new HmacState(evpMd, digestLength, algorithm, usePrecomputedKey, precomputedKeyLength); this.buffer = new InputBuffer(1024); configureLambdas(); } @@ -113,14 +179,16 @@ private void configureLambdas() { if (state.needsRekey) { evpMd = state.evpMd; } - synchronizedUpdateCtxArray(state.context, rawKey, evpMd, src, offset, length); + synchronizedUpdateCtxArray( + state.context, rawKey, evpMd, src, offset, length, state.usePrecomputedKey); state.needsRekey = false; return null; }) .withUpdater( (ignored, src, offset, length) -> { assertInitialized(); - synchronizedUpdateCtxArray(state.context, null, DO_NOT_INIT, src, offset, length); + synchronizedUpdateCtxArray( + state.context, null, DO_NOT_INIT, src, offset, length, state.usePrecomputedKey); }) .withDoFinal( (ignored) -> { @@ -138,7 +206,15 @@ private void configureLambdas() { if (state.needsRekey) { evpMd = state.evpMd; } - synchronizedFastHmac(state.context, rawKey, evpMd, src, offset, length, result); + synchronizedFastHmac( + state.context, + rawKey, + evpMd, + src, + offset, + length, + result, + state.usePrecomputedKey); state.needsRekey = false; return result; }); @@ -200,14 +276,43 @@ public EvpHmac clone() throws CloneNotSupportedException { private static final class HmacState implements Cloneable { private SecretKey key; private final long evpMd; + /** + * Name of the algorithm used to create this instance. This is used to ensure that the key is + * appropriate for the algorithm, when using precomputed keys. + */ + private final String algorithm; + private final int digestLength; private byte[] context = new byte[CONTEXT_SIZE]; private byte[] encoded_key; + /** + * True if precomputed keys are used instead of raw HMAC keys, that is for algorithms + * `HmacXXXWithPrecomputedKey`. + */ + private final boolean usePrecomputedKey; + + private final int precomputedKeyLength; + boolean needsRekey = true; - private HmacState(long evpMd, int digestLength) { + /** + * @param evpMd the evpMd corresponding to the digest used + * @param digestLength the length of the digest in bytes + * @param algorithm the full name algorithm (e.g., "HmacMD5" or "HmacMD5WithPrecomputedKey") + * @param usePrecomputedKey false = normal HMAC, true = uses precomputed keys + * @param precomputedKeyLength length of precomputed keys in bytes + */ + private HmacState( + final long evpMd, + final int digestLength, + final String algorithm, + final boolean usePrecomputedKey, + final int precomputedKeyLength) { this.evpMd = evpMd; this.digestLength = digestLength; + this.algorithm = Objects.requireNonNull(algorithm); + this.usePrecomputedKey = usePrecomputedKey; + this.precomputedKeyLength = precomputedKeyLength; } private void setKey(SecretKey key) throws InvalidKeyException { @@ -218,10 +323,19 @@ private void setKey(SecretKey key) throws InvalidKeyException { if (!"RAW".equalsIgnoreCase(key.getFormat())) { throw new InvalidKeyException("Key must support RAW encoding"); } + if (usePrecomputedKey && !algorithm.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException( + "Key must be for algorithm \"" + algorithm + "\" when using precomputed keys"); + } + byte[] encoded = key.getEncoded(); if (encoded == null) { throw new InvalidKeyException("Key encoding must not be null"); } + if (usePrecomputedKey && encoded.length != precomputedKeyLength) { + throw new InvalidKeyException( + "Key must be of length \"" + precomputedKeyLength + "\" when using precomputed keys"); + } this.encoded_key = encoded; this.key = key; this.needsRekey = true; @@ -312,78 +426,143 @@ private static SelfTestResult runSelfTest(String macName, Class getInstances() { final Map result = new HashMap<>(); diff --git a/src/com/amazon/corretto/crypto/provider/HmacWithPrecomputedKeyKeyFactorySpi.java b/src/com/amazon/corretto/crypto/provider/HmacWithPrecomputedKeyKeyFactorySpi.java new file mode 100644 index 00000000..fd20b176 --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/HmacWithPrecomputedKeyKeyFactorySpi.java @@ -0,0 +1,107 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider; + +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactorySpi; +import javax.crypto.spec.SecretKeySpec; + +class HmacWithPrecomputedKeyKeyFactorySpi extends SecretKeyFactorySpi { + + private final long evpMd; + private final int precomputedKeyLength; + private final String algorithmName; + + /** + * Compute the HMAC precomputed key for digest {@code evpMd} and HMAC key {@code key} and store it + * in {@code result}. + * + * @param output resulting precomputed key + * @param outputLen length of output + * @param key input key + * @param keyLen length of key + * @param evpMd digest used + */ + private static native void getPrecomputedKey( + byte[] output, int outputLen, byte[] key, int keyLen, long evpMd); + + private HmacWithPrecomputedKeyKeyFactorySpi(final String algorithmName, final String digestName) { + this.evpMd = Utils.getEvpMdFromName(digestName); + this.precomputedKeyLength = EvpHmac.getPrecomputedKeyLength(digestName); + this.algorithmName = algorithmName; + } + + @Override + protected SecretKey engineGenerateSecret(final KeySpec keySpec) throws InvalidKeySpecException { + if (!(keySpec instanceof SecretKeySpec)) { + throw new InvalidKeySpecException("KeySpec must be an instance of SecretKeySpec"); + } + final SecretKeySpec spec = (SecretKeySpec) keySpec; + + if (!"RAW".equalsIgnoreCase(spec.getFormat())) { + throw new InvalidKeySpecException("KeySpec must support RAW encoding"); + } + + byte[] precomputedKey = new byte[precomputedKeyLength]; + + byte[] key = spec.getEncoded(); + if (key == null) { + throw new InvalidKeySpecException("Key encoding must not be null"); + } + getPrecomputedKey(precomputedKey, precomputedKeyLength, key, key.length, evpMd); + + return new SecretKeySpec(precomputedKey, algorithmName); + } + + @Override + protected KeySpec engineGetKeySpec(final SecretKey key, final Class keySpec) { + throw new UnsupportedOperationException(); + } + + @Override + protected SecretKey engineTranslateKey(final SecretKey key) { + throw new UnsupportedOperationException(); + } + + static final Map INSTANCES = getInstances(); + + private static final String MD5_DIGEST_NAME = "md5"; + private static final String SHA1_DIGEST_NAME = "sha1"; + private static final String SHA256_DIGEST_NAME = "sha256"; + private static final String SHA384_DIGEST_NAME = "sha384"; + private static final String SHA512_DIGEST_NAME = "sha512"; + + private static Map getInstances() { + final Map result = new HashMap<>(); + result.put( + getSpiFactoryForAlgName(EvpHmac.HMAC_MD5_WITH_PRECOMPUTED_KEY), + new HmacWithPrecomputedKeyKeyFactorySpi( + EvpHmac.HMAC_MD5_WITH_PRECOMPUTED_KEY, MD5_DIGEST_NAME)); + result.put( + getSpiFactoryForAlgName(EvpHmac.HMAC_SHA1_WITH_PRECOMPUTED_KEY), + new HmacWithPrecomputedKeyKeyFactorySpi( + EvpHmac.HMAC_SHA1_WITH_PRECOMPUTED_KEY, SHA1_DIGEST_NAME)); + result.put( + getSpiFactoryForAlgName(EvpHmac.HMAC_SHA256_WITH_PRECOMPUTED_KEY), + new HmacWithPrecomputedKeyKeyFactorySpi( + EvpHmac.HMAC_SHA256_WITH_PRECOMPUTED_KEY, SHA256_DIGEST_NAME)); + result.put( + getSpiFactoryForAlgName(EvpHmac.HMAC_SHA384_WITH_PRECOMPUTED_KEY), + new HmacWithPrecomputedKeyKeyFactorySpi( + EvpHmac.HMAC_SHA384_WITH_PRECOMPUTED_KEY, SHA384_DIGEST_NAME)); + result.put( + getSpiFactoryForAlgName(EvpHmac.HMAC_SHA512_WITH_PRECOMPUTED_KEY), + new HmacWithPrecomputedKeyKeyFactorySpi( + EvpHmac.HMAC_SHA512_WITH_PRECOMPUTED_KEY, SHA512_DIGEST_NAME)); + return Collections.unmodifiableMap(result); + } + + static String getSpiFactoryForAlgName(final String alg) { + return alg.toUpperCase(); + } +} diff --git a/tst/com/amazon/corretto/crypto/provider/test/HmacTest.java b/tst/com/amazon/corretto/crypto/provider/test/HmacTest.java index 19819041..774488b5 100644 --- a/tst/com/amazon/corretto/crypto/provider/test/HmacTest.java +++ b/tst/com/amazon/corretto/crypto/provider/test/HmacTest.java @@ -21,6 +21,8 @@ import java.security.InvalidKeyException; import java.security.Provider.Service; import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -32,6 +34,7 @@ import java.util.zip.GZIPInputStream; import javax.crypto.Mac; import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Hex; @@ -48,13 +51,18 @@ @Execution(ExecutionMode.CONCURRENT) @ResourceLock(value = TestUtil.RESOURCE_GLOBAL, mode = ResourceAccessMode.READ) public class HmacTest { + // FIXME: I do not know which version it will be + private static final String MINIMUM_VERSION_WITH_PRECOMPUTED_KEY = "1.5.0"; + private static final Class UTILS_CLASS; private static final List SUPPORTED_HMACS; static { List macs = new ArrayList<>(); for (final Service s : NATIVE_PROVIDER.getServices()) { - if (s.getType().equals("Mac") && s.getAlgorithm().startsWith("Hmac")) { + if (s.getType().equals("Mac") + && s.getAlgorithm().startsWith("Hmac") + && !s.getAlgorithm().endsWith("WithPrecomputedKey")) { macs.add(s.getAlgorithm()); } } @@ -70,6 +78,28 @@ private static List supportedHmacs() { return SUPPORTED_HMACS; } + private int getPrecomputedKeyLength(String algorithm) { + int precomputedKeySize; + switch (algorithm) { + case "HmacMD5": + precomputedKeySize = 16; + break; + case "HmacSHA1": + precomputedKeySize = 20; + break; + case "HmacSHA256": + precomputedKeySize = 32; + break; + case "HmacSHA384": + case "HmacSHA512": + precomputedKeySize = 64; + break; + default: + throw new IllegalArgumentException("Unknown algorithm: " + algorithm); + } + return precomputedKeySize; + } + @Test public void requireInitialization() throws GeneralSecurityException { final Mac hmac = Mac.getInstance("HmacSHA256", NATIVE_PROVIDER); @@ -415,6 +445,114 @@ public byte[] getEncoded() { assertThrows(InvalidKeyException.class, () -> mac.init(nullFormat)); } + @SuppressWarnings("serial") + @ParameterizedTest + @MethodSource("supportedHmacs") + public void engineInitErrorsWithPrecomputedKey(final String algorithm) throws Exception { + TestUtil.assumeMinimumVersion( + MINIMUM_VERSION_WITH_PRECOMPUTED_KEY, AmazonCorrettoCryptoProvider.INSTANCE); + + final int precomputedKeyLength = getPrecomputedKeyLength(algorithm); + byte[] precomputedKey = new byte[precomputedKeyLength]; + + // Compare to Hmac, HmacWithPrecomputedKey requires the algorithm to be + // "HmacXXXWithPrecomputedKey" + // where XXX is the digest + final String keyAlgorithm = algorithm + "WithPrecomputedKey"; + + final SecretKey validKey = new SecretKeySpec(precomputedKey, keyAlgorithm); + final PublicKey pubKey = + new PublicKey() { + @Override + public String getFormat() { + return "RAW"; + } + + @Override + public byte[] getEncoded() { + return precomputedKey; + } + + @Override + public String getAlgorithm() { + return "RAW"; + } + }; + final SecretKey badLength = new SecretKeySpec(new byte[precomputedKeyLength + 1], keyAlgorithm); + final SecretKey badAlgorithm = new SecretKeySpec(precomputedKey, "Generic"); + final SecretKey badFormat = + new SecretKeySpec(precomputedKey, keyAlgorithm) { + @Override + public String getFormat() { + return "UnexpectedFormat"; + } + }; + final SecretKey nullFormat = + new SecretKeySpec(precomputedKey, keyAlgorithm) { + @Override + public String getFormat() { + return null; + } + }; + final SecretKey nullEncoding = + new SecretKeySpec(precomputedKey, keyAlgorithm) { + @Override + public byte[] getEncoded() { + return null; + } + }; + + final Mac mac = Mac.getInstance(algorithm + "WithPrecomputedKey", NATIVE_PROVIDER); + + assertThrows( + InvalidAlgorithmParameterException.class, + () -> mac.init(validKey, new IvParameterSpec(new byte[0]))); + assertThrows(InvalidKeyException.class, () -> mac.init(pubKey)); + assertThrows(InvalidKeyException.class, () -> mac.init(badFormat)); + assertThrows(InvalidKeyException.class, () -> mac.init(badLength)); + assertThrows(InvalidKeyException.class, () -> mac.init(badAlgorithm)); + assertThrows(InvalidKeyException.class, () -> mac.init(nullEncoding)); + assertThrows(InvalidKeyException.class, () -> mac.init(nullFormat)); + } + + @SuppressWarnings("serial") + @ParameterizedTest + @MethodSource("supportedHmacs") + public void incorrectKeySpecForKeyFactory(final String algorithm) throws Exception { + TestUtil.assumeMinimumVersion( + MINIMUM_VERSION_WITH_PRECOMPUTED_KEY, AmazonCorrettoCryptoProvider.INSTANCE); + + final SecretKeyFactory skf = + SecretKeyFactory.getInstance(algorithm + "WithPrecomputedKey", NATIVE_PROVIDER); + + final KeySpec nonSecretKeySpec = new KeySpec() {}; + final KeySpec badFormat = + new SecretKeySpec("yellowsubmarine".getBytes(StandardCharsets.UTF_8), "Generic") { + @Override + public String getFormat() { + return "UnexpectedFormat"; + } + }; + final KeySpec nullFormat = + new SecretKeySpec("yellowsubmarine".getBytes(StandardCharsets.UTF_8), "Generic") { + @Override + public String getFormat() { + return null; + } + }; + final KeySpec nullEncoding = + new SecretKeySpec("yellowsubmarine".getBytes(StandardCharsets.UTF_8), "Generic") { + @Override + public byte[] getEncoded() { + return null; + } + }; + + assertThrows(InvalidKeySpecException.class, () -> skf.generateSecret(badFormat)); + assertThrows(InvalidKeySpecException.class, () -> skf.generateSecret(nullEncoding)); + assertThrows(InvalidKeySpecException.class, () -> skf.generateSecret(nullFormat)); + } + @ParameterizedTest @MethodSource("supportedHmacs") public void supportsCloneable(final String algorithm) throws Exception { @@ -536,6 +674,39 @@ public void testDraggedState(final String algorithm) throws Exception { assertArraysHexEquals(expected2, duplicate.doFinal(suffix2)); } + @ParameterizedTest + @MethodSource("supportedHmacs") + // Suppress redundant cast warnings; they're redundant in java 9 but not java 8 + @SuppressWarnings({"cast", "RedundantCast"}) + public void testWithPrecomputedKey(final String algorithm) throws Exception { + TestUtil.assumeMinimumVersion(MINIMUM_VERSION_WITH_PRECOMPUTED_KEY, NATIVE_PROVIDER); + + final SecretKeySpec key = + new SecretKeySpec("YellowSubmarine".getBytes(StandardCharsets.US_ASCII), "Generic"); + final byte[] msg = "This is a test message".getBytes(StandardCharsets.US_ASCII); + final Mac jceMac = Mac.getInstance(algorithm, "SunJCE"); + jceMac.init(key); + jceMac.update(msg); + final byte[] expected = jceMac.doFinal(); + + // Compute without precomputed key (sanity check) + Mac nativeMac = Mac.getInstance(algorithm, NATIVE_PROVIDER); + nativeMac.init(key); + nativeMac.update(msg); + assertArrayEquals(expected, nativeMac.doFinal()); + + // Compute the precomputed key + SecretKeyFactory skf = + SecretKeyFactory.getInstance(algorithm + "WithPrecomputedKey", NATIVE_PROVIDER); + SecretKey precomputedKey = skf.generateSecret(key); + + // Check that the computation with the precomputed key matches + nativeMac = Mac.getInstance(algorithm + "WithPrecomputedKey", NATIVE_PROVIDER); + nativeMac.init(precomputedKey); + nativeMac.update(msg); + assertArrayEquals(expected, nativeMac.doFinal()); + } + @ParameterizedTest @MethodSource("supportedHmacs") public void selfTest(final String algorithm) throws Throwable {