diff --git a/.gitmodules b/.gitmodules index 43d73ea6..c7a7a254 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "aws-lc"] path = aws-lc - url = https://github.com/awslabs/aws-lc + url = https://github.com/aws/aws-lc + branch = experimental-pq-hybrid diff --git a/CMakeLists.txt b/CMakeLists.txt index 283953e6..303745d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -261,6 +261,8 @@ add_library( csrc/env.cpp csrc/hkdf.cpp csrc/hmac.cpp + csrc/hpke_cipher.cpp + csrc/hpke_gen.cpp csrc/keyutils.cpp csrc/java_evp_keys.cpp csrc/libcrypto_rng.cpp diff --git a/README.md b/README.md index cd9ce154..18ab3a04 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +# EXPERIMENTAL BRANCH + +This branch includes an experimental implementation of HPKE. + +--- + # Amazon Corretto Crypto Provider The Amazon Corretto Crypto Provider (ACCP) is a collection of high-performance cryptographic implementations exposed via the standard [JCA/JCE](https://docs.oracle.com/en/java/javase/11/security/java-cryptography-architecture-jca-reference-guide.html) interfaces. This means that it can be used as a drop in replacement for many different Java applications. @@ -45,6 +51,7 @@ Cipher algorithms: * RSA/ECB/OAEPPadding * RSA/ECB/OAEPWithSHA-1AndMGF1Padding * RSA/ECB/OAEPWithSHA1AndMGF1Padding +* HPKE Signature algorithms: * SHA1withRSA @@ -68,6 +75,7 @@ Signature algorithms: KeyPairGenerator: * EC * RSA +* HPKE KeyGenerator: * AES diff --git a/aws-lc b/aws-lc index 4368aaa6..72c276e9 160000 --- a/aws-lc +++ b/aws-lc @@ -1 +1 @@ -Subproject commit 4368aaa6975ba41bd76d3bb12fac54c4680247fb +Subproject commit 72c276e9c709a2d9b94e41b06da6abf2b3805a4a diff --git a/build.gradle b/build.gradle index 8a49bad6..6b4b0202 100644 --- a/build.gradle +++ b/build.gradle @@ -16,9 +16,10 @@ group = 'software.amazon.cryptools' version = '2.4.1' ext.isFips = Boolean.getBoolean('FIPS') if (ext.isFips) { - ext.awsLcGitVersionId = 'AWS-LC-FIPS-2.0.13' + // TODO: replace with tags once stable + ext.awsLcGitVersionId = '72c276e9c709a2d9b94e41b06da6abf2b3805a4a' } else { - ext.awsLcGitVersionId = 'v1.30.1' + ext.awsLcGitVersionId = '72c276e9c709a2d9b94e41b06da6abf2b3805a4a' } // Check for user inputted git version ID. diff --git a/csrc/auto_free.h b/csrc/auto_free.h index 5a4de21b..ea29fdf8 100644 --- a/csrc/auto_free.h +++ b/csrc/auto_free.h @@ -6,6 +6,7 @@ #include "env.h" #include #include +#include #include #include #include @@ -103,6 +104,7 @@ OPENSSL_auto(BN_CTX); OPENSSL_auto(EVP_MD_CTX); OPENSSL_auto(EVP_PKEY); OPENSSL_auto(EVP_PKEY_CTX); +OPENSSL_auto(EVP_HPKE_KEY); class OPENSSL_buffer_auto { private: diff --git a/csrc/hpke_cipher.cpp b/csrc/hpke_cipher.cpp new file mode 100644 index 00000000..b9d20f11 --- /dev/null +++ b/csrc/hpke_cipher.cpp @@ -0,0 +1,173 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#include "auto_free.h" +#include "bn.h" +#include "buffer.h" +#include "env.h" +#include "generated-headers.h" +#include "keyutils.h" +#include "util.h" +#include +#include +#include + +using namespace AmazonCorrettoCryptoProvider; + +/* + * Class: com_amazon_corretto_crypto_provider_HpkeCipher + * Method: hpkeWrap + * Signature: (JIIIIB[IIB[IB[I)I + */ +JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_HpkeCipher_hpkeCipher(JNIEnv* pEnv, + jclass, + jlong keyHandle, + jint javaCipherMode, + jint kemId, + jint kdfId, + jint aeadId, + jbyteArray input, + jint inputOffset, + jint inputLen, + jbyteArray aad, + jint aadLen, + jbyteArray output, + jint outputOffset) +{ + try { + raii_env env(pEnv); + + if (!input) { + throw_java_ex(EX_NPE, "Empty input array"); + } + if (!output) { + throw_java_ex(EX_NPE, "Empty output array"); + } + if (inputLen < 0) { + throw_java_ex(EX_RUNTIME_CRYPTO, "Negative input length"); + } + const size_t input_length = (size_t)inputLen; + + const EVP_HPKE_KEY* key = reinterpret_cast(keyHandle); + const EVP_HPKE_KEM* kem = EVP_HPKE_KEM_find_by_id(kemId); + const EVP_HPKE_KDF* kdf = EVP_HPKE_KDF_find_by_id(kdfId); + const EVP_HPKE_AEAD* aead = EVP_HPKE_AEAD_find_by_id(aeadId); + const size_t aead_overhead = EVP_AEAD_max_overhead(EVP_HPKE_AEAD_aead(aead)); + + if (kemId != EVP_HPKE_KEM_id(EVP_HPKE_KEY_kem(key))) { + throw_java_ex(EX_RUNTIME_CRYPTO, "KEM in the key does not match the param"); + } + + std::vector info(0); + java_buffer aadBuf = java_buffer::from_array(env, aad, 0, aadLen); + + size_t result = -1; + + if ((javaCipherMode == 1 /* Encrypt */) || (javaCipherMode == 3 /* Wrap */)) { + // Serialize public key + std::vector public_key_r(EVP_HPKE_KEM_public_key_len(kem)); + size_t public_key_r_len; + CHECK_OPENSSL(EVP_HPKE_KEY_public_key(key, public_key_r.data(), &public_key_r_len, public_key_r.size())); + + // The input is the plaintext message + java_buffer msgBuf = java_buffer::from_array(env, input, inputOffset, input_length); + + // We write the enc and the ciphertext to the output buffer + const size_t encBufLen = EVP_HPKE_KEM_enc_len(kem); + const size_t ctBufLen = input_length + aead_overhead; + const size_t outBufLen = encBufLen + ctBufLen; + java_buffer encBuf = java_buffer::from_array(env, output, outputOffset, encBufLen); + java_buffer ctBuf = java_buffer::from_array(env, output, outputOffset + encBufLen, ctBufLen); + size_t enc_len = 0; + size_t ct_len = 0; + + { + jni_borrow msg(env, msgBuf, "input msg"); + jni_borrow aad(env, aadBuf, "aad"); + jni_borrow enc(env, encBuf, "output enc"); + jni_borrow ct(env, ctBuf, "output ciphertext"); + + CHECK_OPENSSL(EVP_HPKE_seal(enc.data(), &enc_len, enc.len(), ct.data(), &ct_len, ct.len(), kem, kdf, + aead, public_key_r.data(), public_key_r_len, info.data(), info.size(), msg.data(), msg.len(), + aad.data(), aad.len())); + if (enc_len != encBufLen) { + throw_java_ex(EX_RUNTIME_CRYPTO, "Unexpected error, enc buffer length is wrong!"); + } + if (ct_len != ctBufLen) { + throw_java_ex(EX_RUNTIME_CRYPTO, "Unexpected error, ciphertext buffer length is wrong!"); + } + result = outBufLen; + } + } else if ((javaCipherMode == 2 /* Decrypt */) || (javaCipherMode == 4 /* Unwrap */)) { + // The input is the enc and the ciphertext + const size_t encBufLen = EVP_HPKE_KEM_enc_len(kem); + if (input_length < (encBufLen + aead_overhead)) { + throw_java_ex(EX_RUNTIME_CRYPTO, "input too short to unwrap with HPKE"); + } + const size_t ctBufLen = input_length - encBufLen; + java_buffer encBuf = java_buffer::from_array(env, input, inputOffset, encBufLen); + java_buffer ctBuf = java_buffer::from_array(env, input, inputOffset + encBufLen, ctBufLen); + + // We write the plaintext message to the output buffer + java_buffer msgBuf = java_buffer::from_array(env, output, outputOffset); + size_t msg_len = 0; + { + jni_borrow enc(env, encBuf, "input enc"); + jni_borrow ct(env, ctBuf, "input ciphertext"); + jni_borrow aad(env, aadBuf, "aad"); + jni_borrow msg(env, msgBuf, "output msg"); + + CHECK_OPENSSL(EVP_HPKE_open(msg.data(), &msg_len, msg.len(), key, kdf, aead, enc.data(), enc.len(), + info.data(), info.size(), ct.data(), ct.len(), aad.data(), aad.len())) + result = msg_len; + } + } else { + throw_java_ex(EX_RUNTIME_CRYPTO, "Unsupported cipher mode"); + } + return (jint)result; + } catch (java_ex& ex) { + ex.throw_to_java(pEnv); + return -1; + } +} + +/* + * Class: com_amazon_corretto_crypto_provider_HpkeCipher + * Method: hpkeOutputSize + * Signature: (IIIII)I + */ +JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_HpkeCipher_hpkeOutputSize( + JNIEnv* pEnv, jclass, jint javaCipherMode, jint kemId, jint kdfId, jint aeadId, jint inputLen) +{ + const EVP_HPKE_KEM* kem = EVP_HPKE_KEM_find_by_id(kemId); + const EVP_HPKE_AEAD* aead = EVP_HPKE_AEAD_find_by_id(aeadId); + const size_t aead_overhead = EVP_AEAD_max_overhead(EVP_HPKE_AEAD_aead(aead)); + const size_t enc_len = EVP_HPKE_KEM_enc_len(kem); + + try { + raii_env env(pEnv); + + if (inputLen < 0) { + throw_java_ex(EX_RUNTIME_CRYPTO, "negative input length"); + } + const size_t input_length = (size_t)inputLen; + + size_t ret = -1; + + if ((javaCipherMode == 1 /* Encrypt */) || (javaCipherMode == 3 /* Wrap */)) { + // We write the enc and the ciphertext to the output buffer + ret = input_length + enc_len + aead_overhead; + } else if ((javaCipherMode == 2 /* Decrypt */) || (javaCipherMode == 4 /* Unwrap */)) { + // We write the plaintext to the output buffer + if (input_length < (enc_len + aead_overhead)) { + throw_java_ex(EX_RUNTIME_CRYPTO, "input too short to unwrap with HPKE"); + } + ret = (input_length - enc_len - aead_overhead); + } else { + throw_java_ex(EX_RUNTIME_CRYPTO, "Unsupported cipher mode"); + } + return (jint)ret; + } catch (java_ex& ex) { + ex.throw_to_java(pEnv); + return -1; + } +} diff --git a/csrc/hpke_gen.cpp b/csrc/hpke_gen.cpp new file mode 100644 index 00000000..9a8d6d56 --- /dev/null +++ b/csrc/hpke_gen.cpp @@ -0,0 +1,36 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#include "auto_free.h" +#include "bn.h" +#include "buffer.h" +#include "env.h" +#include "generated-headers.h" +#include "keyutils.h" +#include "util.h" +#include +#include +#include + +using namespace AmazonCorrettoCryptoProvider; + +/* + * Class: com_amazon_corretto_crypto_provider_HpkeGen + * Method: generateEvpHpkeKemKeyFromSpec + * Signature: (I)J + */ +JNIEXPORT jlong JNICALL Java_com_amazon_corretto_crypto_provider_HpkeGen_generateEvpHpkeKemKeyFromSpec( + JNIEnv* pEnv, jclass, jint hpke_kem_id) +{ + EVP_HPKE_KEY_auto key; + try { + raii_env env(pEnv); + key.set(EVP_HPKE_KEY_new()); + const EVP_HPKE_KEM* kem = EVP_HPKE_KEM_find_by_id(hpke_kem_id); + CHECK_OPENSSL(kem != NULL); + CHECK_OPENSSL(EVP_HPKE_KEY_generate(key, kem)); + return reinterpret_cast(key.take()); + } catch (java_ex& ex) { + ex.throw_to_java(pEnv); + return 0; + } +} \ No newline at end of file diff --git a/csrc/keyutils.cpp b/csrc/keyutils.cpp index c1a0d244..8f65f5c0 100644 --- a/csrc/keyutils.cpp +++ b/csrc/keyutils.cpp @@ -170,25 +170,6 @@ const EVP_MD* digestFromJstring(raii_env& env, jstring digestName) RSA* new_private_RSA_key_with_no_e(BIGNUM const* n, BIGNUM const* d) { -#ifdef FIPS_BUILD - // AWS-LC-FIPS doesn't have RSA_new_private_key_no_e method yet. - // The following implementation has been copied from AWS-LC: - // https://github.com/aws/aws-lc/blob/v1.30.1/crypto/fipsmodule/rsa/rsa.c#L147 - RSA_auto rsa = RSA_auto::from(RSA_new()); - if (rsa.get() == nullptr) { - throw_openssl("RSA_new failed"); - } - - // RSA struct is not opaque in FIPS mode. - rsa->flags |= RSA_FLAG_NO_BLINDING; - - bn_dup_into(&rsa->n, n); - bn_dup_into(&rsa->d, d); - - return rsa.take(); - -#else - RSA* result = RSA_new_private_key_no_e(n, d); if (result == nullptr) { @@ -196,8 +177,6 @@ RSA* new_private_RSA_key_with_no_e(BIGNUM const* n, BIGNUM const* d) } return result; - -#endif } } diff --git a/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java b/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java index eb646098..327653b0 100644 --- a/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java +++ b/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java @@ -152,6 +152,12 @@ private void buildServiceMap() { } addSignatures(); + addHPKE(); + } + + private void addHPKE() { + addService("KeyPairGenerator", "HPKE", "HpkeGen"); + addService("Cipher", "HPKE", "HpkeCipher"); } private void addSignatures() { diff --git a/src/com/amazon/corretto/crypto/provider/EvpHpkeKey.java b/src/com/amazon/corretto/crypto/provider/EvpHpkeKey.java new file mode 100644 index 00000000..c135aaaa --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/EvpHpkeKey.java @@ -0,0 +1,18 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider; + +class EvpHpkeKey extends EvpKey { + private static final long serialVersionUID = 1; + + final HpkeParameterSpec spec; + + EvpHpkeKey(final InternalKey key, final boolean isPublicKey, HpkeParameterSpec spec) { + super(key, EvpKeyType.HPKE, isPublicKey); + this.spec = spec; + } + + public HpkeParameterSpec getSpec() { + return spec; + } +} diff --git a/src/com/amazon/corretto/crypto/provider/EvpHpkePrivateKey.java b/src/com/amazon/corretto/crypto/provider/EvpHpkePrivateKey.java new file mode 100644 index 00000000..c690e109 --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/EvpHpkePrivateKey.java @@ -0,0 +1,29 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider; + +import com.amazon.corretto.crypto.provider.EvpKey.CanDerivePublicKey; +import java.security.PrivateKey; + +public class EvpHpkePrivateKey extends EvpHpkeKey + implements PrivateKey, CanDerivePublicKey { + private static final long serialVersionUID = 1; + + EvpHpkePrivateKey(InternalKey key, HpkeParameterSpec spec) { + super(key, false, spec); + } + + EvpHpkePrivateKey(final long ptr, HpkeParameterSpec spec) { + this(new InternalKey(ptr), spec); + } + + @Override + public EvpHpkePublicKey getPublicKey() { + // Once our internal key could be elsewhere, we can no longer safely release it when done + ephemeral = false; + sharedKey = true; + final EvpHpkePublicKey result = new EvpHpkePublicKey(internalKey, spec); + result.sharedKey = true; + return result; + } +} diff --git a/src/com/amazon/corretto/crypto/provider/EvpHpkePublicKey.java b/src/com/amazon/corretto/crypto/provider/EvpHpkePublicKey.java new file mode 100644 index 00000000..a71166ca --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/EvpHpkePublicKey.java @@ -0,0 +1,13 @@ +// 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.PublicKey; + +public class EvpHpkePublicKey extends EvpHpkeKey implements PublicKey { + private static final long serialVersionUID = 1; + + EvpHpkePublicKey(InternalKey key, HpkeParameterSpec spec) { + super(key, true, spec); + } +} diff --git a/src/com/amazon/corretto/crypto/provider/EvpKeyType.java b/src/com/amazon/corretto/crypto/provider/EvpKeyType.java index c5fe919a..cc9da98a 100644 --- a/src/com/amazon/corretto/crypto/provider/EvpKeyType.java +++ b/src/com/amazon/corretto/crypto/provider/EvpKeyType.java @@ -17,7 +17,8 @@ /** Corresponds to native constants in OpenSSL which represent keytypes. */ enum EvpKeyType { RSA("RSA", 6, RSAPublicKey.class, RSAPrivateKey.class), - EC("EC", 408, ECPublicKey.class, ECPrivateKey.class); + EC("EC", 408, ECPublicKey.class, ECPrivateKey.class), + HPKE("HPKE", 991, EvpHpkePublicKey.class, EvpHpkePrivateKey.class); final String jceName; final int nativeValue; diff --git a/src/com/amazon/corretto/crypto/provider/HpkeCipher.java b/src/com/amazon/corretto/crypto/provider/HpkeCipher.java new file mode 100644 index 00000000..d07fe0c0 --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/HpkeCipher.java @@ -0,0 +1,406 @@ +// 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.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; + +public class HpkeCipher extends CipherSpi { + + static { + Loader.load(); + } + + private final AmazonCorrettoCryptoProvider provider_; + private final Object lock_ = new Object(); + + private int javaCipherMode_ = -1; + private EvpHpkeKey key_; + private HpkeParameterSpec params_; + + /** tracks whether the cipher has been used (to encrypt, decrypt, wrap, or unwrap) */ + private boolean finalized_ = false; + + private final AccessibleByteArrayOutputStream aadBuffer_; + + HpkeCipher(final AmazonCorrettoCryptoProvider provider) { + Loader.checkNativeLibraryAvailability(); + this.provider_ = provider; + aadBuffer_ = new AccessibleByteArrayOutputStream(); + } + + // Core Native Methods + // ------------------- + + /** + * Performs single-shot HPKE encryption or decryption, as specified in Section 6.1 of RFC 9180. + * + * @return number of bytes written to output, and -1 if failed. + */ + private static native int hpkeCipher( + long keyHandle, + int javaCipherMode, + int kemId, + int KdfId, + int aeadId, + byte[] input, + int inputOffset, + int inputLen, + byte[] aad, + int aadLen, + byte[] output, + int outputOffset); + + /** + * Computes the number of bytes the output buffer needs to be for wrapping and unwrapping. + * + *

