Skip to content

Commit

Permalink
Support for HMAC Precomputed Key (only for non-FIPS builds)
Browse files Browse the repository at this point in the history
This commit adds support for HMAC Precomputed Keys in ACCP.
See aws/aws-lc#1574
  • Loading branch information
Fabrice Benhamouda committed Aug 28, 2024
1 parent 47e9d89 commit 8097f53
Show file tree
Hide file tree
Showing 11 changed files with 851 additions and 68 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,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 (not available in FIPS builds)
* HmacSHA384WithPrecomputedKey (not available in FIPS builds)
* HmacSHA256WithPrecomputedKey (not available in FIPS builds)
* HmacSHA1WithPrecomputedKey (not available in FIPS builds)
* HmacMD5WithPrecomputedKey (not available in FIPS builds)

# 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.
Expand Down Expand Up @@ -375,6 +381,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 `HmacXXX` 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
Expand Down
2 changes: 1 addition & 1 deletion aws-lc
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ ext.isFips = Boolean.getBoolean('FIPS')
if (ext.isFips) {
ext.awsLcGitVersionId = 'AWS-LC-FIPS-2.0.13'
} else {
ext.awsLcGitVersionId = 'v1.33.0'
ext.awsLcGitVersionId = 'v1.34.2'
}

// Check for user inputted git version ID.
Expand Down
141 changes: 128 additions & 13 deletions csrc/hmac.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@
#define DO_NOT_INIT -1
#define DO_NOT_REKEY -2

// Detect the support of precomputed keys using the fact that HMAC_SHA256_PRECOMPUTED_KEY_SIZE is only defined
// when precomputed keys are supported
#ifdef HMAC_SHA256_PRECOMPUTED_KEY_SIZE
#define HMAC_PRECOMPUTED_KEY_SUPPORT 1
#endif

using namespace AmazonCorrettoCryptoProvider;

// Some of the logic around how to manage arrays is non-standard because HMAC is extremely performance sensitive.
// 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;
Expand All @@ -33,13 +39,26 @@ 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<const EVP_MD*>(evpMd), nullptr /* ENGINE */)
!= 1)) {
throw_openssl("Unable to initialize HMAC_CTX");
if (unlikely(usePrecomputedKey)) {
#ifdef HMAC_PRECOMPUTED_KEY_SUPPORT
if (unlikely(
HMAC_Init_from_precomputed_key(ctx, key.data(), key.len(), reinterpret_cast<const EVP_MD*>(evpMd))
!= 1)) {
throw_openssl("Unable to initialize HMAC_CTX using precomputed key");
}
#else
throw_java_ex(EX_ERROR, "Precomputed keys are not supported on this platform/build");
#endif
} else {
if (unlikely(HMAC_Init_ex(
ctx, key.data(), key.len(), reinterpret_cast<const EVP_MD*>(evpMd), nullptr /* ENGINE */)
!= 1)) {
throw_openssl("Unable to initialize HMAC_CTX");
}
}
}
}
}

void update_ctx(raii_env& env, HMAC_CTX* ctx, jni_borrow& input)
{
Expand All @@ -59,6 +78,30 @@ 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)
{
#ifdef HMAC_PRECOMPUTED_KEY_SUPPORT
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.");
}
#else
throw_java_ex(EX_ERROR, "Precomputed keys are not supported on this platform/build");
#endif
return 0; // just to please the static verifier, since throw_java_ex always throws an exception
}

#ifdef __cplusplus
Expand All @@ -77,18 +120,25 @@ 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);
bounce_buffer<HMAC_CTX> ctx = bounce_buffer<HMAC_CTX>::from_array(env, ctxArr);

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);
Expand Down Expand Up @@ -119,17 +169,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 {
Expand All @@ -138,7 +189,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");
Expand All @@ -153,6 +204,70 @@ 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 {
#ifdef HMAC_PRECOMPUTED_KEY_SUPPORT
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<const EVP_MD*>(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.");
}
#else
throw_java_ex(EX_ERROR, "Precomputed keys are not supported on this platform/build");
#endif
} catch (java_ex& ex) {
ex.throw_to_java(pEnv);
}
}

#ifdef __cplusplus
}
#endif
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 8097f53

Please sign in to comment.