Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hmac with precomputed key #396

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
geedo0 marked this conversation as resolved.
Show resolved Hide resolved

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
2 changes: 1 addition & 1 deletion csrc/hkdf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_HkdfS
if (HKDF_extract(output.get(), &out_len, digest, secret.get(), secretLen, salt.get(), saltLen) != 1) {
throw_openssl(EX_RUNTIME_CRYPTO, "HKDF_extract failed.");
}
assert(out_len == EVP_MD_size(digest) && out_len == outputLen);
assert(out_len == EVP_MD_size(digest) && outputLen >= 0 && out_len == (size_t)outputLen);

} catch (java_ex& ex) {
ex.throw_to_java(env);
Expand Down
148 changes: 135 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)
WillChilds-Klein marked this conversation as resolved.
Show resolved Hide resolved
{
#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,77 @@ 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);

HMAC_CTX ctx;
HMAC_CTX_init(&ctx);

if (unlikely(HMAC_Init_ex(&ctx,
key.get(), // key
keyLen, // keyLen
reinterpret_cast<const EVP_MD*>(evpMd), // EVP_MD
nullptr /* ENGINE */)
!= 1)) {
HMAC_CTX_cleanup(&ctx);
throw_openssl("Unable to initialize HMAC_CTX");
}

if (unlikely(HMAC_set_precomputed_key_export(&ctx) != 1)) {
HMAC_CTX_cleanup(&ctx);
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, result.get(), &actualOutputLen) != 1)) {
HMAC_CTX_cleanup(&ctx);
throw_openssl("Unable to call HMAC_get_precomputed_key");
}
if (unlikely(outputLen < 0 || (size_t)outputLen != actualOutputLen)) {
HMAC_CTX_cleanup(&ctx);
throw_java_ex(EX_ERROR, "THIS SHOULD NOT BE REACHABLE. invalid output precomputed key length.");
}

HMAC_CTX_cleanup(&ctx);
#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
Loading