From fd899cae5efcda61f183d7adf15ab643346c7b33 Mon Sep 17 00:00:00 2001 From: John Downey Date: Mon, 13 Jul 2026 10:40:13 +0200 Subject: [PATCH] crypto: add crypto:kdf/4 --- lib/crypto/c_src/Makefile.in | 1 + lib/crypto/c_src/atoms.c | 42 ++ lib/crypto/c_src/atoms.h | 21 + lib/crypto/c_src/crypto.c | 4 + lib/crypto/c_src/kdf.c | 763 +++++++++++++++++++++ lib/crypto/c_src/kdf.h | 65 ++ lib/crypto/c_src/openssl_config.h | 8 + lib/crypto/doc/guides/algorithm_details.md | 19 + lib/crypto/src/crypto.erl | 180 ++++- lib/crypto/test/crypto_SUITE.erl | 452 +++++++++++- 10 files changed, 1546 insertions(+), 9 deletions(-) create mode 100644 lib/crypto/c_src/kdf.c create mode 100644 lib/crypto/c_src/kdf.h diff --git a/lib/crypto/c_src/Makefile.in b/lib/crypto/c_src/Makefile.in index 646ea7da894b..13070224fc1d 100644 --- a/lib/crypto/c_src/Makefile.in +++ b/lib/crypto/c_src/Makefile.in @@ -109,6 +109,7 @@ CRYPTO_OBJS = $(OBJDIR)/crypto$(TYPEMARKER).o \ $(OBJDIR)/hash$(TYPEMARKER).o \ $(OBJDIR)/hmac$(TYPEMARKER).o \ $(OBJDIR)/info$(TYPEMARKER).o \ + $(OBJDIR)/kdf$(TYPEMARKER).o \ $(OBJDIR)/mac$(TYPEMARKER).o \ $(OBJDIR)/math$(TYPEMARKER).o \ $(OBJDIR)/pkey$(TYPEMARKER).o \ diff --git a/lib/crypto/c_src/atoms.c b/lib/crypto/c_src/atoms.c index e32b32d8a8b3..cf7bd793c816 100644 --- a/lib/crypto/c_src/atoms.c +++ b/lib/crypto/c_src/atoms.c @@ -163,6 +163,27 @@ ERL_NIF_TERM atom_mlkem768; ERL_NIF_TERM atom_mlkem1024; #endif +#ifdef HAVE_KDF +ERL_NIF_TERM atom_salt; +ERL_NIF_TERM atom_info; +ERL_NIF_TERM atom_iterations; +ERL_NIF_TERM atom_n; +ERL_NIF_TERM atom_r; +ERL_NIF_TERM atom_p; +ERL_NIF_TERM atom_maxmem; +ERL_NIF_TERM atom_mac; +ERL_NIF_TERM atom_extract_and_expand; +ERL_NIF_TERM atom_extract_only; +ERL_NIF_TERM atom_expand_only; +#endif + +#ifdef HAVE_ARGON2 +ERL_NIF_TERM atom_memory; +ERL_NIF_TERM atom_parallelism; +ERL_NIF_TERM atom_secret; +ERL_NIF_TERM atom_ad; +#endif + int init_atoms(ErlNifEnv *env) { atom_true = enif_make_atom(env,"true"); atom_false = enif_make_atom(env,"false"); @@ -296,5 +317,26 @@ int init_atoms(ErlNifEnv *env) { atom_mlkem768 = enif_make_atom(env,"mlkem768"); atom_mlkem1024 = enif_make_atom(env,"mlkem1024"); #endif + +#ifdef HAVE_KDF + atom_salt = enif_make_atom(env,"salt"); + atom_info = enif_make_atom(env,"info"); + atom_iterations = enif_make_atom(env,"iterations"); + atom_n = enif_make_atom(env,"n"); + atom_r = enif_make_atom(env,"r"); + atom_p = enif_make_atom(env,"p"); + atom_maxmem = enif_make_atom(env,"maxmem"); + atom_mac = enif_make_atom(env,"mac"); + atom_extract_and_expand = enif_make_atom(env,"extract_and_expand"); + atom_extract_only = enif_make_atom(env,"extract_only"); + atom_expand_only = enif_make_atom(env,"expand_only"); +#endif + +#ifdef HAVE_ARGON2 + atom_memory = enif_make_atom(env,"memory"); + atom_parallelism = enif_make_atom(env,"parallelism"); + atom_secret = enif_make_atom(env,"secret"); + atom_ad = enif_make_atom(env,"ad"); +#endif return 1; } diff --git a/lib/crypto/c_src/atoms.h b/lib/crypto/c_src/atoms.h index 7d693efd313a..e18d455a60ed 100644 --- a/lib/crypto/c_src/atoms.h +++ b/lib/crypto/c_src/atoms.h @@ -161,6 +161,27 @@ extern ERL_NIF_TERM atom_mlkem768; extern ERL_NIF_TERM atom_mlkem1024; #endif +#ifdef HAVE_KDF +extern ERL_NIF_TERM atom_salt; +extern ERL_NIF_TERM atom_info; +extern ERL_NIF_TERM atom_iterations; +extern ERL_NIF_TERM atom_n; +extern ERL_NIF_TERM atom_r; +extern ERL_NIF_TERM atom_p; +extern ERL_NIF_TERM atom_maxmem; +extern ERL_NIF_TERM atom_mac; +extern ERL_NIF_TERM atom_extract_and_expand; +extern ERL_NIF_TERM atom_extract_only; +extern ERL_NIF_TERM atom_expand_only; +#endif + +#ifdef HAVE_ARGON2 +extern ERL_NIF_TERM atom_memory; +extern ERL_NIF_TERM atom_parallelism; +extern ERL_NIF_TERM atom_secret; +extern ERL_NIF_TERM atom_ad; +#endif + int init_atoms(ErlNifEnv *env); #endif /* E_ATOMS_H__ */ diff --git a/lib/crypto/c_src/crypto.c b/lib/crypto/c_src/crypto.c index 7cb406806398..616f42bbd920 100644 --- a/lib/crypto/c_src/crypto.c +++ b/lib/crypto/c_src/crypto.c @@ -47,6 +47,7 @@ #include "hash_equals.h" #include "hmac.h" #include "info.h" +#include "kdf.h" #include "math.h" #include "pbkdf2_hmac.h" #include "pkey.h" @@ -101,6 +102,8 @@ static ErlNifFunc nif_funcs[] = { {"hash_equals_nif", 2, hash_equals_nif, 0}, {"pbkdf2_hmac_nif", 5, pbkdf2_hmac_nif, 0}, + {"kdf_nif", 4, kdf_nif, 0}, + {"kdf_algorithms", 0, kdf_algorithms, 0}, {"pkey_sign_nif", 5, pkey_sign_nif, 0}, {"pkey_sign_heavy_nif", 5, pkey_sign_heavy_nif, ERL_NIF_DIRTY_JOB_CPU_BOUND}, {"pkey_verify_nif", 6, pkey_verify_nif, 0}, @@ -341,6 +344,7 @@ static int initialize(ErlNifEnv* env, ERL_NIF_TERM load_info) init_mac_types(env); init_cipher_types(env); init_algorithms_types(env); + init_kdf_types(env); library_initialized = 1; ret = 0; diff --git a/lib/crypto/c_src/kdf.c b/lib/crypto/c_src/kdf.c new file mode 100644 index 000000000000..6d5edf08698c --- /dev/null +++ b/lib/crypto/c_src/kdf.c @@ -0,0 +1,763 @@ +/* + * %CopyrightBegin% + * + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2026 John Downey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * %CopyrightEnd% + */ + +#include "common.h" +#include "kdf.h" + +#ifdef HAVE_KDF + +#include +#include +#include "digest.h" + +static struct kdf_type_t kdf_types[] = { +#ifdef HAVE_ARGON2 + {"argon2id", "ARGON2ID", 0, OSSL_KDF_PARAM_PASSWORD, ARGON2_kdf, 0, NULL}, + {"argon2i", "ARGON2I", 0, OSSL_KDF_PARAM_PASSWORD, ARGON2_kdf, 0, NULL}, + {"argon2d", "ARGON2D", 0, OSSL_KDF_PARAM_PASSWORD, ARGON2_kdf, 0, NULL}, +#endif + {"hkdf", "HKDF", 0, OSSL_KDF_PARAM_KEY, HKDF_kdf, 0, NULL}, + {"pbkdf2", "PBKDF2", 0, OSSL_KDF_PARAM_PASSWORD, PBKDF2_kdf, 0, NULL}, + {"scrypt", "SCRYPT", 0, OSSL_KDF_PARAM_PASSWORD, SCRYPT_kdf, 0, NULL}, + {"sskdf", "SSKDF", 0, OSSL_KDF_PARAM_SECRET, SSKDF_kdf, 0, NULL}, + + /*==== End of list ==== */ + {NULL, NULL, 0, NULL, NO_kdf, 0, NULL} +}; + +void init_kdf_types(ErlNifEnv* env) +{ + struct kdf_type_t* kdf; + + for (kdf = kdf_types; kdf->str; kdf++) { + kdf->atom = enif_make_atom(env, kdf->str); + kdf->kdf = EVP_KDF_fetch(NULL, kdf->ossl_name, ""); + +#ifdef FIPS_SUPPORT + { + EVP_KDF *tmp = EVP_KDF_fetch(NULL, kdf->ossl_name, "fips=yes"); + if (tmp) { + EVP_KDF_free(tmp); + kdf->flags &= ~NO_FIPS_KDF; + } else { + kdf->flags |= NO_FIPS_KDF; + } + } +#endif + } +} + +static struct kdf_type_t* get_kdf_type(ERL_NIF_TERM type) +{ + struct kdf_type_t* kdf; + + for (kdf = kdf_types; kdf->str; kdf++) { + if (type == kdf->atom) { + return kdf; + } + } + + return NULL; +} + +/* Reject the first option key the KDF does not recognize. */ +static int check_options(ErlNifEnv* env, ERL_NIF_TERM map, + const ERL_NIF_TERM allowed[], size_t n_allowed, + ERL_NIF_TERM* return_term) +{ + ErlNifMapIterator iter; + ERL_NIF_TERM key, value; + + if (!enif_map_iterator_create(env, map, &iter, ERL_NIF_MAP_ITERATOR_FIRST)) { + *return_term = EXCP_BADARG_N(env, 3, "Not a map"); + return 0; + } + + while (enif_map_iterator_get_pair(env, &iter, &key, &value)) { + size_t i; + int found = 0; + + for (i = 0; i < n_allowed; i++) { + if (key == allowed[i]) { + found = 1; + break; + } + } + + if (!found) { + char buffer[64]; + if (enif_snprintf(buffer, sizeof(buffer), "Unknown option: %T", key) > 0) { + *return_term = EXCP_BADARG_N(env, 3, buffer); + } else { + *return_term = EXCP_BADARG_N(env, 3, "Unknown option"); + } + + enif_map_iterator_destroy(env, &iter); + return 0; + } + + enif_map_iterator_next(env, &iter); + } + + enif_map_iterator_destroy(env, &iter); + return 1; +} + +/* Fetch a binary option from the map. */ +static int get_bin_opt(ErlNifEnv* env, ERL_NIF_TERM map, ERL_NIF_TERM key, + const char* name, int required, ErlNifBinary* bin, + int* present, ERL_NIF_TERM* return_term) +{ + ERL_NIF_TERM value; + + if (!enif_get_map_value(env, map, key, &value)) { + *present = 0; + if (required) { + char buf[64]; + enif_snprintf(buf, sizeof(buf), "Missing option: %s", name); + *return_term = EXCP_BADARG_N(env, 3, buf); + return 0; + } + + return 1; + } + + *present = 1; + if (!enif_inspect_binary(env, value, bin)) { + char buf[64]; + enif_snprintf(buf, sizeof(buf), "Option %s must be a binary", name); + *return_term = EXCP_BADARG_N(env, 3, buf); + return 0; + } + + return 1; +} + +/* Fetch a positive integer option from the map. A non-zero max sets an + inclusive upper bound and 0 means no upper bound. */ +static int get_uint_opt(ErlNifEnv* env, ERL_NIF_TERM map, ERL_NIF_TERM key, + const char* name, int required, uint64_t max, + uint64_t* val, int* present, ERL_NIF_TERM* return_term) +{ + ERL_NIF_TERM value; + ErlNifUInt64 tmp; + + if (!enif_get_map_value(env, map, key, &value)) { + *present = 0; + if (required) { + char buf[64]; + enif_snprintf(buf, sizeof(buf), "Missing option: %s", name); + *return_term = EXCP_BADARG_N(env, 3, buf); + return 0; + } + return 1; + } + + *present = 1; + if (!enif_get_uint64(env, value, &tmp) || tmp < 1) { + char buffer[64]; + enif_snprintf(buffer, sizeof(buffer), + "Option %s must be a positive integer", name); + *return_term = EXCP_BADARG_N(env, 3, buffer); + return 0; + } + + if (max && tmp > max) { + char buffer[64]; + enif_snprintf(buffer, sizeof(buffer), "Option %s is too large", name); + *return_term = EXCP_BADARG_N(env, 3, buffer); + return 0; + } + + *val = (uint64_t)tmp; + return 1; +} + +/* Fetch the digest option as an OSSL digest name. If md is non-NULL it + receives the resolved EVP_MD. require_flags is a mask of digest_type_t + flags the digest must carry (0 for no extra requirement). */ +static int get_digest_opt(ErlNifEnv* env, ERL_NIF_TERM map, + const char** name, const EVP_MD** md, + unsigned require_flags, ERL_NIF_TERM* return_term) +{ + ERL_NIF_TERM value; + struct digest_type_t* digest; + + if (!enif_get_map_value(env, map, atom_digest, &value)) { + *return_term = EXCP_BADARG_N(env, 3, "Missing option: digest"); + return 0; + } + + digest = get_digest_type(value); + if (digest == NULL) { + *return_term = EXCP_BADARG_N(env, 3, "Bad digest type"); + return 0; + } + + if (DIGEST_FORBIDDEN_IN_FIPS(digest)) { + *return_term = EXCP_NOTSUP_N(env, 3, "Digest type not supported in FIPS"); + return 0; + } + + if (digest->md.p == NULL) { + *return_term = EXCP_NOTSUP_N(env, 3, "Unsupported digest type"); + return 0; + } + + /* XOFs (shake128/shake256) are not usable as the HMAC/hash primitive for + these KDFs; OpenSSL otherwise fails the derive with an opaque error. */ + if (digest->xof_default_length != 0) { + *return_term = EXCP_BADARG_N(env, 3, "XOF digest not supported"); + return 0; + } + + if ((digest->flags & require_flags) != require_flags) { + *return_term = EXCP_BADARG_N(env, 3, "Not eligible digest type"); + return 0; + } + + *name = digest->str_v3; + if (md) { + *md = digest->md.p; + } + + return 1; +} + +static ERL_NIF_TERM kdf_derive(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{/* (Type, KeyMaterial, KeyLen, Options) */ + struct kdf_type_t* kdf; + OSSL_PARAM params[8]; + ErlNifBinary key_material, salt, info, out; + int out_allocated = 0; + ERL_NIF_TERM ret; + uint64_t iterations, scn, scr, scp, maxmem; + ErlNifUInt64 keylen; + size_t n = 0; +#ifdef HAVE_ARGON2 + uint64_t memory, parallelism; + ErlNifBinary secret, ad; +#endif + + int present; + int mode_val = EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND; + const char* digest_name; + const EVP_MD* hkdf_md = NULL; + EVP_KDF_CTX* ctx = NULL; + + ASSERT(argc == 4); + + if ((kdf = get_kdf_type(argv[0])) == NULL) { + return EXCP_BADARG_N(env, 0, "Unknown KDF"); + } + + if (kdf->kdf == NULL) { + return EXCP_NOTSUP(env, "KDF not supported by the cryptolib"); + } + + if (KDF_FORBIDDEN_IN_FIPS(kdf)) { + return EXCP_NOTSUP(env, "KDF not supported in FIPS mode"); + } + + if (!enif_inspect_binary(env, argv[1], &key_material)) { + return EXCP_BADARG_N(env, 1, "Key material must be a binary"); + } + + if (!enif_get_uint64(env, argv[2], &keylen) || keylen < 1 || keylen > INT_MAX) { + return EXCP_BADARG_N(env, 2, "Bad key length"); + } + + if (!enif_is_map(env, argv[3])) { + return EXCP_BADARG_N(env, 3, "Not a map"); + } + + params[n++] = + OSSL_PARAM_construct_octet_string(kdf->input_name, key_material.data, key_material.size); + + switch (kdf->kind) { + +#ifdef HAVE_ARGON2 + case ARGON2_kdf: { + const ERL_NIF_TERM allowed[] = {atom_salt, atom_memory, atom_iterations, + atom_parallelism, atom_secret, atom_ad}; + /* RFC 9106 (Argon2) parameter bounds, matching the limits OpenSSL + enforces, so bad input yields a clear badarg here instead of an opaque + derive failure: + - tag length >= 4 (RFC 9106 3.1 T; ARGON2_MIN_OUTLEN) + - salt >= 8 bytes (RFC 9106 3.1 S; ARGON2_MIN_SALT_LENGTH) + - parallelism <= 2^24 - 1 (RFC 9106 3.1 p; ARGON2_MAX_LANES) + - memory >= 8 * lanes (RFC 9106 3.1 m) */ + if (keylen < 4) { /* RFC 9106 3.1: tag length T >= 4 */ + ret = EXCP_BADARG_N(env, 2, "Argon2 KeyLen must be at least 4"); + goto done; + } + + if (!check_options(env, argv[3], allowed, 6, &ret)) { + goto done; + } + + if (!get_bin_opt(env, argv[3], atom_salt, "salt", 1, &salt, &present, &ret)) { + goto done; + } + + if (salt.size < 8) { /* RFC 9106 3.1: salt S >= 8 bytes */ + ret = EXCP_BADARG_N(env, 3, "Option salt must be at least 8 bytes"); + goto done; + } + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, + salt.data, salt.size); + + if (!get_uint_opt(env, argv[3], atom_memory, "memory", 1, UINT32_MAX, &memory, &present, &ret)) { + goto done; + } + params[n++] = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memory); + + if (!get_uint_opt(env, argv[3], atom_iterations, "iterations", 1, UINT32_MAX, &iterations, &present, &ret)) { + goto done; + } + params[n++] = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_ITER, &iterations); + + if (!get_uint_opt(env, argv[3], atom_parallelism, "parallelism", 1, + 0xFFFFFF /* RFC 9106 3.1: p <= 2^24 - 1 (ARGON2_MAX_LANES) */, + ¶llelism, &present, &ret)) { + goto done; + } + params[n++] = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_ARGON2_LANES, ¶llelism); + + if (memory < 8 * parallelism) { /* RFC 9106 3.1: m >= 8 * p */ + ret = EXCP_BADARG_N(env, 3, + "Option memory must be at least 8 times parallelism"); + goto done; + } + + if (!get_bin_opt(env, argv[3], atom_secret, "secret", 0, &secret, &present, &ret)) { + goto done; + } + if (present) { + if (secret.size > UINT32_MAX) { /* RFC 9106 3.1: secret K <= 2^32 - 1 */ + ret = EXCP_BADARG_N(env, 3, "Option secret is too large"); + goto done; + } + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SECRET, + secret.data, secret.size); + } + + if (!get_bin_opt(env, argv[3], atom_ad, "ad", 0, &ad, &present, &ret)) { + goto done; + } + if (present) { + if (ad.size > UINT32_MAX) { /* RFC 9106 3.1: associated data X <= 2^32 - 1 */ + ret = EXCP_BADARG_N(env, 3, "Option ad is too large"); + goto done; + } + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_ARGON2_AD, + ad.data, ad.size); + } + + break; + } +#endif /* HAVE_ARGON2 */ + + case HKDF_kdf: { + const ERL_NIF_TERM allowed[] = {atom_digest, atom_salt, atom_info, atom_mode}; + ERL_NIF_TERM mode; + + if (!check_options(env, argv[3], allowed, 4, &ret)) { + goto done; + } + + if (!get_digest_opt(env, argv[3], &digest_name, &hkdf_md, 0, &ret)) { + goto done; + } + + params[n++] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)digest_name, 0); + + /* The salt is only used in the extract step, so expand_only should + * reject it rather than silently drop it. */ + if (enif_get_map_value(env, argv[3], atom_mode, &mode)) { + if (mode == atom_extract_and_expand) { + mode_val = EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND; + } else if (mode == atom_extract_only) { + mode_val = EVP_KDF_HKDF_MODE_EXTRACT_ONLY; + if (keylen != (ErlNifUInt64)EVP_MD_get_size(hkdf_md)) { + ret = EXCP_BADARG_N(env, 2, + "extract_only KeyLen must equal the digest size"); + goto done; + } + } else if (mode == atom_expand_only) { + mode_val = EVP_KDF_HKDF_MODE_EXPAND_ONLY; + } else { + ret = EXCP_BADARG_N(env, 3, "Bad hkdf mode"); + goto done; + } + + params[n++] = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode_val); + } + + /* HKDF-Expand can emit at most 255 * HashLen bytes (RFC 5869 2.3). + extract_only is pinned to the digest size above; every other mode + (default == extract_and_expand, and expand_only) expands. */ + { + int md_size = EVP_MD_get_size(hkdf_md); + if (mode_val != EVP_KDF_HKDF_MODE_EXTRACT_ONLY + && md_size > 0 + && keylen > (ErlNifUInt64)255 * md_size) { + ret = EXCP_BADARG_N(env, 2, + "KeyLen must be at most 255 times the digest size"); + goto done; + } + } + + if (!get_bin_opt(env, argv[3], atom_salt, "salt", 0, &salt, &present, &ret)) { + goto done; + } + + if (present) { + if (mode_val == EVP_KDF_HKDF_MODE_EXPAND_ONLY) { + ret = EXCP_BADARG_N(env, 3, + "Option salt not used with mode => expand_only"); + goto done; + } + + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, + salt.data, salt.size); + } + + if (!get_bin_opt(env, argv[3], atom_info, "info", 0, &info, &present, &ret)) { + goto done; + } + + if (present) { + if (mode_val == EVP_KDF_HKDF_MODE_EXTRACT_ONLY) { + ret = EXCP_BADARG_N(env, 3, + "Option info not used with mode => extract_only"); + goto done; + } + + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_INFO, + info.data, info.size); + } + + break; + } + + case PBKDF2_kdf: { + const ERL_NIF_TERM allowed[] = {atom_digest, atom_salt, atom_iterations}; + if (!check_options(env, argv[3], allowed, 3, &ret)) { + goto done; + } + + if (!get_digest_opt(env, argv[3], &digest_name, NULL, + PBKDF2_ELIGIBLE_DIGEST, &ret)) { + goto done; + } + + params[n++] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)digest_name, 0); + + if (!get_bin_opt(env, argv[3], atom_salt, "salt", 1, &salt, &present, &ret)) { + goto done; + } + + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, + salt.data, salt.size); + + if (!get_uint_opt(env, argv[3], atom_iterations, "iterations", 1, 0, &iterations, &present, &ret)) { + goto done; + } + + params[n++] = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_ITER, &iterations); + break; + } + + case SCRYPT_kdf: { + const ERL_NIF_TERM allowed[] = {atom_salt, atom_n, atom_r, atom_p, atom_maxmem}; + if (!check_options(env, argv[3], allowed, 5, &ret)) { + goto done; + } + + if (!get_bin_opt(env, argv[3], atom_salt, "salt", 1, &salt, &present, &ret)) { + goto done; + } + + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, salt.data, salt.size); + if (!get_uint_opt(env, argv[3], atom_n, "n", 1, 0, &scn, &present, &ret)) { + goto done; + } + + if (scn < 2 || (scn & (scn - 1)) != 0) { + ret = EXCP_BADARG_N(env, 3, + "Option n must be a power of two greater than 1"); + goto done; + } + + params[n++] = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_N, &scn); + + if (!get_uint_opt(env, argv[3], atom_r, "r", 1, UINT32_MAX, &scr, &present, &ret)) { + goto done; + } + + params[n++] = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_R, &scr); + + if (!get_uint_opt(env, argv[3], atom_p, "p", 1, UINT32_MAX, &scp, &present, &ret)) { + goto done; + } + + params[n++] = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_P, &scp); + + /* Pass maxmem through only when supplied; otherwise OpenSSL applies its + own default and enforces the n/r/p memory limit itself. */ + if (!get_uint_opt(env, argv[3], atom_maxmem, "maxmem", 0, 0, &maxmem, &present, &ret)) { + goto done; + } + + if (present) { + params[n++] = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_MAXMEM, &maxmem); + } + + break; + } + + case SSKDF_kdf: { + const ERL_NIF_TERM allowed[] = {atom_digest, atom_mac, atom_salt, atom_info}; + int has_mac = 0; + ERL_NIF_TERM mac; + if (!check_options(env, argv[3], allowed, 4, &ret)) { + goto done; + } + + if (!get_digest_opt(env, argv[3], &digest_name, NULL, 0, &ret)) { + goto done; + } + + params[n++] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)digest_name, 0); + + if (enif_get_map_value(env, argv[3], atom_mac, &mac)) { + if (mac != atom_hmac) { + ret = EXCP_BADARG_N(env, 3, "Bad mac type"); + goto done; + } + has_mac = 1; + params[n++] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_MAC, + (char*)"HMAC", 0); + } + + if (!get_bin_opt(env, argv[3], atom_salt, "salt", 0, &salt, &present, &ret)) { + goto done; + } + + if (present) { + /* SSKDF only uses salt as the HMAC/KMAC key */ + if (!has_mac) { + ret = EXCP_BADARG_N(env, 3, "Option salt requires mac => hmac"); + goto done; + } + + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, + salt.data, salt.size); + } + + if (!get_bin_opt(env, argv[3], atom_info, "info", 0, &info, &present, &ret)) { + goto done; + } + + if (present) { + params[n++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_INFO, + info.data, info.size); + } + + break; + } + + default: + return EXCP_NOTSUP(env, "Unsupported KDF"); + } + + params[n] = OSSL_PARAM_construct_end(); + + if ((ctx = EVP_KDF_CTX_new(kdf->kdf)) == NULL) { + ret = EXCP_ERROR(env, "Can't create KDF context"); + goto done; + } + + if (!enif_alloc_binary((size_t)keylen, &out)) { + ret = EXCP_ERROR(env, "Can't allocate binary"); + goto done; + } + + out_allocated = 1; + + if (EVP_KDF_derive(ctx, out.data, (size_t)keylen, params) <= 0) { + ret = EXCP_ERROR(env, "Low-level call failed"); + goto done; + } + + out_allocated = 0; + ret = enif_make_binary(env, &out); + + done: + if (out_allocated) { + enif_release_binary(&out); + } + + if (ctx) { + EVP_KDF_CTX_free(ctx); + } + + return ret; +} + +/* True if the caller-controlled binary inputs (the key material plus every + binary-valued option in the map) total no more than MAX_BYTES_TO_NIF. Used to + keep long HMAC/hash passes off the normal schedulers. */ +static int kdf_is_small_input(ErlNifEnv* env, ERL_NIF_TERM key_material, ERL_NIF_TERM map) +{ + ErlNifBinary bin; + size_t total = 0, map_size; + ErlNifMapIterator iter; + + /* A valid options map has only a handful of keys (at most 4 for any + fast-path KDF); a larger map is invalid input and badargs anyway. Bound + the size here so the scan below can't run unbounded on a normal + scheduler. Oversized maps fall through to the dirty scheduler. */ + if (!enif_get_map_size(env, map, &map_size) || map_size > 8) { + return 0; + } + + if (enif_inspect_binary(env, key_material, &bin)) { + total += bin.size; + } + + if (total <= MAX_BYTES_TO_NIF + && enif_map_iterator_create(env, map, &iter, ERL_NIF_MAP_ITERATOR_FIRST)) { + ERL_NIF_TERM key, value; + while (enif_map_iterator_get_pair(env, &iter, &key, &value)) { + if (enif_inspect_binary(env, value, &bin)) { + total += bin.size; + if (total > MAX_BYTES_TO_NIF) { + break; + } + } + + enif_map_iterator_next(env, &iter); + } + + enif_map_iterator_destroy(env, &iter); + } + + return total <= MAX_BYTES_TO_NIF; +} + +#else /* !HAVE_KDF */ + +void init_kdf_types(ErlNifEnv* env) +{ + (void)env; +} + +#endif /* HAVE_KDF */ + +ERL_NIF_TERM kdf_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{/* (Type, KeyMaterial, KeyLen, Options) */ +#ifdef HAVE_KDF + struct kdf_type_t* kdfp; + + ASSERT(argc == 4); + + if ((kdfp = get_kdf_type(argv[0])) == NULL) { + return EXCP_BADARG_N(env, 0, "Unknown KDF"); + } + + switch (kdfp->kind) { + case HKDF_kdf: + case SSKDF_kdf: { + /* HKDF-Expand runs one HMAC per output block, and SSKDF re-hashes the + full secret+info for every output block, so for both the work grows + with keylen. Cap keylen (a few blocks at most) to keep small + derivations on a normal scheduler; the key material and + salt/secret/info are caller-controlled, hence the input-size cap. + Larger outputs fall through to the dirty scheduler. */ + ErlNifUInt64 keylen; + if (enif_get_uint64(env, argv[2], &keylen) + && keylen <= 64 + && kdf_is_small_input(env, argv[1], argv[3])) { + return kdf_derive(env, argc, argv); + } + + break; + } + + case PBKDF2_kdf: { + /* A small iteration count, key length, and inputs keep the work short + enough for a normal scheduler. */ + ERL_NIF_TERM iter_term; + ErlNifUInt64 iter, keylen; + + if (enif_get_uint64(env, argv[2], &keylen) + && keylen <= 64 + && enif_get_map_value(env, argv[3], atom_iterations, &iter_term) + && enif_get_uint64(env, iter_term, &iter) + && iter <= 100 + && kdf_is_small_input(env, argv[1], argv[3])) { + return kdf_derive(env, argc, argv); + } + break; + } + + default: + break; + } + + /* use a dirty CPU scheduler otherwise */ + return enif_schedule_nif(env, "kdf_derive", + ERL_NIF_DIRTY_JOB_CPU_BOUND, + kdf_derive, argc, argv); +#else + return EXCP_NOTSUP(env, "Unsupported: requires OpenSSL 3.0 or later"); +#endif +} + +ERL_NIF_TERM kdf_algorithms(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{/* () */ +#ifdef HAVE_KDF + ERL_NIF_TERM ret = enif_make_list(env, 0); + struct kdf_type_t* kdf; + + for (kdf = kdf_types; kdf->str; kdf++) { + if (kdf->kdf == NULL) { + continue; + } + + if (KDF_FORBIDDEN_IN_FIPS(kdf)) { + continue; + } + + ret = enif_make_list_cell(env, kdf->atom, ret); + } + return ret; +#else + + return enif_make_list(env, 0); +#endif +} diff --git a/lib/crypto/c_src/kdf.h b/lib/crypto/c_src/kdf.h new file mode 100644 index 000000000000..c9ffae0d4e38 --- /dev/null +++ b/lib/crypto/c_src/kdf.h @@ -0,0 +1,65 @@ +/* + * %CopyrightBegin% + * + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2026 John Downey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * %CopyrightEnd% + */ + +#ifndef E_KDF_H__ +#define E_KDF_H__ 1 + +#include "common.h" + +#ifdef HAVE_KDF + +enum kdf_kind { + NO_kdf = 0, + ARGON2_kdf, + HKDF_kdf, + PBKDF2_kdf, + SCRYPT_kdf, + SSKDF_kdf +}; + +struct kdf_type_t { + const char *str; /* before init, NULL for end-of-table */ + const char *ossl_name; /* the KDF name as in OpenSSL 3.x */ + ERL_NIF_TERM atom; /* after init */ + const char *input_name; /* OSSL_KDF_PARAM_* of the key material */ + int kind; /* enum kdf_kind */ + unsigned flags; + EVP_KDF *kdf; /* after init, NULL if notsup */ +}; + +/* masks in the flags field of kdf_type_t */ +#define NO_FIPS_KDF 1 + +#ifdef FIPS_SUPPORT +# define KDF_FORBIDDEN_IN_FIPS(P) (((P)->flags & NO_FIPS_KDF) && FIPS_MODE()) +#else +# define KDF_FORBIDDEN_IN_FIPS(P) 0 +#endif + +#endif /* HAVE_KDF */ + +void init_kdf_types(ErlNifEnv* env); + +ERL_NIF_TERM kdf_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +ERL_NIF_TERM kdf_algorithms(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); + +#endif /* E_KDF_H__ */ diff --git a/lib/crypto/c_src/openssl_config.h b/lib/crypto/c_src/openssl_config.h index fa97d9065e4d..64e4a3b9216d 100644 --- a/lib/crypto/c_src/openssl_config.h +++ b/lib/crypto/c_src/openssl_config.h @@ -240,6 +240,14 @@ # define HAVE_BLAKE2 #endif +#ifdef HAS_3_0_API +# define HAVE_KDF +#endif + +#if OPENSSL_VERSION_NUMBER >= PACKED_OPENSSL_VERSION_PLAIN(3,2,0) +# define HAVE_ARGON2 +#endif + #ifndef OPENSSL_NO_BF # define HAVE_BF #endif diff --git a/lib/crypto/doc/guides/algorithm_details.md b/lib/crypto/doc/guides/algorithm_details.md index dc6eedc89f24..f54f26a9025f 100644 --- a/lib/crypto/doc/guides/algorithm_details.md +++ b/lib/crypto/doc/guides/algorithm_details.md @@ -252,6 +252,25 @@ column is present in the list returned by | MD5 | md5 | | | RIPEMD | ripemd160 | | +## Key Derivation Functions + +To be used in [kdf/4](`crypto:kdf/4`), which documents the full set of options +accepted by each function. + +To dynamically check availability, check that the wanted name in the _KDF_ +column is present in the list returned by +[crypto:supports(kdfs)](`crypto:supports/1`). + +| **KDF** | **Standard** | **Required options** | **Limited to** **OpenSSL versions** | +| -------------------------------- | --------------- | --------------------------------------------- | ----------------------------------- | +| `argon2id`, `argon2i`, `argon2d` | RFC 9106 | `salt`, `memory`, `iterations`, `parallelism` | ≥3.2 | +| `hkdf` | RFC 5869 | `digest` | ≥3.0 | +| `pbkdf2` | RFC 8018 | `digest`, `salt`, `iterations` | ≥3.0 | +| `scrypt` | RFC 7914 | `salt`, `n`, `r`, `p` | ≥3.0 | +| `sskdf` | NIST SP 800-56C | `digest` | ≥3.0 | + +_Table: Key derivation functions_ + ## Public Key Cryptography ### RSA diff --git a/lib/crypto/src/crypto.erl b/lib/crypto/src/crypto.erl index f26a2b2a73dc..998bc2ecbe14 100644 --- a/lib/crypto/src/crypto.erl +++ b/lib/crypto/src/crypto.erl @@ -84,6 +84,18 @@ This module provides a set of cryptographic functions. - **SRP** - [The SRP Authentication and Key Exchange System (RFC 2945)](http://www.ietf.org/rfc/rfc2945.txt) +- **KDFs - Key Derivation Functions** - + + - **Argon2i**, **Argon2d**, and **Argon2id** - [Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications (RFC 9106)](https://www.rfc-editor.org/rfc/rfc9106) + + - **HKDF** - [HMAC-based Extract-and-Expand Key Derivation Function (RFC 5869)](https://www.rfc-editor.org/rfc/rfc5869) + + - **SSKDF** - [Single-Step Key Derivation Function (NIST SP 800-56C)](https://doi.org/10.6028/NIST.SP.800-56Cr2) + + - **PBKDF2** - [Password-Based Key Derivation Function 2 (RFC 8018)](https://www.rfc-editor.org/rfc/rfc8018) + + - **Scrypt** - [The scrypt Password-Based Key Derivation Function (RFC 7914)](https://www.rfc-editor.org/rfc/rfc7914) + > #### Note {: .info } > > The actual supported algorithms and features depends on their availability in @@ -177,6 +189,7 @@ end -export([rand_seed/1]). -export([format_error/2]). -export([pbkdf2_hmac/5]). +-export([kdf/4]). %%%---------------------------------------------------------------- %% Deprecated functions @@ -288,6 +301,7 @@ end ng_crypto_get_data_nif/1, ng_crypto_one_time_nif/5, strong_rand_bytes_nif/1, strong_rand_range_nif/1, rand_uniform_nif/2, mod_exp_nif/4, do_exor/2, hash_equals_nif/2, pbkdf2_hmac_nif/5, + kdf_nif/4, kdf_algorithms/0, pkey_sign_nif/5, pkey_verify_nif/6, pkey_crypt_nif/6, pkey_sign_heavy_nif/5, encapsulate_key_nif/2, decapsulate_key_nif/3, @@ -319,7 +333,14 @@ end sha3/0, mldsa/0, slh_dsa/0, - kem/0 + kem/0, + kdf/0, + argon2_opts/0, + sskdf_opts/0, + hkdf_opts/0, + scrypt_opts/0, + pbkdf2_opts/0, + kdf_opts/0 ]). -export_type([ @@ -366,6 +387,54 @@ end -doc(#{group => <<"Key Encapsulation Mechanism">>}). -type kem() :: mlkem512 | mlkem768 | mlkem1024. +-doc "Key derivation functions.". +-doc(#{group => <<"KDF API">>}). +-type kdf() :: argon2d | argon2i | argon2id | sskdf | hkdf | pbkdf2 | scrypt. + +-doc "Options for the Argon2 (`argon2id`, `argon2i`, `argon2d`) functions, see `kdf/4`.". +-doc(#{group => <<"KDF API">>}). +-type argon2_opts() :: #{salt := binary(), + memory := pos_integer(), + iterations := pos_integer(), + parallelism := pos_integer(), + secret => binary(), + ad => binary()}. + +-doc "Options for the single-step (`sskdf`) key derivation function, see `kdf/4`.". +-doc(#{group => <<"KDF API">>}). +-type sskdf_opts() :: #{digest := hmac_hash_algorithm(), + info => binary()} + | #{digest := hmac_hash_algorithm(), + mac := hmac, + salt => binary(), + info => binary()}. + +-doc "Options for the `hkdf` key derivation function, see `kdf/4`.". +-doc(#{group => <<"KDF API">>}). +-type hkdf_opts() :: #{digest := hmac_hash_algorithm(), + salt => binary(), + info => binary(), + mode => extract_and_expand | extract_only | expand_only}. + +-doc "Options for the `scrypt` key derivation function, see `kdf/4`.". +-doc(#{group => <<"KDF API">>}). +-type scrypt_opts() :: #{salt := binary(), + n := pos_integer(), + r := pos_integer(), + p := pos_integer(), + maxmem => pos_integer()}. + +-doc "Options for the `pbkdf2` key derivation function, see `kdf/4`.". +-doc(#{group => <<"KDF API">>}). +-type pbkdf2_opts() :: #{digest := sha1() | sha2(), + salt := binary(), + iterations := pos_integer()}. + +-doc "Options for `kdf/4`, determined by the chosen `t:kdf/0`.". +-doc(#{group => <<"KDF API">>}). +-type kdf_opts() :: argon2_opts() | sskdf_opts() | hkdf_opts() + | scrypt_opts() | pbkdf2_opts(). + %%% Keys -doc(#{group => <<"Public/Private Keys">>,equiv => rsa_params()}). -type rsa_public() :: [key_integer()] . % [E, N] @@ -835,10 +904,12 @@ stop() -> | {public_keys, PKs} | {macs, Macs} | {curves, Curves} - | {rsa_opts, RSAopts}, + | {rsa_opts, RSAopts} + | {kdfs, KDFs}, Hashs :: [sha1() | sha2() | sha3() | sha3_xof() | blake2() | ripemd160 | sm3 | compatibility_only_hash()], Ciphers :: [cipher()], KEMs :: [kem()], + KDFs :: [kdf()], PKs :: [rsa | dss | ecdsa | dh | ecdh | eddh | ec_gf2m | mldsa() | slh_dsa()], Macs :: [hmac | cmac | poly1305 | siphash], Curves :: [ec_named_curve() | edwards_curve_dh() | edwards_curve_ed()], @@ -850,7 +921,8 @@ supports() -> | [{T,supports(T)} || T <- [public_keys, macs, curves, - rsa_opts] + rsa_opts, + kdfs] ] ]. @@ -870,17 +942,20 @@ algorithms. | public_keys | macs | curves - | rsa_opts, + | rsa_opts + | kdfs, Support :: Hashs | Ciphers | KEMs | PKs | Macs | Curves - | RSAopts, + | RSAopts + | KDFs, Hashs :: [sha1() | sha2() | sha3() | sha3_xof() | blake2() | ripemd160 | compatibility_only_hash()], Ciphers :: [cipher()], KEMs :: [kem()], + KDFs :: [kdf()], PKs :: [rsa | dss | ecdsa | dh | ecdh | eddh | ec_gf2m], Macs :: [hmac | cmac | poly1305 | siphash], Curves :: [ec_named_curve() | edwards_curve_dh() | edwards_curve_ed()], @@ -895,7 +970,8 @@ supports(ciphers) -> add_cipher_aliases(cipher_algorithms()); supports(kems) -> kem_algorithms_nif(); supports(macs) -> mac_algorithms(); supports(curves) -> curve_algorithms(); -supports(rsa_opts) -> rsa_opts_algorithms(). +supports(rsa_opts) -> rsa_opts_algorithms(); +supports(kdfs) -> kdf_algorithms(). -doc(#{group => <<"Utility Functions">>}). -doc """ @@ -1012,6 +1088,7 @@ HMAC. Uses the [3-tuple style](`m:crypto#error_3tup`) for error handling. +See also `kdf/4`, which supports more key derivation functions. ## Examples @@ -1020,7 +1097,7 @@ Uses the [3-tuple style](`m:crypto#error_3tup`) for error handling. <<18,15,182,207,252,248,179,44,67,231,34,82,86,196,248,55>> ``` """. --doc(#{group => <<"Engine API">>,since => <<"OTP 24.2">>}). +-doc(#{group => <<"KDF API">>,since => <<"OTP 24.2">>}). -spec pbkdf2_hmac(Digest, Pass, Salt, Iter, KeyLen) -> Result when Digest :: sha | sha224 | sha256 | sha384 | sha512 | sha512_224 | sha512_256, Pass :: binary(), @@ -1033,6 +1110,94 @@ pbkdf2_hmac(Digest, Pass, Salt, Iter, KeyLen) -> pbkdf2_hmac_nif(_, _, _, _, _) -> ?nif_stub. +-doc """ +Derive a key of `KeyLen` bytes from the input keying material `KeyMaterial` +using the key derivation function `Type`. + +`KeyMaterial` is the secret input of the KDF: the password for the +password-based functions (`argon2*`, `scrypt`, `pbkdf2`), the input +keying material (or pseudorandom key in `expand_only` mode) for `hkdf`, +and the shared secret for `sskdf`. + +Requires OpenSSL 3.0 or later; the argon2 functions require OpenSSL 3.2 or +later. Use [`supports(kdfs)`](`supports/1`) to check availability at runtime. + +Uses the [3-tuple style](`m:crypto#error_3tup`) for error handling; an +unavailable KDF raises a `notsup` exception. + +The options for each `Type` are: + +- **`argon2id`, `argon2i`, `argon2d`** - The Argon2 password hashing + functions of [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106). + `argon2id` is the recommended variant. + + Required: `salt` (a binary), `memory` (memory cost in KiB), + `iterations` (passes), `parallelism` (lanes) (positive integers). + Optional: `secret` (key/pepper), `ad` (associated data) (binaries). + + RFC 9106 recommends `memory => 2097152, iterations => 1, parallelism => 4` + (2 GiB) as its first choice. For memory-constrained environments, its second + recommendation is `memory => 65536, iterations => 3, parallelism => 4` + (64 MiB). + +- **`hkdf`** - HMAC-based extract-and-expand key derivation of + [RFC 5869](https://www.rfc-editor.org/rfc/rfc5869). + + Required: `digest` (a hash algorithm atom, for example `sha256`). + Optional: `salt`, `info` (binaries) and `mode` + (`extract_and_expand` (default), `extract_only` or `expand_only`). + With `extract_only`, `KeyLen` must equal the digest output size. + +- **`sskdf`** - The single-step (Concatenation) key derivation function of + [NIST SP 800-56C](https://doi.org/10.6028/NIST.SP.800-56Cr2). + `KeyMaterial` is the shared secret. + + Required: `digest` (a hash algorithm atom, for example `sha256`). + Optional: `info` (the `FixedInfo`/`OtherInfo` binary) and `mac`, which + selects the auxiliary function. By default the digest is used directly as a + hash; with `mac => hmac` the auxiliary function is HMAC keyed by `salt` + (an optional binary), using `digest` as the underlying hash. + +- **`scrypt`** - The scrypt password-based key derivation function of + [RFC 7914](https://www.rfc-editor.org/rfc/rfc7914). + + Required: `salt` (a binary), `n` (CPU/memory cost, a power + of two), `r` (block size), `p` (parallelization) (positive integers). + Optional: `maxmem` (memory limit in bytes; the cryptolib default is + `1074790400`, that is 1025 MiB). + +- **`pbkdf2`** - PKCS #5 password-based key derivation function 2 of + [RFC 8018](https://www.rfc-editor.org/rfc/rfc8018). + + Required: `digest`, `salt`, `iterations`. + See also `pbkdf2_hmac/5`, which also works with cryptolibs older + than OpenSSL 3.0. + +Example (in production derive a fresh random `salt` per password with +[`strong_rand_bytes/1`](`strong_rand_bytes/1`) and store it with the key): + +```erlang +1> Key = crypto:kdf(scrypt, <<"pleaseletmein">>, 64, + #{salt => <<"SodiumChloride">>, n => 16384, r => 8, p => 1}), + binary:encode_hex(Key, lowercase). +<<"7023bdcb3afd7348461c06cd81fd38eb" + "fda8fbba904f8e3ea9b543f6545da1f2" + "d5432955613f0fcf62d49705242a9af9" + "e61e85dc0d651e40dfcf017b45575887">> +``` +""". +-doc(#{group => <<"KDF API">>, + since => <<"OTP 30.0">>}). +-spec kdf(Type, KeyMaterial, KeyLen, Options) -> binary() + when Type :: kdf(), + KeyMaterial :: binary(), + KeyLen :: pos_integer(), + Options :: kdf_opts(). +kdf(Type, KeyMaterial, KeyLen, Options) -> + ?nif_call(kdf_nif(Type, KeyMaterial, KeyLen, Options)). + +kdf_nif(_, _, _, _) -> ?nif_stub. + %%%================================================================ %%% %%% Hashing @@ -4840,6 +5005,7 @@ hash_algorithms() -> ?nif_stub. pubkey_algorithms() -> ?nif_stub. cipher_algorithms() -> ?nif_stub. kem_algorithms_nif() -> ?nif_stub. +kdf_algorithms() -> ?nif_stub. mac_algorithms() -> ?nif_stub. curve_algorithms() -> ?nif_stub. rsa_opts_algorithms() -> ?nif_stub. diff --git a/lib/crypto/test/crypto_SUITE.erl b/lib/crypto/test/crypto_SUITE.erl index 795a849ae284..c600e9ec1c3b 100644 --- a/lib/crypto/test/crypto_SUITE.erl +++ b/lib/crypto/test/crypto_SUITE.erl @@ -142,6 +142,26 @@ pbkdf2_hmac/1, pbkdf2_hmac_invalid_input/0, pbkdf2_hmac_invalid_input/1, + kdf_hkdf/0, + kdf_hkdf/1, + kdf_sskdf/0, + kdf_sskdf/1, + kdf_pbkdf2/0, + kdf_pbkdf2/1, + kdf_scrypt/0, + kdf_scrypt/1, + kdf_argon2/0, + kdf_argon2/1, + kdf_invalid_input/0, + kdf_invalid_input/1, + kdf_pbkdf2_invalid_input/0, + kdf_pbkdf2_invalid_input/1, + kdf_argon2_invalid_input/0, + kdf_argon2_invalid_input/1, + kdf_scrypt_invalid_input/0, + kdf_scrypt_invalid_input/1, + kdf_sskdf_invalid_input/0, + kdf_sskdf_invalid_input/1, privkey_to_pubkey/1, %% Others: @@ -253,7 +273,17 @@ all() -> hash_equals, concurrent_access, pbkdf2_hmac, - pbkdf2_hmac_invalid_input + pbkdf2_hmac_invalid_input, + kdf_hkdf, + kdf_sskdf, + kdf_pbkdf2, + kdf_scrypt, + kdf_argon2, + kdf_invalid_input, + kdf_pbkdf2_invalid_input, + kdf_argon2_invalid_input, + kdf_scrypt_invalid_input, + kdf_sskdf_invalid_input ]. -define(NEW_CIPHER_TYPE_SCHEMA, @@ -407,7 +437,12 @@ groups() -> {group, aes_256_cfb8}, {group, aes_128_ofb}, {group, aes_192_ofb}, - {group, aes_256_ofb} + {group, aes_256_ofb}, + + {group, no_argon2d}, + {group, no_argon2i}, + {group, no_argon2id}, + {group, no_scrypt} ]}, {md4, [], [hash]}, @@ -511,6 +546,10 @@ groups() -> {no_chacha20, [], [no_support]}, {no_rc2_cbc, [], [no_support]}, {no_rc4, [], [no_support]}, + {no_argon2d, [], [no_support]}, + {no_argon2i, [], [no_support]}, + {no_argon2id, [], [no_support]}, + {no_scrypt, [], [no_support]}, {api_errors, [], [api_errors_ecdh, bad_key_length, bad_cipher_name, @@ -5521,6 +5560,415 @@ pbkdf2_hmac_invalid_input(Config) when is_list(Config) -> error:{notsup, _, "Unsupported CRYPTO_PKCS5_PBKDF2_HMAC"++_} -> {skip, "No CRYPTO_PKCS5_PBKDF2_HMAC"} end. + +kdf_hkdf() -> + [{doc, "Test kdf/4 with hkdf, RFC 5869 test vectors"}]. +kdf_hkdf(Config) when is_list(Config) -> + case lists:member(hkdf, crypto:supports(kdfs)) of + false -> + {skip, "hkdf not supported"}; + true -> + IKM = binary:copy(<<16#0b>>, 22), + Salt = <<16#000102030405060708090a0b0c:(13*8)>>, + Info = <<16#f0f1f2f3f4f5f6f7f8f9:(10*8)>>, + %% A.1: extract-and-expand (default mode) + OKM1 = binary:decode_hex( + <<"3cb25f25faacd57a90434f64d0362f2a" + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + "34007208d5b887185865">>), + OKM1 = crypto:kdf(hkdf, IKM, 42, #{digest => sha256, + salt => Salt, + info => Info}), + %% Same, mode given explicitly + OKM1 = crypto:kdf(hkdf, IKM, 42, #{digest => sha256, + salt => Salt, + info => Info, + mode => extract_and_expand}), + %% A.1 PRK: extract-only; KeyLen must equal digest size + PRK1 = binary:decode_hex( + <<"077709362c2e32df0ddc3f0dc47bba63" + "90b6c73bb50f9c3122ec844ad7c2b3e5">>), + PRK1 = crypto:kdf(hkdf, IKM, 32, #{digest => sha256, + salt => Salt, + mode => extract_only}), + %% A.1 again as expand-only from the PRK + OKM1 = crypto:kdf(hkdf, PRK1, 42, #{digest => sha256, + info => Info, + mode => expand_only}), + %% A.3: zero-length salt and info + OKM3 = binary:decode_hex( + <<"8da4e775a563c18f715f802a063c5a31" + "b8a11f5c5ee1879ec3454e5f3c738d2d" + "9d201395faa4b61a96c8">>), + OKM3 = crypto:kdf(hkdf, IKM, 42, #{digest => sha256, + salt => <<>>, + info => <<>>}), + ok + end. + +kdf_sskdf() -> + [{doc, "Test kdf/4 with sskdf, hash mode (RFC 7518 Appendix C) and " + "HMAC mode (NIST CAVP vector)"}]. +kdf_sskdf(Config) when is_list(Config) -> + case lists:member(sskdf, crypto:supports(kdfs)) of + false -> + {skip, "sskdf not supported"}; + true -> + F = fun(Secret, Opts) -> + binary:encode_hex( + crypto:kdf(sskdf, Secret, 16, Opts), + lowercase) + end, + Secret = binary:decode_hex( + <<"9e56d91d817135d372834283bf84269c" + "fb316ea3da806a48f6daa7798cfe90c4">>), + Info = <<0,0,0,7,"A128GCM", + 0,0,0,5,"Alice", + 0,0,0,3,"Bob", + 0,0,0,128>>, + <<"56aa8deaf8236d205c2228cd71a7101a">> = + F(Secret, #{digest => sha256, info => Info}), + <<"13479e9a91dd20fdd757d68ffe8869fb">> = + F(binary:decode_hex(<<"6ee6c00d70a6cd14bd5a4e8fcfec8386">>), + #{digest => sha256, mac => hmac, + salt => binary:decode_hex( + <<"532f5131e0a2fecc722f87e5aa2062cb">>), + info => binary:decode_hex( + <<"861aa2886798231259bd0314">>)}), + ok + end. + +kdf_pbkdf2() -> + [{doc, "Test kdf/4 with pbkdf2, RFC 6070, RFC 3962 and RFC 7914 " + "test vectors"}]. +kdf_pbkdf2(Config) when is_list(Config) -> + case lists:member(pbkdf2, crypto:supports(kdfs)) of + false -> + {skip, "pbkdf2 not supported"}; + true -> + F = fun(Pass, Salt, Iter, KeyLen) -> + binary:encode_hex( + crypto:kdf(pbkdf2, Pass, KeyLen, + #{digest => sha, + salt => Salt, + iterations => Iter})) + end, + F256 = fun(Pass, Salt, Iter, KeyLen) -> + binary:encode_hex( + crypto:kdf(pbkdf2, Pass, KeyLen, + #{digest => sha256, + salt => Salt, + iterations => Iter})) + end, + %% RFC 6070 + <<"0C60C80F961F0E71F3A9B524AF6012062FE037A6">> = + F(<<"password">>, <<"salt">>, 1, 20), + <<"EA6C014DC72D6F8CCD1ED92ACE1D41F0D8DE8957">> = + F(<<"password">>, <<"salt">>, 2, 20), + <<"4B007901B765489ABEAD49D926F721D065A429C1">> = + F(<<"password">>, <<"salt">>, 4096, 20), + <<"EEFE3D61CD4DA4E4E9945B3D6BA2158C2634E984">> = + F(<<"password">>, <<"salt">>, 16777216, 20), + <<"3D2EEC4FE41C849B80C8D83662C0E44A8B291A964CF2F07038">> = + F(<<"passwordPASSWORDpassword">>, + <<"saltSALTsaltSALTsaltSALTsaltSALTsalt">>, 4096, 25), + <<"56FA6AA75548099DCC37D7F03425E0C3">> = + F(<<"pass\0word">>, <<"sa\0lt">>, 4096, 16), + %% RFC 3962 + <<"CDEDB5281BB2F801565A1122B2563515">> = + F(<<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 1, 16), + <<"CDEDB5281BB2F801565A1122B25635150AD1F7A04BB9F3A333ECC0E2E1F70837">> = + F(<<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 1, 32), + <<"01DBEE7F4A9E243E988B62C73CDA935D">> = + F(<<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 2, 16), + <<"01DBEE7F4A9E243E988B62C73CDA935DA05378B93244EC8F48A99E61AD799D86">> = + F(<<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 2, 32), + <<"5C08EB61FDF71E4E4EC3CF6BA1F5512B">> = + F(<<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 1200, 16), + <<"5C08EB61FDF71E4E4EC3CF6BA1F5512BA7E52DDBC5E5142F708A31E2E62B1E13">> = + F(<<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 1200, 32), + <<"D1DAA78615F287E6A1C8B120D7062A49">> = + F(<<"password">>, binary:encode_unsigned(16#1234567878563412), 5, 16), + <<"D1DAA78615F287E6A1C8B120D7062A493F98D203E6BE49A6ADF4FA574B6E64EE">> = + F(<<"password">>, binary:encode_unsigned(16#1234567878563412), 5, 32), + <<"139C30C0966BC32BA55FDBF212530AC9">> = + F(<<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">>, + <<"pass phrase equals block size">>, 1200, 16), + <<"139C30C0966BC32BA55FDBF212530AC9C5EC59F1A452F5CC9AD940FEA0598ED1">> = + F(<<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">>, + <<"pass phrase equals block size">>, 1200, 32), + <<"9CCAD6D468770CD51B10E6A68721BE61">> = + F(<<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">>, + <<"pass phrase exceeds block size">>, 1200, 16), + <<"9CCAD6D468770CD51B10E6A68721BE611A8B4D282601DB3B36BE9246915EC82A">> = + F(<<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">>, + <<"pass phrase exceeds block size">>, 1200, 32), + <<"6B9CF26D45455A43A5B8BB276A403B39">> = + F(binary:encode_unsigned(16#f09d849e), <<"EXAMPLE.COMpianist">>, 50, 16), + <<"6B9CF26D45455A43A5B8BB276A403B39E7FE37A0C41E02C281FF3069E1E94F52">> = + F(binary:encode_unsigned(16#f09d849e), <<"EXAMPLE.COMpianist">>, 50, 32), + %% SHA256 variant, RFC 7914 (section 11) + <<"55AC046E56E3089FEC1691C22544B605" + "F94185216DDE0465E68B9D57C20DACBC" + "49CA9CCCF179B645991664B39D77EF31" + "7C71B845B1E30BD509112041D3A19783">> = + F256(<<"passwd">>, <<"salt">>, 1, 64), + <<"4DDCD8F60B98BE21830CEE5EF22701F9" + "641A4418D04C0414AEFF08876B34AB56" + "A1D425A1225833549ADB841B51C9B317" + "6A272BDEBBA1D078478F62B397F33C8D">> = + F256(<<"Password">>, <<"NaCl">>, 80000, 64), + %% Cross-check against the existing pbkdf2_hmac/5 + Expected = crypto:pbkdf2_hmac(sha256, <<"pass">>, <<"salt">>, + 1000, 32), + Expected = crypto:kdf(pbkdf2, <<"pass">>, 32, + #{digest => sha256, + salt => <<"salt">>, + iterations => 1000}), + ok + end. + +kdf_scrypt() -> + [{doc, "Test kdf/4 with scrypt, RFC 7914 test vectors"}]. +kdf_scrypt(Config) when is_list(Config) -> + case lists:member(scrypt, crypto:supports(kdfs)) of + false -> + {skip, "scrypt not supported"}; + true -> + F = fun(Pass, Salt, N, R, P) -> + binary:encode_hex( + crypto:kdf(scrypt, Pass, 64, + #{salt => Salt, + n => N, r => R, p => P}), + lowercase) + end, + <<"77d6576238657b203b19ca42c18a0497" + "f16b4844e3074ae8dfdffa3fede21442" + "fcd0069ded0948f8326a753a0fc81f17" + "e8d3e0fb2e0d3628cf35e20c38d18906">> = + F(<<>>, <<>>, 16, 1, 1), + <<"fdbabe1c9d3472007856e7190d01e9fe" + "7c6ad7cbc8237830e77376634b373162" + "2eaf30d92e22a3886ff109279d9830da" + "c727afb94a83ee6d8360cbdfa2cc0640">> = + F(<<"password">>, <<"NaCl">>, 1024, 8, 16), + <<"7023bdcb3afd7348461c06cd81fd38eb" + "fda8fbba904f8e3ea9b543f6545da1f2" + "d5432955613f0fcf62d49705242a9af9" + "e61e85dc0d651e40dfcf017b45575887">> = + F(<<"pleaseletmein">>, <<"SodiumChloride">>, 16384, 8, 1), + %% maxmem is accepted as an option + _ = crypto:kdf(scrypt, <<"p">>, 16, + #{salt => <<"s">>, + n => 16, r => 1, p => 1, + maxmem => 1024*1024*64}), + ok + end. + +kdf_argon2() -> + [{doc, "Test kdf/4 with argon2d/i/id, RFC 9106 test vectors"}]. +kdf_argon2(Config) when is_list(Config) -> + case [K || K <- [argon2d, argon2i, argon2id], + lists:member(K, crypto:supports(kdfs))] of + [argon2d, argon2i, argon2id] -> + Password = binary:copy(<<1>>, 32), + Opts = #{salt => binary:copy(<<2>>, 16), + secret => binary:copy(<<3>>, 8), + ad => binary:copy(<<4>>, 12), + memory => 32, + iterations => 3, + parallelism => 4}, + %% RFC 9106 section 5.1 + <<"512b391b6f1162975371d30919734294" + "f868e3be3984f3c1a13a4db9fabe4acb">> = + binary:encode_hex(crypto:kdf(argon2d, Password, 32, Opts), + lowercase), + %% RFC 9106 section 5.2 + <<"c814d9d1dc7f37aa13f0d77f2494bda1" + "c8de6b016dd388d29952a4c4672b6ce8">> = + binary:encode_hex(crypto:kdf(argon2i, Password, 32, Opts), + lowercase), + %% RFC 9106 section 5.3 + <<"0d640df58d78766c08c037a34a8b53c9" + "d01ef0452d75b65eb52520e96b01e659">> = + binary:encode_hex(crypto:kdf(argon2id, Password, 32, Opts), + lowercase), + %% Without the optional secret/ad keys it must still derive + 32 = byte_size(crypto:kdf(argon2id, Password, 32, + maps:without([secret, ad], Opts))), + ok; + _ -> + {skip, "argon2 not supported (requires OpenSSL 3.2+, no FIPS)"} + end. + +%% Assert that crypto:kdf/4 rejects the given input with a badarg carrying +%% ErrorStr. Shared by the kdf_*_invalid_input testcases. +kdf_expect_badarg(Type, KeyMaterial, KeyLen, Opts, ErrorStr) -> + try crypto:kdf(Type, KeyMaterial, KeyLen, Opts) of + Res -> ct:fail("Unexpected result ~p", [Res]) + catch + error:{badarg, {_, _}, ErrorStr} -> ok; + Tag:Err -> + ct:fail("Unexpected exception ~p:~p", [Tag, Err]) + end. + +kdf_invalid_input() -> + [{doc, "Test kdf/4 with invalid generic and hkdf input"}]. +kdf_invalid_input(Config) when is_list(Config) -> + case lists:member(hkdf, crypto:supports(kdfs)) of + false -> + {skip, "hkdf not supported"}; + true -> + %% Unknown KDF type + kdf_expect_badarg(no_such_kdf, <<"key">>, 32, #{}, "Unknown KDF"), + GoodHkdf = #{digest => sha256}, + %% Bad key material + kdf_expect_badarg(hkdf, "not a binary", 32, GoodHkdf, + "Key material must be a binary"), + kdf_expect_badarg(hkdf, an_atom, 32, GoodHkdf, + "Key material must be a binary"), + %% Bad key length + kdf_expect_badarg(hkdf, <<"key">>, 0, GoodHkdf, "Bad key length"), + kdf_expect_badarg(hkdf, <<"key">>, -1, GoodHkdf, "Bad key length"), + kdf_expect_badarg(hkdf, <<"key">>, bad, GoodHkdf, "Bad key length"), + %% Options not a map + kdf_expect_badarg(hkdf, <<"key">>, 32, [], "Not a map"), + %% Unknown option key + kdf_expect_badarg(hkdf, <<"key">>, 32, GoodHkdf#{iterations => 1}, + "Unknown option: iterations"), + %% Missing required option + kdf_expect_badarg(hkdf, <<"key">>, 32, #{}, "Missing option: digest"), + %% Wrong value types + kdf_expect_badarg(hkdf, <<"key">>, 32, GoodHkdf#{salt => "not a binary"}, + "Option salt must be a binary"), + kdf_expect_badarg(hkdf, <<"key">>, 32, GoodHkdf#{digest := sha42}, + "Bad digest type"), + %% XOF digests can't key an HMAC; reject with a clear badarg rather + %% than letting OpenSSL fail the derive with an opaque error. + kdf_expect_badarg(hkdf, <<"key">>, 32, GoodHkdf#{digest := shake256}, + "XOF digest not supported"), + kdf_expect_badarg(hkdf, <<"key">>, 32, GoodHkdf#{mode => bad_mode}, + "Bad hkdf mode"), + kdf_expect_badarg(hkdf, <<"key">>, 42, GoodHkdf#{mode => extract_only}, + "extract_only KeyLen must equal the digest size"), + %% HKDF-Expand caps output at 255 * HashLen (sha256 => 255*32 = 8160) + 8160 = byte_size(crypto:kdf(hkdf, <<"key">>, 8160, GoodHkdf)), + kdf_expect_badarg(hkdf, <<"key">>, 8161, GoodHkdf, + "KeyLen must be at most 255 times the digest size"), + %% salt is ignored in expand_only mode + kdf_expect_badarg(hkdf, <<"key">>, 32, + GoodHkdf#{salt => <<"s">>, mode => expand_only}, + "Option salt not used with mode => expand_only"), + %% info is ignored in extract_only mode + kdf_expect_badarg(hkdf, <<"key">>, 32, + GoodHkdf#{info => <<"info">>, mode => extract_only}, + "Option info not used with mode => extract_only"), + ok + end. + +kdf_pbkdf2_invalid_input() -> + [{doc, "Test kdf/4 with invalid pbkdf2 input"}]. +kdf_pbkdf2_invalid_input(Config) when is_list(Config) -> + case lists:member(pbkdf2, crypto:supports(kdfs)) of + false -> + {skip, "pbkdf2 not supported"}; + true -> + kdf_expect_badarg(pbkdf2, <<"p">>, 32, + #{digest => sha256, + salt => <<"s">>, + iterations => not_an_int}, + "Option iterations must be a positive integer"), + kdf_expect_badarg(pbkdf2, <<"p">>, 32, + #{digest => sha256, + salt => <<"s">>, + iterations => 0}, + "Option iterations must be a positive integer"), + %% digests without PBKDF2_ELIGIBLE_DIGEST are rejected. sha3_256 is + %% FIPS-approved (so it isn't rejected earlier as a FIPS-forbidden + %% digest) but is not PBKDF2-eligible, so this holds under FIPS too. + kdf_expect_badarg(pbkdf2, <<"p">>, 32, + #{digest => sha3_256, + salt => <<"s">>, + iterations => 1000}, + "Not eligible digest type"), + ok + end. + +kdf_argon2_invalid_input() -> + [{doc, "Test kdf/4 with invalid argon2 input"}]. +kdf_argon2_invalid_input(Config) when is_list(Config) -> + case lists:member(argon2id, crypto:supports(kdfs)) of + false -> + {skip, "argon2 not supported"}; + true -> + kdf_expect_badarg(argon2id, <<"p">>, 32, + #{salt => <<"0123456789abcdef">>, + memory => 1 bsl 32, + iterations => 3, + parallelism => 4}, + "Option memory is too large"), + %% keylen < 4 (RFC 9106 minimum tag length) + kdf_expect_badarg(argon2id, <<"p">>, 3, + #{salt => <<"0123456789abcdef">>, + memory => 65536, + iterations => 3, + parallelism => 4}, + "Argon2 KeyLen must be at least 4"), + %% parallelism > 2^24-1 + kdf_expect_badarg(argon2id, <<"p">>, 32, + #{salt => <<"0123456789abcdef">>, + memory => 65536, + iterations => 3, + parallelism => 1 bsl 24}, + "Option parallelism is too large"), + %% memory < 8 * parallelism + kdf_expect_badarg(argon2id, <<"p">>, 32, + #{salt => <<"0123456789abcdef">>, + memory => 8, + iterations => 3, + parallelism => 4}, + "Option memory must be at least 8 times parallelism"), + %% salt < 8 bytes (RFC 9106 minimum) + kdf_expect_badarg(argon2id, <<"p">>, 32, + #{salt => <<"short">>, + memory => 65536, + iterations => 3, + parallelism => 4}, + "Option salt must be at least 8 bytes"), + ok + end. + +kdf_scrypt_invalid_input() -> + [{doc, "Test kdf/4 with invalid scrypt input"}]. +kdf_scrypt_invalid_input(Config) when is_list(Config) -> + case lists:member(scrypt, crypto:supports(kdfs)) of + false -> + {skip, "scrypt not supported"}; + true -> + kdf_expect_badarg(scrypt, <<"p">>, 16, + #{salt => <<"s">>, n => 3, r => 1, p => 1}, + "Option n must be a power of two greater than 1"), + ok + end. + +kdf_sskdf_invalid_input() -> + [{doc, "Test kdf/4 with invalid sskdf input"}]. +kdf_sskdf_invalid_input(Config) when is_list(Config) -> + case lists:member(sskdf, crypto:supports(kdfs)) of + false -> + {skip, "sskdf not supported"}; + true -> + %% salt is only used as the HMAC key + kdf_expect_badarg(sskdf, <<"s">>, 16, + #{digest => sha256, salt => <<"x">>}, + "Option salt requires mac => hmac"), + %% XOF digests are rejected with a clear badarg + kdf_expect_badarg(sskdf, <<"s">>, 16, + #{digest => shake256}, + "XOF digest not supported"), + ok + end. + get_priv_pub_from_sign_verify(L) -> lists:foldl(fun get_priv_pub/2, [], L).