For wrapping, the size is greater than the input buffer since the output buffer also needs + * to include an AEAD tag and KEM encapsulate. + * + *

For unwrapping, the size is smaller, since the output does not need the AEAD tag or the KEM + * encapsulate. + */ + private static native int hpkeOutputSize( + int javaCipherMode, int kemId, int KdfId, int aeadId, int inputLen); + + // Core Java Methods + // ----------------- + + private static void checkModeKeyParams(int opmode, Key key, AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if ((opmode != Cipher.WRAP_MODE) + && (opmode != Cipher.UNWRAP_MODE) + && (opmode != Cipher.ENCRYPT_MODE) + && (opmode != Cipher.DECRYPT_MODE)) { + throw new IllegalStateException( + "HpkeCipher only supports WRAP_MODE, UNWRAP_MODE, ENCRYPT_MODE, and DECRYPT_MODE."); + } + if (key == null) { + throw new IllegalStateException("HpkeCipher does not support a null key."); + } + if (params == null) { + throw new InvalidAlgorithmParameterException( + "HpkeCipher does not support a null parameters."); + } + if (!(params instanceof HpkeParameterSpec)) { + throw new InvalidAlgorithmParameterException( + "HpkeCipher only supports HpkeParameterSpec parameters."); + } + if (!(key instanceof EvpHpkeKey)) { + throw new InvalidKeyException("HpkeCipher only supports EvpHpkeKey keys."); + } + if (((EvpHpkeKey) key).getSpec() != params) { + throw new InvalidKeyException("Spec of key does not match the params provided"); + } + if (key instanceof EvpHpkePublicKey) { + if ((opmode != Cipher.WRAP_MODE) && (opmode != Cipher.ENCRYPT_MODE)) { + throw new IllegalStateException("Need PublicKey to wrap and encrypt"); + } + } else if (key instanceof EvpHpkePrivateKey) { + if ((opmode != Cipher.UNWRAP_MODE) && (opmode != Cipher.DECRYPT_MODE)) { + throw new IllegalStateException("Need PrivateKey to unwrap and decrypt"); + } + } else { + throw new InvalidKeyException( + "HpkeCipher only supports EvpHpkePublicKey and EvpHpkePrivate key types, given: " + + key.getClass()); + } + } + + /** + * Initializes a HPKE instance. + * + * @param opmode cipher mode, one of ENCRYPT, DECRYPT, WRAP, and UNWRAP + * @param key must be an instance of EvpHpkePublicKey for ENCRYPT and WRAP, and EvpHpkePrivateKey + * for DECRYPT and UNWRAP + * @param params the algorithm parameters, must be an instance of HpkeParameterSpec, and may not + * be null. + * @param random a source of randomness, which we disregard, may be null. + */ + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + checkModeKeyParams(opmode, key, params); + synchronized (lock_) { + javaCipherMode_ = opmode; + if ((opmode == Cipher.UNWRAP_MODE) || (opmode == Cipher.DECRYPT_MODE)) { + key_ = (EvpHpkePrivateKey) key; + } else { + key_ = (EvpHpkePublicKey) key; + } + params_ = (HpkeParameterSpec) params; + finalized_ = false; + aadBuffer_.reset(); + } + } + + /** + * Internal method to perform single-shot HPKE encryption or decryption. + * + *

The underlying encrypt and decrypt functions are specified in Section 6.1 of RFC 9180. + * + *

For encryption, the output concatenates the encapsulated key and the ciphertext. + * + *

For decryption, the input ciphertext must be a concatenation of the key and the ciphertext. + * + * @return number of bytes written to output + */ + private int internalOneShotHpke( + byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) + throws ShortBufferException { + finalized_ = true; // cannot update AAD after this function is called + if ((outputOffset > output.length) + || (inputOffset > input.length) + || ((inputOffset + inputLen) > input.length)) { + throw new ArrayIndexOutOfBoundsException(); + } + if (engineGetOutputSize(inputLen) > (output.length - outputOffset)) { + throw new ShortBufferException(); + } + // if input will overlap with output, then copy input to a new buffer + if ((input == output) && ((inputOffset + engineGetOutputSize(inputLen)) > outputOffset)) { + byte[] tmp = new byte[inputLen]; + System.arraycopy(input, inputOffset, tmp, 0, inputLen); + input = tmp; + inputOffset = 0; + } + synchronized (lock_) { + try { + checkModeKeyParams(javaCipherMode_, key_, params_); + } catch (InvalidAlgorithmParameterException | InvalidKeyException e) { + throw new IllegalStateException(e); + } + + byte[] aad = aadBuffer_.getDataBuffer(); + int aadLen = aadBuffer_.size(); + + final byte[] finalInput = input; + final int finalInputOffset = inputOffset; + return key_.use( + ptr -> + hpkeCipher( + ptr, + javaCipherMode_, + params_.getKemId(), + params_.getKdfId(), + params_.getAeadId(), + finalInput, + finalInputOffset, + inputLen, + aad, + aadLen, + output, + outputOffset)); + } + } + + /** + * Wrapper around {@link #internalOneShotHpke(byte[], int, int, byte[], int) internalOneShotHpke}, + * to return a new output buffer. + */ + byte[] internalOneShotHpke(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + byte[] output = new byte[engineGetOutputSize(inputLen)]; + try { + int len = internalOneShotHpke(input, inputOffset, inputLen, output, 0); + if (len != output.length) { + throw new RuntimeCryptoException( + "HpkeCipher expected output of length " + output.length + ", got output of len " + len); + } + return output; + } catch (ShortBufferException e) { + throw new RuntimeException(e); + } + } + + /** + * Supply AAD to HPKE. + * + *

Must be called before the cipher is used, i.e., before DoFinal, Wrap, or Unwrap. + */ + @Override + protected void engineUpdateAAD(byte[] src, int offset, int len) { + synchronized (lock_) { + if (finalized_) { + throw new IllegalStateException( + "Cannot update AAD after HpkeCipher has been used to Wrap, Unwrap, Encrypt, or" + + " Decrypt"); + } + aadBuffer_.write(src, offset, len); + } + } + + /** + * Wraps key using HPKE single-shot encryption. + * + *

Provided key must be an instance of SecretKey, PrivateKey, or PublicKey. SecretKey are + * encoded with their native encoding, PrivateKeys are encoded using PKCS8, and PublicKeys are + * encoded using X509. + * + * @return concatenation of KEM encapsulated key and encrypted encoded key + */ + @Override + protected byte[] engineWrap(Key key) throws IllegalBlockSizeException, InvalidKeyException { + if (javaCipherMode_ != Cipher.WRAP_MODE) { + throw new IllegalStateException("Cipher must be in WRAP_MODE"); + } + try { + final byte[] encoded = Utils.encodeForWrapping(provider_, key); + return internalOneShotHpke(encoded, 0, encoded.length); + } catch (final BadPaddingException e) { + throw new InvalidKeyException("Failed to wrap key", e); + } + } + + /** + * Unwraps key using HPKE single-shot decryption. + * + *

The encrypted key must be an instance of SecretKey, PrivateKey, or PublicKey. SecretKeys + * must be encoded as the raw key, PrivateKeys must be encoded using PKCS8, and PublicKeys must be + * encoded using X509. + * + * @param wrappedKey concatenation of the KEM encapsulated key and the encrypted encoded key + * @return decrypted key + */ + @Override + protected Key engineUnwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) + throws InvalidKeyException, NoSuchAlgorithmException { + if (javaCipherMode_ != Cipher.UNWRAP_MODE) { + throw new IllegalStateException("Cipher must be in UNWRAP_MODE"); + } + try { + final byte[] unwrappedKey = internalOneShotHpke(wrappedKey, 0, wrappedKey.length); + return Utils.buildUnwrappedKey(provider_, unwrappedKey, wrappedKeyAlgorithm, wrappedKeyType); + } catch (final BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException e) { + throw new InvalidKeyException("Failed to unwrap key", e); + } + } + + /** + * Performs HPKE single-shot encryption or decryption, as specified in Section 6.1 of RFC 9180. + * + *

For encryption, the output concatenates the encapsulated key and the ciphertext. + * + *

For decryption, the input ciphertext must be a concatenation of the key and the ciphertext. + * + * @return number of bytes written to output + */ + @Override + protected int engineDoFinal( + byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) + throws ShortBufferException { + if ((javaCipherMode_ != Cipher.ENCRYPT_MODE) && (javaCipherMode_ != Cipher.DECRYPT_MODE)) { + throw new IllegalStateException("cipher must be in ENCRYPT_MODE or DECRYPT_MODE"); + } + return internalOneShotHpke(input, inputOffset, inputLen, output, outputOffset); + } + + @Override + protected int engineGetOutputSize(int inputLen) { + synchronized (lock_) { + try { + checkModeKeyParams(javaCipherMode_, key_, params_); + return hpkeOutputSize( + javaCipherMode_, params_.getKemId(), params_.getKdfId(), params_.getAeadId(), inputLen); + } catch (InvalidAlgorithmParameterException | InvalidKeyException e) { + throw new IllegalStateException(e); + } + } + } + + // Boilerplate Methods + // ------------------- + + @Override + protected AlgorithmParameters engineGetParameters() { + AlgorithmParameters params; + try { + params = AlgorithmParameters.getInstance("HPKE"); + params.init(params_); + return params; + } catch (NoSuchAlgorithmException | InvalidParameterSpecException e) { + throw new RuntimeException(e); + } + } + + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + return internalOneShotHpke(input, inputOffset, inputLen); + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + if (params == null) { + throw new InvalidAlgorithmParameterException("cannot initialize HpkeCipher with null params"); + } + try { + AlgorithmParameterSpec spec = params.getParameterSpec(HpkeParameterSpec.class); + engineInit(opmode, key, spec, random); + } catch (InvalidParameterSpecException e) { + throw new InvalidAlgorithmParameterException(e); + } + } + + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException { + if (key instanceof EvpHpkeKey) { + try { + engineInit(opmode, key, ((EvpHpkeKey) key).getSpec(), random); + } catch (InvalidAlgorithmParameterException e) { + throw new InvalidKeyException(e); + } + } else { + throw new InvalidKeyException("HpkeCipher can only be initialized with EvpHpkeKey."); + } + } + + // Unsupported Methods + // ------------------- + + @Override + protected void engineSetMode(String mode) throws NoSuchAlgorithmException { + throw new NoSuchAlgorithmException("HpkeCipher does not support modes"); + } + + @Override + protected void engineSetPadding(String padding) throws NoSuchPaddingException { + throw new NoSuchPaddingException("HpkeCipher does not support padding"); + } + + @Override + protected int engineGetBlockSize() { + throw new IllegalStateException("HpkeCipher does not support block sizes"); + } + + @Override + protected byte[] engineGetIV() { + throw new IllegalStateException("HpkeCipher does not support IVs"); + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + throw new IllegalStateException("HpkeCipher currently does not support updates"); + } + + @Override + protected int engineUpdate( + byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) + throws ShortBufferException { + throw new IllegalStateException("HpkeCipher currently does not support updates"); + } +} diff --git a/src/com/amazon/corretto/crypto/provider/HpkeGen.java b/src/com/amazon/corretto/crypto/provider/HpkeGen.java new file mode 100644 index 00000000..26e2bb36 --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/HpkeGen.java @@ -0,0 +1,59 @@ +// 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.InvalidAlgorithmParameterException; +import java.security.InvalidParameterException; +import java.security.KeyPair; +import java.security.KeyPairGeneratorSpi; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +public class HpkeGen extends KeyPairGeneratorSpi { + private final AmazonCorrettoCryptoProvider provider_; + private HpkeParameterSpec spec = null; + + HpkeGen(AmazonCorrettoCryptoProvider provider) { + Loader.checkNativeLibraryAvailability(); + provider_ = provider; + } + + /** + * Generates a new HPKE key and returns a pointer to it. + * + * @param hpke_kem_id HPKE KEM ID defined in Table 2 of RFC 9180 + * @return native pointer to a newly allocated EVP_HPKE_KEY structure, does not need to be + * explicitly freed when wrapped inside InternalKey (or EvpHpkeKey) which calls OPENSSL_free + * when destroyed. + */ + private static native long generateEvpHpkeKemKeyFromSpec(int hpke_kem_id); + + @Override + public void initialize(final AlgorithmParameterSpec params, final SecureRandom rnd) + throws InvalidAlgorithmParameterException { + if (params instanceof HpkeParameterSpec) { + // TODO: do validation + spec = (HpkeParameterSpec) params; + } else { + throw new InvalidAlgorithmParameterException("Unsupported AlgorithmParameterSpec: " + spec); + } + } + + @Override + public void initialize(final int keysize, final SecureRandom rnd) + throws InvalidParameterException { + throw new InvalidParameterException( + "Cannot initialize a KEM key with keysize, must use AlgorithmParameterSpec."); + } + + @Override + public KeyPair generateKeyPair() { + if (spec == null) { + throw new InvalidParameterException("Spec not initialized"); + } + final EvpHpkePrivateKey privateKey = + new EvpHpkePrivateKey(generateEvpHpkeKemKeyFromSpec(spec.getKemId()), spec); + final EvpHpkePublicKey publicKey = privateKey.getPublicKey(); + return new KeyPair(publicKey, privateKey); + } +} diff --git a/src/com/amazon/corretto/crypto/provider/HpkeParameterSpec.java b/src/com/amazon/corretto/crypto/provider/HpkeParameterSpec.java new file mode 100644 index 00000000..20eb1bb8 --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/HpkeParameterSpec.java @@ -0,0 +1,84 @@ +// 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.AlgorithmParameterSpec; + +public class HpkeParameterSpec implements AlgorithmParameterSpec { + + // Selected ciphersuites from RFC 9180 + + private static final int mode_base = 0x00; + + private static final int kem_x25519 = 0x0020; + private static final int kem_mlkem768 = 0xff01; + private static final int kem_mlkem1024 = 0xff02; + private static final int kem_pqt25519 = 0xff03; + private static final int kem_pqt256 = 0xff04; + private static final int kem_pqt384 = 0xff05; + + private static final int kdf_hkdf_sha256 = 0x0001; + private static final int kdf_hkdf_sha384 = 0x0002; + + private static final int aead_aes128_gcm = 0x0001; + private static final int aead_aes256_gcm = 0x0002; + private static final int aead_chacha20_poly1305 = 0x0003; + + /** Base mode, DHKEM-X25519, HKDF-SHA256, AES128-GCM */ + public static final HpkeParameterSpec X25519Sha256Aes128gcm = + new HpkeParameterSpec(mode_base, kem_x25519, kdf_hkdf_sha256, aead_aes128_gcm); + /** Base mode, DHKEM-X25519, HKDF-SHA256, ChaCha20/Poly1305 z */ + public static final HpkeParameterSpec X25519Sha256Chapoly = + new HpkeParameterSpec(mode_base, kem_x25519, kdf_hkdf_sha256, aead_chacha20_poly1305); + + // Selected PQ and PQ/T ciphersuites (experimental) + + /** Base mode, HPKE-MLKEM768, HKDF-SHA256, AES256-GCM */ + public static final HpkeParameterSpec Mlkem768Sha256Aes256gcm = + new HpkeParameterSpec(mode_base, kem_mlkem768, kdf_hkdf_sha256, aead_aes256_gcm); + /** Base mode, HPKE-MLKEM1024, HKDF-SHA384, AES256-GCM */ + public static final HpkeParameterSpec Mlkem1024Sha384Aes256gcm = + new HpkeParameterSpec(mode_base, kem_mlkem1024, kdf_hkdf_sha384, aead_aes256_gcm); + + /** Base mode, HPKE-PQT25519, HKDF-SHA256, AES256-GCM */ + public static final HpkeParameterSpec Pqt25519Sha256Aes256gcm = + new HpkeParameterSpec(mode_base, kem_pqt25519, kdf_hkdf_sha256, aead_aes256_gcm); + /** Base mode, HPKE-PQT256, HKDF-SHA256, AES256-GCM */ + public static final HpkeParameterSpec Pqt256Sha256Aes256gcm = + new HpkeParameterSpec(mode_base, kem_pqt256, kdf_hkdf_sha256, aead_aes256_gcm); + /** Base mode, HPKE-PQT384, HKDF-SHA384, AES256-GCM */ + public static final HpkeParameterSpec Pqt384Sha384Aes256gcm = + new HpkeParameterSpec(mode_base, kem_pqt384, kdf_hkdf_sha384, aead_aes256_gcm); + + /** HPKE mode, defined in Table 1 of RFC 9180 */ + private final int mode; + /** HPKE KEM ID, defined in Table 2 of RFC 9180 */ + private final int kemId; + /** HPKE KDF ID, defined in Table 3 of RFC 9180 */ + private final int kdfId; + /** HPKE AEAD ID, defined in Table 5 of RFC 9180 */ + private final int aeadId; + + private HpkeParameterSpec(int mode, int kemId, int kdfId, int aeadId) { + this.mode = mode; + this.kemId = kemId; + this.kdfId = kdfId; + this.aeadId = aeadId; + } + + public int getMode() { + return mode; + } + + public int getKemId() { + return kemId; + } + + public int getKdfId() { + return kdfId; + } + + public int getAeadId() { + return aeadId; + } +} diff --git a/tst/com/amazon/corretto/crypto/provider/test/HpkeCipherTest.java b/tst/com/amazon/corretto/crypto/provider/test/HpkeCipherTest.java new file mode 100644 index 00000000..03bf7fe8 --- /dev/null +++ b/tst/com/amazon/corretto/crypto/provider/test/HpkeCipherTest.java @@ -0,0 +1,171 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.amazon.corretto.crypto.provider.HpkeParameterSpec; +import java.security.*; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.List; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(TestResultLogger.class) +@Execution(ExecutionMode.CONCURRENT) +@ResourceLock(value = TestUtil.RESOURCE_GLOBAL, mode = ResourceAccessMode.READ) +public class HpkeCipherTest { + + static List namedSpecs() { + return Arrays.asList( + HpkeParameterSpec.X25519Sha256Aes128gcm, + HpkeParameterSpec.X25519Sha256Chapoly, + HpkeParameterSpec.Mlkem768Sha256Aes256gcm, + HpkeParameterSpec.Mlkem1024Sha384Aes256gcm, + HpkeParameterSpec.Pqt25519Sha256Aes256gcm, + HpkeParameterSpec.Pqt256Sha256Aes256gcm, + HpkeParameterSpec.Pqt384Sha384Aes256gcm); + } + + private KeyPairGenerator getGenerator() throws GeneralSecurityException { + return KeyPairGenerator.getInstance("HPKE", TestUtil.NATIVE_PROVIDER); + } + + private KeyPair getKeyPair(HpkeParameterSpec spec) throws GeneralSecurityException { + final KeyPairGenerator generator = getGenerator(); + generator.initialize(spec); + return generator.generateKeyPair(); + } + + private static Cipher getCipher() throws GeneralSecurityException { + return Cipher.getInstance("HPKE", TestUtil.NATIVE_PROVIDER); + } + + private static Cipher getInitCipher(KeyPair keyPair, int opmode) throws GeneralSecurityException { + Cipher cipher = getCipher(); + if ((opmode == Cipher.ENCRYPT_MODE) || (opmode == Cipher.WRAP_MODE)) { + cipher.init(opmode, keyPair.getPublic()); + } else if ((opmode == Cipher.DECRYPT_MODE) || (opmode == Cipher.UNWRAP_MODE)) { + cipher.init(opmode, keyPair.getPrivate()); + } + return cipher; + } + + @ParameterizedTest + @MethodSource("namedSpecs") + public void basicCorrectness(HpkeParameterSpec spec) throws GeneralSecurityException { + // Generate a key pair + final KeyPair keyPair = getKeyPair(spec); + assertNotNull(keyPair.getPublic()); + assertNotNull(keyPair.getPrivate()); + + // Initialize ciphers + final Cipher encryptCipher = getInitCipher(keyPair, Cipher.ENCRYPT_MODE); + final Cipher decryptCipher = getInitCipher(keyPair, Cipher.DECRYPT_MODE); + final Cipher wrapCipher = getInitCipher(keyPair, Cipher.WRAP_MODE); + final Cipher unwrapCipher = getInitCipher(keyPair, Cipher.UNWRAP_MODE); + + // Test encrypting data with aad + final byte[] message = TestUtil.arrayOf((byte) 0x42, 42); + final byte[] aad1 = TestUtil.arrayOf((byte) 0x24, 24); + final byte[] aad2 = TestUtil.arrayOf((byte) 0x12, 12); + encryptCipher.updateAAD(aad1, 0, aad1.length); + encryptCipher.updateAAD(aad2, 0, aad2.length); + final byte[] ciphertext = encryptCipher.doFinal(message); + decryptCipher.updateAAD(aad1, 0, aad1.length); + decryptCipher.updateAAD(aad2, 0, aad2.length); + final byte[] decrypted = decryptCipher.doFinal(ciphertext); + assertArrayEquals(message, decrypted); + + // Test wrapping AES key + final SecretKeySpec aesKey = new SecretKeySpec(TestUtil.getRandomBytes(16), "AES"); + final SecretKey unwrappedSecretKey = + (SecretKey) unwrapCipher.unwrap(wrapCipher.wrap(aesKey), "AES", Cipher.SECRET_KEY); + assertEquals(aesKey.getAlgorithm(), unwrappedSecretKey.getAlgorithm()); + assertArrayEquals(aesKey.getEncoded(), unwrappedSecretKey.getEncoded()); + + // Test wrapping RSA keys + final KeyPairGenerator rsaGenerator = KeyPairGenerator.getInstance("RSA"); + rsaGenerator.initialize(4096); + final KeyPair rsaKeyPair = rsaGenerator.generateKeyPair(); + final PublicKey rsaPublicKey = rsaKeyPair.getPublic(); + final PublicKey unwrappedRsaPublicKey = + (PublicKey) unwrapCipher.unwrap(wrapCipher.wrap(rsaPublicKey), "RSA", Cipher.PUBLIC_KEY); + assertEquals(rsaPublicKey.getAlgorithm(), unwrappedRsaPublicKey.getAlgorithm()); + assertArrayEquals(rsaPublicKey.getEncoded(), unwrappedRsaPublicKey.getEncoded()); + final PrivateKey rsaPrivateKey = rsaKeyPair.getPrivate(); + final PrivateKey unwrappedRsaPrivateKey = + (PrivateKey) unwrapCipher.unwrap(wrapCipher.wrap(rsaPrivateKey), "RSA", Cipher.PRIVATE_KEY); + assertEquals(rsaPrivateKey.getAlgorithm(), unwrappedRsaPrivateKey.getAlgorithm()); + assertArrayEquals(rsaPrivateKey.getEncoded(), unwrappedRsaPrivateKey.getEncoded()); + + // Test wrapping EC keys + final KeyPairGenerator ecGenerator = KeyPairGenerator.getInstance("EC"); + ecGenerator.initialize(new ECGenParameterSpec("NIST P-384")); + final KeyPair ecKeyPair = ecGenerator.generateKeyPair(); + final PublicKey ecPublicKey = ecKeyPair.getPublic(); + final PublicKey unwrappedEcPublicKey = + (PublicKey) unwrapCipher.unwrap(wrapCipher.wrap(ecPublicKey), "EC", Cipher.PUBLIC_KEY); + assertEquals(ecPublicKey.getAlgorithm(), unwrappedEcPublicKey.getAlgorithm()); + assertArrayEquals(ecPublicKey.getEncoded(), unwrappedEcPublicKey.getEncoded()); + final PrivateKey ecPrivateKey = ecKeyPair.getPrivate(); + final PrivateKey unwrappedEcPrivateKey = + (PrivateKey) unwrapCipher.unwrap(wrapCipher.wrap(ecPrivateKey), "EC", Cipher.PRIVATE_KEY); + assertEquals(ecPrivateKey.getAlgorithm(), unwrappedEcPrivateKey.getAlgorithm()); + assertArrayEquals(ecPrivateKey.getEncoded(), unwrappedEcPrivateKey.getEncoded()); + } + + @Test + public void invalidUses() throws GeneralSecurityException { + final HpkeParameterSpec spec = HpkeParameterSpec.Mlkem768Sha256Aes256gcm; + final KeyPair keyPair = getKeyPair(spec); + final byte[] input = TestUtil.arrayOf((byte) 0x42, 42); + + final Cipher wrapCipher = getInitCipher(keyPair, Cipher.WRAP_MODE); + final Cipher unwrapCipher = getInitCipher(keyPair, Cipher.UNWRAP_MODE); + TestUtil.assertThrows(IllegalStateException.class, () -> wrapCipher.doFinal(input)); + TestUtil.assertThrows(IllegalStateException.class, () -> unwrapCipher.doFinal(input)); + + final SecretKeySpec aesKey = new SecretKeySpec(TestUtil.getRandomBytes(16), "AES"); + final Cipher encryptCipher = getInitCipher(keyPair, Cipher.ENCRYPT_MODE); + final Cipher decryptCipher = getInitCipher(keyPair, Cipher.DECRYPT_MODE); + TestUtil.assertThrows(IllegalStateException.class, () -> encryptCipher.wrap(aesKey)); + TestUtil.assertThrows( + IllegalStateException.class, () -> decryptCipher.unwrap(input, "AES", Cipher.SECRET_KEY)); + } + + @Test + public void overlappingInputAndOutput() throws GeneralSecurityException { + final HpkeParameterSpec spec = HpkeParameterSpec.Mlkem768Sha256Aes256gcm; + final KeyPair keyPair = getKeyPair(spec); + final Cipher encryptCipher = getInitCipher(keyPair, Cipher.ENCRYPT_MODE); + final Cipher decryptCipher = getInitCipher(keyPair, Cipher.DECRYPT_MODE); + + final byte[] msg = TestUtil.arrayOf((byte) 0x42, 42); + + // in-place encrypt and decrypt on buf + byte[] buf = new byte[encryptCipher.getOutputSize(msg.length)]; + System.arraycopy(msg, 0, buf, 0, msg.length); + + encryptCipher.doFinal(buf, 0, msg.length, buf, 0); + assertFalse(Arrays.equals(msg, Arrays.copyOfRange(buf, 0, msg.length))); + decryptCipher.doFinal(buf, 0, buf.length, buf, 0); + assertArrayEquals(msg, Arrays.copyOfRange(buf, 0, msg.length)); + } +} diff --git a/tst/com/amazon/corretto/crypto/provider/test/HpkeGenTest.java b/tst/com/amazon/corretto/crypto/provider/test/HpkeGenTest.java new file mode 100644 index 00000000..6702d211 --- /dev/null +++ b/tst/com/amazon/corretto/crypto/provider/test/HpkeGenTest.java @@ -0,0 +1,49 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider.test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.amazon.corretto.crypto.provider.HpkeParameterSpec; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(TestResultLogger.class) +@Execution(ExecutionMode.CONCURRENT) +@ResourceLock(value = TestUtil.RESOURCE_GLOBAL, mode = ResourceAccessMode.READ) +public class HpkeGenTest { + static List namedSpecs() { + return Arrays.asList( + HpkeParameterSpec.X25519Sha256Aes128gcm, + HpkeParameterSpec.X25519Sha256Chapoly, + HpkeParameterSpec.Mlkem768Sha256Aes256gcm, + HpkeParameterSpec.Mlkem1024Sha384Aes256gcm, + HpkeParameterSpec.Pqt25519Sha256Aes256gcm, + HpkeParameterSpec.Pqt256Sha256Aes256gcm, + HpkeParameterSpec.Pqt384Sha384Aes256gcm); + } + + private KeyPairGenerator getGenerator() throws GeneralSecurityException { + return KeyPairGenerator.getInstance("HPKE", TestUtil.NATIVE_PROVIDER); + } + + @ParameterizedTest + @MethodSource("namedSpecs") + public void basicKeygen(HpkeParameterSpec spec) throws GeneralSecurityException { + final KeyPairGenerator generator = getGenerator(); + generator.initialize(spec); + final KeyPair keyPair = generator.generateKeyPair(); + assertNotNull(keyPair.getPublic()); + assertNotNull(keyPair.getPrivate()); + } +}