From 8a3dc7143c934a644d4c222d9b20371a1d32a7f1 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 16 Jan 2026 01:36:48 +0100 Subject: [PATCH 01/13] WIP: generate and compute key using PSA API Signed-off-by: Davide Bettio --- libs/estdlib/src/crypto.erl | 87 ++++- src/libAtomVM/otp_crypto.c | 450 ++++++++++++++++++++++++++ tests/erlang_tests/CMakeLists.txt | 4 + tests/erlang_tests/test_crypto_pk.erl | 68 ++++ tests/test.c | 2 + 5 files changed, 610 insertions(+), 1 deletion(-) create mode 100644 tests/erlang_tests/test_crypto_pk.erl diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index 1b16d749b4..bb3b515913 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -2,6 +2,7 @@ % This file is part of AtomVM. % % Copyright 2023 Fred Dushin +% Copyright 2026 Davide Bettio % % Licensed under the Apache License, Version 2.0 (the "License") % you may not use this file except in compliance with the License. @@ -23,7 +24,10 @@ hash/2, crypto_one_time/4, crypto_one_time/5, - strong_rand_bytes/1 + generate_key/2, + compute_key/4, + strong_rand_bytes/1, + info_lib/0 ]). -type hash_algorithm() :: md5 | sha | sha224 | sha256 | sha384 | sha512. @@ -50,6 +54,23 @@ -type crypto_opt() :: {encrypt, boolean()} | {padding, padding()}. -type crypto_opts() :: [crypto_opt()]. +-type pk_type() :: eddh | eddsa | ecdh. + +%% Curves/params accepted by the current AtomVM PSA implementation. +%% Note: not all combinations are supported by every function (see docs below). +-type pk_param() :: + x25519 + | x448 + | ed25519 + | ed448 + | secp256r1 + | secp384r1 + | secp521r1 + | secp256k1 + | brainpoolP256r1 + | brainpoolP384r1 + | brainpoolP512r1. + %%----------------------------------------------------------------------------- %% @param Type the hash algorithm %% @param Data the data to hash @@ -102,6 +123,53 @@ crypto_one_time(_Cipher, _Key, _Data, _FlagOrOptions) -> crypto_one_time(_Cipher, _Key, _IV, _Data, _FlagOrOptions) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Type the key algorithm family +%% @param Param curve/parameter selection +%% @returns Returns a tuple `{PublicKey, PrivateKey}`. +%% @doc Generate a public/private key pair using the AtomVM PSA backend. +%% +%% Supported forms: +%% * `eddh` with `x25519 | x448` +%% * `ecdh` with `x25519 | x448 | secp256k1 | secp256r1 | secp384r1 | secp521r1 | +%% brainpoolP256r1 | brainpoolP384r1 | brainpoolP512r1` +%% * `eddsa` with `ed25519 | ed448` (availability depends on the underlying mbedTLS build) +%% +%% Keys are returned as **raw exported key material** produced by the PSA API. +%% For secp* curves, the public key is the PSA “export_public_key” encoding +%% (typically an uncompressed EC point). +%% @end +%%----------------------------------------------------------------------------- +-spec generate_key(Type :: pk_type(), Param :: pk_param()) -> {binary(), binary()}. +generate_key(_Type, _Param) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Type the key agreement type +%% @param OtherPublicKey peer public key (binary) +%% @param MyPrivateKey local private key (binary) +%% @param Param curve/parameter selection +%% @returns Returns the shared secret as a binary. +%% @doc Compute a shared secret using AtomVM PSA raw key agreement. +%% +%% Supported forms: +%% * `eddh` with `x25519 | x448` +%% * `ecdh` with `x25519 | x448 | secp256k1 | secp256r1 | secp384r1 | secp521r1 | +%% brainpoolP256r1 | brainpoolP384r1 | brainpoolP512r1` +%% +%% The public/private key binaries must be in the same format as returned by +%% `generate_key/2`. +%% @end +%%----------------------------------------------------------------------------- +-spec compute_key( + Type :: pk_type(), + OtherPublicKey :: binary(), + MyPrivateKey :: binary(), + Param :: pk_param() +) -> binary(). +compute_key(_Type, _OtherPublicKey, _MyPrivateKey, _Param) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param N desired length of cryptographically secure random data %% @returns Returns Cryptographically secure random data of length `N' @@ -112,3 +180,20 @@ crypto_one_time(_Cipher, _Key, _IV, _Data, _FlagOrOptions) -> -spec strong_rand_bytes(N :: non_neg_integer()) -> binary(). strong_rand_bytes(_N) -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @returns Returns a list of tuples describing the crypto library name, version number, +%% and version string. +%% @doc Get the name and version of the libraries used by crypto. +%% +%% Returns a list containing a single tuple `{Name, VerNum, VerStr}` where: +%% * `Name` is the library name as a binary (e.g., `<<"mbedtls">>`) +%% * `VerNum` is the numeric version according to the library's versioning scheme +%% * `VerStr` is the version string as a binary +%% +%% Example: `[{<<"mbedtls">>, 50790144, <<"Mbed TLS 3.6.1">>}]` +%% @end +%%----------------------------------------------------------------------------- +-spec info_lib() -> [{binary(), integer(), binary()}]. +info_lib() -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index 704e1e1446..2ed149b9db 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -39,7 +39,12 @@ #include #include +#ifdef MBEDTLS_PSA_CRYPTO_C +#include +#endif + // #define ENABLE_TRACE +#include "term.h" #include "trace.h" #define MAX_MD_SIZE 64 @@ -564,6 +569,390 @@ static term nif_crypto_crypto_one_time(Context *ctx, int argc, term argv[]) RAISE_ERROR(make_crypto_error(__FILE__, source_line, err_msg, ctx)); } +#ifdef MBEDTLS_PSA_CRYPTO_C + +enum pk_type_t +{ + InvalidPkType = 0, + Eddh, + Eddsa, + Ecdh +}; + +// not working with latest mbedtls (yet): PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS)) +// Also tried Eddsa and failed with bits=255 +static const AtomStringIntPair pk_type_table[] = { + { ATOM_STR("\x4", "eddh"), Eddh }, + { ATOM_STR("\x5", "eddsa"), Eddsa }, + { ATOM_STR("\x4", "ecdh"), Ecdh }, + SELECT_INT_DEFAULT(InvalidPkType) +}; + +enum pk_param_t +{ + InvalidPkParam = 0, + X25519, + X448, + Ed25519, + Ed448, + Secp256r1, + // prime256v1 + Secp384r1, + Secp521r1, + Secp256k1, + BrainpoolP256r1, + BrainpoolP384r1, + BrainpoolP512r1 +}; + +static const AtomStringIntPair pk_param_table[] = { + { ATOM_STR("\x6", "x25519"), X25519 }, + { ATOM_STR("\x4", "x448"), X448 }, + { ATOM_STR("\x7", "ed25519"), Ed25519 }, + { ATOM_STR("\x5", "ed448"), Ed448 }, + + { ATOM_STR("\x9", "secp256r1"), Secp256r1 }, + // prime256v1 + { ATOM_STR("\x9", "secp384r1"), Secp384r1 }, + { ATOM_STR("\x9", "secp521r1"), Secp521r1 }, + { ATOM_STR("\x9", "secp256k1"), Secp256k1 }, + { ATOM_STR("\xF", "brainpoolP256r1"), BrainpoolP256r1 }, + { ATOM_STR("\xF", "brainpoolP384r1"), BrainpoolP384r1 }, + { ATOM_STR("\xF", "brainpoolP512r1"), BrainpoolP512r1 }, + + SELECT_INT_DEFAULT(InvalidPkType) +}; + +static void do_psa_init(void) +{ + if (UNLIKELY(psa_crypto_init() != PSA_SUCCESS)) { + abort(); + } +} + +static term nif_crypto_generate_key(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + enum pk_type_t key_type = interop_atom_term_select_int(pk_type_table, argv[0], glb); + enum pk_param_t pk_param = interop_atom_term_select_int(pk_param_table, argv[1], glb); + + psa_key_type_t psa_key_type; + size_t psa_key_bits; + switch (key_type) { + case Eddh: + // In OTP cotext: Eddh is Ecdh only on Montgomery curves + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY); + switch (pk_param) { + case X25519: + psa_key_bits = 255; + break; + case X448: + psa_key_bits = 448; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + break; + case Eddsa: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS); + switch (pk_param) { + case Ed25519: + psa_key_bits = 255; + break; + case Ed448: + psa_key_bits = 448; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + break; + case Ecdh: + switch (pk_param) { + case X25519: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY); + psa_key_bits = 255; + break; + case X448: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY); + psa_key_bits = 448; + break; + case Secp256k1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_K1); + psa_key_bits = 256; + break; + case Secp256r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 256; + break; + case Secp384r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 384; + break; + case Secp521r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 521; + break; + case BrainpoolP256r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 256; + break; + case BrainpoolP384r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 384; + break; + case BrainpoolP512r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 512; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + break; + + default: + RAISE_ERROR(BADARG_ATOM); + } + + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_EXPORT); + psa_set_key_type(&attributes, psa_key_type); + psa_set_key_bits(&attributes, psa_key_bits); + + psa_key_id_t key_id = 0; + psa_status_t status = psa_generate_key(&attributes, &key_id); + psa_reset_key_attributes(&attributes); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx)); + default: + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx)); + } + + bool successful = false; + term result = ERROR_ATOM; + + size_t exported_priv_size = PSA_KEY_EXPORT_ECC_KEY_PAIR_MAX_SIZE(psa_key_bits); + uint8_t *exported_priv = NULL; + size_t exported_pub_size = PSA_KEY_EXPORT_ECC_PUBLIC_KEY_MAX_SIZE(psa_key_bits); + uint8_t *exported_pub = NULL; + exported_priv = malloc(exported_priv_size); + exported_pub = malloc(exported_pub_size); + if (UNLIKELY(exported_priv == NULL || exported_pub == NULL)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + size_t exported_priv_length = 0; + status = psa_export_key(key_id, exported_priv, exported_priv_size, &exported_priv_length); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Failed private key export", ctx); + goto cleanup; + } + + size_t exported_pub_length = 0; + status = psa_export_public_key(key_id, exported_pub, exported_pub_size, &exported_pub_length); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Failed public key export", ctx); + goto cleanup; + } + + if (UNLIKELY(memory_ensure_free(ctx, + TERM_BINARY_HEAP_SIZE(exported_priv_length) + + TERM_BINARY_HEAP_SIZE(exported_pub_length) + TUPLE_SIZE(2)) + != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + term priv_term = term_from_literal_binary(exported_priv, exported_priv_length, &ctx->heap, glb); + term pub_term = term_from_literal_binary(exported_pub, exported_pub_length, &ctx->heap, glb); + successful = true; + result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, pub_term); + term_put_tuple_element(result, 1, priv_term); + +cleanup: + psa_destroy_key(key_id); + if (exported_priv) { + memset(exported_priv, 0, exported_priv_size); + } + free(exported_priv); + if (exported_pub) { + memset(exported_pub, 0, exported_pub_size); + } + free(exported_pub); + + if (UNLIKELY(!successful)) { + RAISE_ERROR(result); + } + + return result; +} + +static term nif_crypto_compute_key(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + enum pk_type_t key_type = interop_atom_term_select_int(pk_type_table, argv[0], glb); + enum pk_param_t pk_param = interop_atom_term_select_int(pk_param_table, argv[3], glb); + + psa_algorithm_t psa_algo; + psa_key_type_t psa_key_type; + size_t psa_key_bits; + + switch (key_type) { + case Eddh: + // In OTP cotext: Eddh is Ecdh only on Montgomery curves + psa_algo = PSA_ALG_ECDH; + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY); + switch (pk_param) { + case X25519: + psa_key_bits = 255; + break; + case X448: + psa_key_bits = 448; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + break; + case Ecdh: + psa_algo = PSA_ALG_ECDH; + switch (pk_param) { + case X25519: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY); + psa_key_bits = 255; + break; + case X448: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY); + psa_key_bits = 448; + break; + case Secp256k1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_K1); + psa_key_bits = 256; + break; + case Secp256r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 256; + break; + case Secp384r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 384; + break; + case Secp521r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 521; + break; + case BrainpoolP256r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 256; + break; + case BrainpoolP384r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 384; + break; + case BrainpoolP512r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 512; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + break; + + default: + RAISE_ERROR(BADARG_ATOM); + } + + term pub_key_term = argv[1]; + VALIDATE_VALUE(pub_key_term, term_is_binary); + const void *pub_key = term_binary_data(pub_key_term); + size_t pub_key_size = term_binary_size(pub_key_term); + + term priv_key_term = argv[2]; + VALIDATE_VALUE(priv_key_term, term_is_binary); + const void *priv_key = term_binary_data(priv_key_term); + size_t priv_key_size = term_binary_size(priv_key_term); + + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, psa_key_type); + psa_set_key_bits(&attr, psa_key_bits); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_DERIVE); + psa_set_key_algorithm(&attr, psa_algo); + + psa_key_id_t key_id = 0; + psa_status_t status = psa_import_key(&attr, priv_key, priv_key_size, &key_id); + psa_reset_key_attributes(&attr); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx)); + default: + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx)); + } + + bool success = false; + term result = ERROR_ATOM; + + size_t shared_out_size = PSA_RAW_KEY_AGREEMENT_OUTPUT_SIZE(psa_key_type, psa_key_bits); + uint8_t *shared_out = malloc(shared_out_size); + if (IS_NULL_PTR(shared_out)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + size_t shared_len = 0; + status = psa_raw_key_agreement( + psa_algo, key_id, pub_key, pub_key_size, shared_out, shared_out_size, &shared_len); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + result + = make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx); + goto cleanup; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(shared_len)) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + success = true; + result = term_from_literal_binary(shared_out, shared_len, &ctx->heap, glb); + +cleanup: + psa_destroy_key(key_id); + if (shared_out) { + memset(shared_out, 0, shared_out_size); + } + free(shared_out); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + +#endif + // not static since we are using it elsewhere to provide backward compatibility term nif_crypto_strong_rand_bytes(Context *ctx, int argc, term argv[]) { @@ -598,6 +987,39 @@ term nif_crypto_strong_rand_bytes(Context *ctx, int argc, term argv[]) return out_bin; } +term nif_crypto_info_lib(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + UNUSED(argv); + + const char *mbedtls_str = "mbedtls"; + size_t mbedtls_len = strlen("mbedtls"); + + // 18 bytes including null byte according to mbedtls doc + char version_string[18]; + mbedtls_version_get_string_full(version_string); + size_t version_string_len = strlen(version_string); + + if (UNLIKELY(memory_ensure_free(ctx, + LIST_SIZE(1, TUPLE_SIZE(3)) + TERM_BINARY_HEAP_SIZE(mbedtls_len) + + TERM_BINARY_HEAP_SIZE(version_string_len) + BOXED_INT64_SIZE) + != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term mbedtls_term = term_from_literal_binary(mbedtls_str, mbedtls_len, &ctx->heap, ctx->global); + term version_term = term_make_maybe_boxed_int64(MBEDTLS_VERSION_NUMBER, &ctx->heap); + term version_string_term + = term_from_literal_binary(version_string, version_string_len, &ctx->heap, ctx->global); + + term mbedtls_tuple = term_alloc_tuple(3, &ctx->heap); + term_put_tuple_element(mbedtls_tuple, 0, mbedtls_term); + term_put_tuple_element(mbedtls_tuple, 1, version_term); + term_put_tuple_element(mbedtls_tuple, 2, version_string_term); + + return term_list_prepend(mbedtls_tuple, term_nil(), &ctx->heap); +} + static const struct Nif crypto_hash_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_hash @@ -606,10 +1028,24 @@ static const struct Nif crypto_crypto_one_time_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_crypto_one_time }; +#ifdef MBEDTLS_PSA_CRYPTO_C +static const struct Nif crypto_generate_key_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_generate_key +}; +static const struct Nif crypto_compute_key_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_compute_key +}; +#endif static const struct Nif crypto_strong_rand_bytes_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_strong_rand_bytes }; +static const struct Nif crypto_info_lib = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_info_lib +}; // // Entrypoints @@ -631,10 +1067,24 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_crypto_one_time_nif; } +#ifdef MBEDTLS_PSA_CRYPTO_C + if (strcmp("generate_key/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_generate_key_nif; + } + if (strcmp("compute_key/4", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_compute_key_nif; + } +#endif if (strcmp("strong_rand_bytes/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_strong_rand_bytes_nif; } + if (strcmp("info_lib/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_info_lib; + } } return NULL; } diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 2d96b4a1a4..a510c38625 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -660,6 +660,8 @@ else() set(OTP25_OR_GREATER_TESTS) endif() +compile_erlang(test_crypto_pk) + set(erlang_test_beams add.beam fact.beam @@ -1196,6 +1198,8 @@ set(erlang_test_beams ${OTP23_OR_GREATER_TESTS} ${OTP25_OR_GREATER_TESTS} + + test_crypto_pk.beam ) if(NOT AVM_DISABLE_JIT) diff --git a/tests/erlang_tests/test_crypto_pk.erl b/tests/erlang_tests/test_crypto_pk.erl new file mode 100644 index 0000000000..69c1be35eb --- /dev/null +++ b/tests/erlang_tests/test_crypto_pk.erl @@ -0,0 +1,68 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_crypto_pk). +-export([start/0, test_generate_and_compute_key/0]). + +start() -> + ok = mbedtls_conditional_run(test_generate_and_compute_key, 16#03000000), + 0. + +mbedtls_conditional_run(F, RVer) -> + Info = crypto:info_lib(), + case find_openssl_or_mbedtls_ver(Info, RVer) of + true -> + ?MODULE:F(); + false -> + erlang:display({skipped, ?MODULE, F}), + ok + end. + +find_openssl_or_mbedtls_ver([], _RVer) -> + false; +find_openssl_or_mbedtls_ver([{<<"OpenSSL">>, _, _} | _T], _RVer) -> + true; +find_openssl_or_mbedtls_ver([{<<"mbedtls">>, Ver, _} | _T], RVer) when Ver >= RVer -> + true; +find_openssl_or_mbedtls_ver([_ | T], RVer) -> + find_openssl_or_mbedtls_ver(T, RVer). + +test_generate_and_compute_key() -> + {Pub, Priv} = crypto:generate_key(eddh, x25519), + true = is_binary(Pub), + 32 = byte_size(Pub), + true = is_binary(Priv), + 32 = byte_size(Priv), + + ComputedKey = crypto:compute_key(eddh, Pub, Priv, x25519), + true = is_binary(ComputedKey), + 32 = byte_size(ComputedKey), + + {Pub2, Priv2} = crypto:generate_key(eddh, x25519), + true = is_binary(Pub2), + 32 = byte_size(Pub2), + true = is_binary(Priv2), + 32 = byte_size(Priv2), + + ComputedKey2 = crypto:compute_key(eddh, Pub2, Priv2, x25519), + true = is_binary(ComputedKey2), + 32 = byte_size(ComputedKey2), + + ok. diff --git a/tests/test.c b/tests/test.c index 8ed932e7fc..843b6e1fcb 100644 --- a/tests/test.c +++ b/tests/test.c @@ -626,6 +626,8 @@ struct Test tests[] = { TEST_CASE(test_inline_arith), + TEST_CASE(test_crypto_pk), + // TEST CRASHES HERE: TEST_CASE(memlimit), { NULL, 0, false, false } From 47deaf9bceff58ec5c9dd7e8cc0928992d85edf1 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 22 Jan 2026 15:18:43 +0100 Subject: [PATCH 02/13] WIP: Add support to crypto:mac Signed-off-by: Davide Bettio --- libs/estdlib/src/crypto.erl | 38 ++++++ src/libAtomVM/otp_crypto.c | 176 +++++++++++++++++++++++++ tests/erlang_tests/CMakeLists.txt | 2 + tests/erlang_tests/test_crypto_mac.erl | 167 +++++++++++++++++++++++ tests/test.c | 1 + 5 files changed, 384 insertions(+) create mode 100644 tests/erlang_tests/test_crypto_mac.erl diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index bb3b515913..1543cd91e3 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -26,6 +26,7 @@ crypto_one_time/5, generate_key/2, compute_key/4, + mac/4, strong_rand_bytes/1, info_lib/0 ]). @@ -71,6 +72,18 @@ | brainpoolP384r1 | brainpoolP512r1. +-type mac_type() :: cmac | hmac. + +-type cmac_subtype() :: + aes_128_cbc + | aes_128_ecb + | aes_192_cbc + | aes_192_ecb + | aes_256_cbc + | aes_256_ecb. + +-type mac_subtype() :: cmac_subtype() | hash_algorithm() | ripemd160. + %%----------------------------------------------------------------------------- %% @param Type the hash algorithm %% @param Data the data to hash @@ -170,6 +183,31 @@ generate_key(_Type, _Param) -> compute_key(_Type, _OtherPublicKey, _MyPrivateKey, _Param) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Type MAC algorithm family (`cmac` or `hmac`) +%% @param SubType MAC subtype (cipher for CMAC, digest for HMAC) +%% @param Key MAC key bytes +%% @param Data message bytes (iodata) +%% @returns Returns the computed MAC as a binary. +%% @doc Compute a MAC using the AtomVM PSA backend. +%% +%% Supported forms: +%% * `mac(cmac, Cipher, Key, Data)` where `Cipher` selects AES key size: +%% `aes_128_(cbc|ecb) | aes_192_(cbc|ecb) | aes_256_(cbc|ecb)` +%% (key length must match the selected size) +%% * `mac(hmac, Digest, Key, Data)` where `Digest` is a supported hash atom +%% (AtomVM accepts `ripemd160` in addition to the `hash/2` digests) +%% @end +%%----------------------------------------------------------------------------- +-spec mac( + Type :: mac_type(), + SubType :: mac_subtype(), + Key :: binary(), + Data :: iodata() +) -> binary(). +mac(_Type, _SubType, _Key, _Data) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param N desired length of cryptographically secure random data %% @returns Returns Cryptographically secure random data of length `N' diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index 2ed149b9db..4b80c4c881 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -951,6 +951,174 @@ static term nif_crypto_compute_key(Context *ctx, int argc, term argv[]) return result; } +static const AtomStringIntPair hmac_algorithm_table[] = { + { ATOM_STR("\x3", "sha"), PSA_ALG_SHA_1 }, + { ATOM_STR("\x6", "sha224"), PSA_ALG_SHA_224 }, + { ATOM_STR("\x6", "sha256"), PSA_ALG_SHA_256 }, + { ATOM_STR("\x6", "sha384"), PSA_ALG_SHA_384 }, + { ATOM_STR("\x6", "sha512"), PSA_ALG_SHA_512 }, + { ATOM_STR("\x8", "sha3_224"), PSA_ALG_SHA3_224 }, + { ATOM_STR("\x8", "sha3_256"), PSA_ALG_SHA3_256 }, + { ATOM_STR("\x8", "sha3_384"), PSA_ALG_SHA3_384 }, + { ATOM_STR("\x8", "sha3_512"), PSA_ALG_SHA3_512 }, + { ATOM_STR("\x3", "md5"), PSA_ALG_MD5 }, + { ATOM_STR("\x9", "ripemd160"), PSA_ALG_RIPEMD160 }, + + SELECT_INT_DEFAULT(PSA_ALG_NONE) +}; + +static const AtomStringIntPair cmac_algorithm_bits_table[] = { + { ATOM_STR("\xB", "aes_128_cbc"), 128 }, + { ATOM_STR("\xB", "aes_128_ecb"), 128 }, + { ATOM_STR("\xB", "aes_192_cbc"), 192 }, + { ATOM_STR("\xB", "aes_192_ecb"), 192 }, + { ATOM_STR("\xB", "aes_256_cbc"), 256 }, + { ATOM_STR("\xB", "aes_256_ecb"), 256 }, + + SELECT_INT_DEFAULT(0) +}; + +static const AtomStringIntPair mac_key_table[] = { + { ATOM_STR("\x4", "cmac"), PSA_KEY_TYPE_AES }, + { ATOM_STR("\x4", "hmac"), PSA_KEY_TYPE_HMAC }, + + SELECT_INT_DEFAULT(PSA_KEY_TYPE_NONE) +}; + +static term nif_crypto_mac(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + term mac_type_term = argv[0]; + term sub_type_term = argv[1]; + + // argv[2] is key, will handle here bellow + // argv[3] is data, will handle it later + + bool success = false; + term result = ERROR_ATOM; + psa_key_id_t key_id = 0; + void *maybe_allocated_key = NULL; + void *maybe_allocated_data = NULL; + size_t mac_out_size = 0; + void *mac_out = NULL; + + term key_term = argv[2]; + const void *key; + size_t key_len; + term iodata_handle_result = handle_iodata(key_term, &key, &key_len, &maybe_allocated_key); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx)); + } + + psa_key_type_t psa_key_type = interop_atom_term_select_int(mac_key_table, mac_type_term, glb); + psa_algorithm_t psa_key_algo; + psa_key_bits_t key_bit_size; + switch (psa_key_type) { + case PSA_KEY_TYPE_AES: + psa_key_algo = PSA_ALG_CMAC; + key_bit_size + = interop_atom_term_select_int(cmac_algorithm_bits_table, sub_type_term, glb); + if (UNLIKELY(key_bit_size == 0)) { + result = make_crypto_error(__FILE__, __LINE__, "Unknown cipher", ctx); + goto cleanup; + } + + if (UNLIKELY(key_bit_size != key_len * 8)) { + result = make_crypto_error(__FILE__, __LINE__, "Bad key size", ctx); + goto cleanup; + } + break; + case PSA_KEY_TYPE_HMAC: { + psa_algorithm_t sub_type_algo + = interop_atom_term_select_int(hmac_algorithm_table, sub_type_term, glb); + if (UNLIKELY(sub_type_algo == PSA_ALG_NONE)) { + result + = make_crypto_error(__FILE__, __LINE__, "Bad digest algorithm for HMAC", ctx); + goto cleanup; + } + psa_key_algo = PSA_ALG_HMAC(sub_type_algo); + key_bit_size = key_len * 8; + } break; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unknown mac algorithm", ctx); + goto cleanup; + } + + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, psa_key_type); + psa_set_key_bits(&attr, key_bit_size); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE); + psa_set_key_algorithm(&attr, psa_key_algo); + + psa_status_t status = psa_import_key(&attr, key, key_len, &key_id); + psa_reset_key_attributes(&attr); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unsupported algorithm", ctx)); + default: + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx)); + } + + term data_term = argv[3]; + const void *data; + size_t data_len; + iodata_handle_result = handle_iodata(data_term, &data, &data_len, &maybe_allocated_data); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + result = make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx); + goto cleanup; + } + + mac_out_size = PSA_MAC_LENGTH(psa_key_type, key_bit_size, psa_key_algo); + mac_out = malloc(mac_out_size); + if (IS_NULL_PTR(mac_out)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + size_t mac_len = 0; + status = psa_mac_compute(key_id, psa_key_algo, data, data_len, mac_out, mac_out_size, &mac_len); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + result = make_crypto_error(__FILE__, __LINE__, "Unsupported algorithm", ctx); + goto cleanup; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(mac_len)) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + success = true; + result = term_from_literal_binary(mac_out, mac_len, &ctx->heap, glb); + +cleanup: + psa_destroy_key(key_id); + if (mac_out) { + memset(mac_out, 0, mac_out_size); + } + free(mac_out); + free(maybe_allocated_key); + free(maybe_allocated_data); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + #endif // not static since we are using it elsewhere to provide backward compatibility @@ -1037,6 +1205,10 @@ static const struct Nif crypto_compute_key_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_compute_key }; +static const struct Nif crypto_mac_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_mac +}; #endif static const struct Nif crypto_strong_rand_bytes_nif = { .base.type = NIFFunctionType, @@ -1076,6 +1248,10 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_compute_key_nif; } + if (strcmp("mac/4", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_mac_nif; + } #endif if (strcmp("strong_rand_bytes/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index a510c38625..29c18f77ca 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -661,6 +661,7 @@ else() endif() compile_erlang(test_crypto_pk) +compile_erlang(test_crypto_mac) set(erlang_test_beams add.beam @@ -1200,6 +1201,7 @@ set(erlang_test_beams ${OTP25_OR_GREATER_TESTS} test_crypto_pk.beam + test_crypto_mac.beam ) if(NOT AVM_DISABLE_JIT) diff --git a/tests/erlang_tests/test_crypto_mac.erl b/tests/erlang_tests/test_crypto_mac.erl new file mode 100644 index 0000000000..a68b8860f2 --- /dev/null +++ b/tests/erlang_tests/test_crypto_mac.erl @@ -0,0 +1,167 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_crypto_mac). +-export([start/0, test_hmac/0, test_cmac/0, test_hmac_iolist/0, test_cmac_iolist/0]). + +start() -> + ok = mbedtls_conditional_run(test_hmac, 16#03000000), + ok = mbedtls_conditional_run(test_cmac, 16#03000000), + ok = mbedtls_conditional_run(test_hmac_iolist, 16#03000000), + ok = mbedtls_conditional_run(test_cmac_iolist, 16#03000000), + 0. + +mbedtls_conditional_run(F, RVer) -> + Info = crypto:info_lib(), + case find_openssl_or_mbedtls_ver(Info, RVer) of + true -> + ?MODULE:F(); + false -> + erlang:display({skipped, ?MODULE, F}), + ok + end. + +find_openssl_or_mbedtls_ver([], _RVer) -> + false; +find_openssl_or_mbedtls_ver([{<<"OpenSSL">>, _, _} | _T], _RVer) -> + true; +find_openssl_or_mbedtls_ver([{<<"mbedtls">>, Ver, _} | _T], RVer) when Ver >= RVer -> + true; +find_openssl_or_mbedtls_ver([_ | T], RVer) -> + find_openssl_or_mbedtls_ver(T, RVer). + +test_hmac() -> + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, 176>> = crypto:mac( + hmac, sha256, <<"Hello">>, <<"Data">> + ), + + <<153, 146, 251, 20, 217, 139, 50, 190, 240, 28, 191, 144, 120, 206, 138, 44, 47, 139, 14, 233, + 146, 3, 76, 170, 214, 207, 208, 7, 109, 0, 155, 23>> = crypto:mac( + hmac, sha256, <<"Hello">>, <<"">> + ), + + {error, {badarg, {File, Line}, "Bad digest algorithm for HMAC"}} = expect_error(fun() -> + crypto:mac(hmac, sha89, <<"Key">>, <<"Data">>) + end), + true = is_list(File), + true = is_integer(Line) and (Line >= 0), + + ok. + +test_cmac() -> + <<227, 175, 5, 166, 7, 167, 180, 81, 30, 6, 147, 30, 211, 6, 207, 186>> = crypto:mac( + cmac, aes_128_cbc, <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, <<"Data">> + ), + + <<19, 247, 81, 225, 123, 105, 6, 29, 226, 176, 251, 80, 224, 17, 174, 122>> = crypto:mac( + cmac, aes_128_cbc, <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, <<"More Data">> + ), + + <<151, 221, 110, 90, 136, 44, 189, 86, 76, 57, 174, 125, 28, 90, 49, 170>> = crypto:mac( + cmac, aes_128_cbc, <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, <<"">> + ), + + {error, {badarg, {File, Line}, "Bad key size"}} = expect_error(fun() -> + crypto:mac( + cmac, aes_128_cbc, <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14>>, <<"Data">> + ) + end), + true = is_list(File), + true = is_integer(Line) and (Line >= 0), + + {error, {badarg, {File2, Line2}, "Unknown cipher"}} = expect_error(fun() -> + crypto:mac(cmac, none, <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14>>, <<"Data">>) + end), + true = is_list(File2), + true = is_integer(Line2) and (Line2 >= 0), + + ok. + +test_hmac_iolist() -> + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, 176>> = crypto:mac( + hmac, sha256, [$H, <<"ell">>, <<"o">>], <<"Data">> + ), + + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, + 176>> = crypto:mac( + hmac, sha256, <<"Hello">>, [$D, <<"at">>, <<"a">>] + ), + + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, + 176>> = crypto:mac( + hmac, sha256, [<<"He">>, $l, $l, <<"o">>], [<<"Da">>, <<"ta">>] + ), + + <<153, 146, 251, 20, 217, 139, 50, 190, 240, 28, 191, 144, 120, 206, 138, 44, 47, 139, 14, 233, + 146, 3, 76, 170, 214, 207, 208, 7, 109, 0, 155, + 23>> = crypto:mac( + hmac, sha256, <<"Hello">>, [<<"">>] + ), + + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, + 176>> = crypto:mac( + hmac, sha256, <<"Hello">>, [[[$D], <<"a">>], <<"ta">>] + ), + + ok. + +test_cmac_iolist() -> + <<227, 175, 5, 166, 7, 167, 180, 81, 30, 6, 147, 30, 211, 6, 207, 186>> = crypto:mac( + cmac, + aes_128_cbc, + [<<0, 1, 2, 3>>, <<4, 5, 6, 7>>, <<8, 9, 10, 11, 12, 13, 14, 15>>], + <<"Data">> + ), + + <<227, 175, 5, 166, 7, 167, 180, 81, 30, 6, 147, 30, 211, 6, 207, 186>> = crypto:mac( + cmac, aes_128_cbc, <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, [$D, <<"ata">>] + ), + + <<227, 175, 5, 166, 7, 167, 180, 81, 30, 6, 147, 30, 211, 6, 207, 186>> = crypto:mac( + cmac, + aes_128_cbc, + [<<0, 1, 2, 3, 4, 5, 6, 7>>, <<8, 9, 10, 11, 12, 13, 14, 15>>], + [<<"Da">>, <<"ta">>] + ), + + <<19, 247, 81, 225, 123, 105, 6, 29, 226, 176, 251, 80, 224, 17, 174, 122>> = crypto:mac( + cmac, + aes_128_cbc, + <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + [<<"More">>, <<" ">>, <<"Data">>] + ), + + <<151, 221, 110, 90, 136, 44, 189, 86, 76, 57, 174, 125, 28, 90, 49, 170>> = crypto:mac( + cmac, aes_128_cbc, <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, [<<"">>] + ), + + ok. + +expect_error(Fun) -> + try Fun() of + Res -> {unexpected, Res} + catch + C:R -> {C, R} + end. diff --git a/tests/test.c b/tests/test.c index 843b6e1fcb..6a72c38ee2 100644 --- a/tests/test.c +++ b/tests/test.c @@ -627,6 +627,7 @@ struct Test tests[] = { TEST_CASE(test_inline_arith), TEST_CASE(test_crypto_pk), + TEST_CASE(test_crypto_mac), // TEST CRASHES HERE: TEST_CASE(memlimit), From 87800d7f587c572311c3018db4e5aaad08fd5ec7 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 25 Jan 2026 16:49:50 +0100 Subject: [PATCH 03/13] Sign and verify function Signed-off-by: Davide Bettio --- libs/estdlib/src/crypto.erl | 69 +++++ src/libAtomVM/otp_crypto.c | 412 +++++++++++++++++++++++++- tests/erlang_tests/test_crypto_pk.erl | 115 ++++++- 3 files changed, 591 insertions(+), 5 deletions(-) diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index 1543cd91e3..69e21d8720 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -26,6 +26,8 @@ crypto_one_time/5, generate_key/2, compute_key/4, + sign/4, + verify/5, mac/4, strong_rand_bytes/1, info_lib/0 @@ -72,6 +74,19 @@ | brainpoolP384r1 | brainpoolP512r1. +%% ECDSA is currently supported only on short Weierstrass secp* and brainpool curves. +-type ecdsa_curve() :: + secp256k1 + | secp256r1 + | secp384r1 + | secp521r1 + | brainpoolP256r1 + | brainpoolP384r1 + | brainpoolP512r1. + +-type ecdsa_private_key() :: [binary() | ecdsa_curve()]. +-type ecdsa_public_key() :: [binary() | ecdsa_curve()]. + -type mac_type() :: cmac | hmac. -type cmac_subtype() :: @@ -183,6 +198,60 @@ generate_key(_Type, _Param) -> compute_key(_Type, _OtherPublicKey, _MyPrivateKey, _Param) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Algorithm signing algorithm (AtomVM supports `ecdsa`) +%% @param DigestType hash algorithm identifier +%% @param Data message bytes (iodata) +%% @param Key signing key material +%% @returns Returns a DER-encoded signature as a binary. +%% @doc Create a digital signature using the AtomVM PSA backend. +%% +%% AtomVM currently supports: +%% * `Algorithm = ecdsa` +%% * `Key = [PrivateKeyBin, Curve]` where `Curve` is one of +%% `secp256k1 | secp256r1 | secp384r1 | secp521r1 | +%% brainpoolP256r1 | brainpoolP384r1 | brainpoolP512r1` +%% +%% The signature is returned in **DER** form. +%% @end +%%----------------------------------------------------------------------------- +-spec sign( + Algorithm :: ecdsa, + DigestType :: hash_algorithm(), + Data :: iodata(), + Key :: ecdsa_private_key() +) -> binary(). +sign(_Algorithm, _DigestType, _Data, _Key) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Algorithm verification algorithm (AtomVM supports `ecdsa`) +%% @param DigestType hash algorithm identifier +%% @param Data message bytes (iodata) +%% @param Signature DER-encoded signature +%% @param Key verification key material +%% @returns Returns `true` if the signature is valid, otherwise `false`. +%% @doc Verify a digital signature using the AtomVM PSA backend. +%% +%% AtomVM currently supports: +%% * `Algorithm = ecdsa` +%% * `Key = [PublicKeyBin, Curve]` where `Curve` is one of +%% `secp256k1 | secp256r1 | secp384r1 | secp521r1 | +%% brainpoolP256r1 | brainpoolP384r1 | brainpoolP512r1` +%% +%% Invalid DER signatures yield `false` (not an exception). +%% @end +%%----------------------------------------------------------------------------- +-spec verify( + Algorithm :: ecdsa, + DigestType :: hash_algorithm(), + Data :: iodata(), + Signature :: binary(), + Key :: ecdsa_public_key() +) -> boolean(). +verify(_Algorithm, _DigestType, _Data, _Signature, _Key) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Type MAC algorithm family (`cmac` or `hmac`) %% @param SubType MAC subtype (cipher for CMAC, digest for HMAC) diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index 4b80c4c881..441903dff3 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -40,6 +40,7 @@ #include #ifdef MBEDTLS_PSA_CRYPTO_C +#include #include #endif @@ -47,6 +48,11 @@ #include "term.h" #include "trace.h" +#if MBEDTLS_VERSION_NUMBER > 0x03060100 +#define HAVE_MBEDTLS_ECDSA_RAW_TO_DER 1 +#define HAVE_MBEDTLS_ECDSA_DER_TO_RAW 1 +#endif + #define MAX_MD_SIZE 64 enum crypto_algorithm @@ -372,7 +378,8 @@ static bool bool_to_mbedtls_operation(term encrypt_flag, mbedtls_operation_t *op } } -static term make_crypto_error(const char *file, int line, const char *message, Context *ctx) +static term make_crypto_error_tag( + const char *file, int line, const char *message, term tag, Context *ctx) { int err_needed_mem = (strlen(file) * CONS_SIZE) + TUPLE_SIZE(2) + (strlen(message) * CONS_SIZE) + TUPLE_SIZE(3); @@ -389,13 +396,18 @@ static term make_crypto_error(const char *file, int line, const char *message, C term message_t = interop_bytes_to_list(message, strlen(message), &ctx->heap); term err_t = term_alloc_tuple(3, &ctx->heap); - term_put_tuple_element(err_t, 0, BADARG_ATOM); + term_put_tuple_element(err_t, 0, tag); term_put_tuple_element(err_t, 1, file_line_t); term_put_tuple_element(err_t, 2, message_t); return err_t; } +static term make_crypto_error(const char *file, int line, const char *message, Context *ctx) +{ + return make_crypto_error_tag(file, line, message, BADARG_ATOM, ctx); +} + static term nif_crypto_crypto_one_time(Context *ctx, int argc, term argv[]) { bool has_iv = argc == 5; @@ -951,7 +963,7 @@ static term nif_crypto_compute_key(Context *ctx, int argc, term argv[]) return result; } -static const AtomStringIntPair hmac_algorithm_table[] = { +static const AtomStringIntPair psa_hash_algorithm_table[] = { { ATOM_STR("\x3", "sha"), PSA_ALG_SHA_1 }, { ATOM_STR("\x6", "sha224"), PSA_ALG_SHA_224 }, { ATOM_STR("\x6", "sha256"), PSA_ALG_SHA_256 }, @@ -967,6 +979,374 @@ static const AtomStringIntPair hmac_algorithm_table[] = { SELECT_INT_DEFAULT(PSA_ALG_NONE) }; +#ifdef HAVE_MBEDTLS_ECDSA_RAW_TO_DER + +#define CRYPTO_SIGN_AVAILABLE 1 + +static term nif_crypto_sign(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + term alg_term = argv[0]; + if (UNLIKELY( + !globalcontext_is_term_equal_to_atom_string(glb, alg_term, ATOM_STR("\x5", "ecdsa")))) { + RAISE_ERROR( + make_crypto_error_tag(__FILE__, __LINE__, "Invalid public key", ERROR_ATOM, ctx)); + } + + term hash_algo_term = argv[1]; + psa_algorithm_t hash_algo + = interop_atom_term_select_int(psa_hash_algorithm_table, hash_algo_term, glb); + if (UNLIKELY(hash_algo == PSA_ALG_NONE)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad digest type", ctx)); + } + psa_algorithm_t psa_key_alg = PSA_ALG_ECDSA(hash_algo); + + // argv[2] is data, will handle later + + term key_list_term = argv[3]; + if (UNLIKELY(!term_is_nonempty_list(key_list_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA private key", ctx)); + } + + term priv_term = term_get_list_head(key_list_term); + if (UNLIKELY(!term_is_binary(priv_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA private key", ctx)); + } + const void *priv = term_binary_data(priv_term); + size_t priv_len = term_binary_size(priv_term); + + term key_list_term_tail = term_get_list_tail(key_list_term); + if (UNLIKELY(!term_is_nonempty_list(key_list_term_tail))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA private key", ctx)); + } + term priv_param_term = term_get_list_head(key_list_term_tail); + + enum pk_param_t pk_param = interop_atom_term_select_int(pk_param_table, priv_param_term, glb); + psa_key_type_t psa_key_type; + size_t psa_key_bits; + + switch (pk_param) { + case Secp256k1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_K1); + psa_key_bits = 256; + break; + case Secp256r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 256; + break; + case Secp384r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 384; + break; + case Secp521r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 521; + break; + case BrainpoolP256r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 256; + break; + case BrainpoolP384r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 384; + break; + case BrainpoolP512r1: + psa_key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 512; + break; + default: + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA private key", ctx)); + } + + if (UNLIKELY(priv_len != PSA_BITS_TO_BYTES(psa_key_bits))) { + // OTP even accepts empty binaries as keys, PSA API doesn't like it + // so we rather fail before with an understandable error message + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA private key", ctx)); + } + + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, psa_key_type); + psa_set_key_bits(&attr, psa_key_bits); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE); + psa_set_key_algorithm(&attr, psa_key_alg); + + psa_key_id_t key_id = 0; + psa_status_t status = psa_import_key(&attr, priv, priv_len, &key_id); + psa_reset_key_attributes(&attr); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx)); + default: + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx)); + } + + term result = ERROR_ATOM; + bool success = false; + + size_t sig_raw_size = PSA_ECDSA_SIGNATURE_SIZE(psa_key_bits); + uint8_t *sig_raw = NULL; + size_t sig_der_size = MBEDTLS_ECDSA_MAX_SIG_LEN(psa_key_bits); + void *sig_der = NULL; + void *maybe_allocated_data = NULL; + + term data_term = argv[2]; + const void *data; + size_t data_len; + term iodata_handle_result = handle_iodata(data_term, &data, &data_len, &maybe_allocated_data); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + result = make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx); + goto cleanup; + } + + sig_raw = malloc(sig_raw_size); + if (IS_NULL_PTR(sig_raw)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + size_t sig_raw_len = 0; + status = psa_sign_message( + key_id, psa_key_alg, data, data_len, sig_raw, sig_raw_size, &sig_raw_len); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + result + = make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx); + goto cleanup; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + assert(sig_raw_len == sig_raw_size); + + sig_der = malloc(sig_der_size); + if (IS_NULL_PTR(sig_der)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + size_t sig_der_len = 0; + int ret = mbedtls_ecdsa_raw_to_der( + psa_key_bits, sig_raw, sig_raw_len, sig_der, sig_der_size, &sig_der_len); + if (ret != 0) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(sig_der_len)) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + success = true; + result = term_from_literal_binary(sig_der, sig_der_len, &ctx->heap, glb); + +cleanup: + psa_destroy_key(key_id); + + free(maybe_allocated_data); + free(sig_raw); + free(sig_der); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + +#endif + +#ifdef HAVE_MBEDTLS_ECDSA_DER_TO_RAW + +#define CRYPTO_VERIFY_AVAILABLE 1 + +static term nif_crypto_verify(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + term alg_term = argv[0]; + if (UNLIKELY( + !globalcontext_is_term_equal_to_atom_string(glb, alg_term, ATOM_STR("\x5", "ecdsa")))) { + RAISE_ERROR( + make_crypto_error_tag(__FILE__, __LINE__, "Invalid public key", ERROR_ATOM, ctx)); + } + + term hash_algo_term = argv[1]; + psa_algorithm_t hash_algo + = interop_atom_term_select_int(psa_hash_algorithm_table, hash_algo_term, glb); + if (UNLIKELY(hash_algo == PSA_ALG_NONE)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad digest type", ctx)); + } + psa_algorithm_t psa_key_alg = PSA_ALG_ECDSA(hash_algo); + + // argv[2] is data, will handle it later + + term sig_der_term = argv[3]; + VALIDATE_VALUE(sig_der_term, term_is_binary); + const void *sig_der = term_binary_data(sig_der_term); + size_t sig_der_len = term_binary_size(sig_der_term); + + term key_list_term = argv[4]; + if (UNLIKELY(!term_is_nonempty_list(key_list_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA public key", ctx)); + } + + term pub_term = term_get_list_head(key_list_term); + if (UNLIKELY(!term_is_binary(pub_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA public key", ctx)); + } + const void *pub = term_binary_data(pub_term); + size_t pub_len = term_binary_size(pub_term); + + term key_list_term_tail = term_get_list_tail(key_list_term); + if (UNLIKELY(!term_is_nonempty_list(key_list_term_tail))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA public key", ctx)); + } + term priv_param_term = term_get_list_head(key_list_term_tail); + + enum pk_param_t pk_param = interop_atom_term_select_int(pk_param_table, priv_param_term, glb); + psa_key_type_t psa_key_type; + size_t psa_key_bits; + + switch (pk_param) { + case Secp256k1: + psa_key_type = PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_K1); + psa_key_bits = 256; + break; + case Secp256r1: + psa_key_type = PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 256; + break; + case Secp384r1: + psa_key_type = PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 384; + break; + case Secp521r1: + psa_key_type = PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1); + psa_key_bits = 521; + break; + case BrainpoolP256r1: + psa_key_type = PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 256; + break; + case BrainpoolP384r1: + psa_key_type = PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 384; + break; + case BrainpoolP512r1: + psa_key_type = PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_BRAINPOOL_P_R1); + psa_key_bits = 512; + break; + default: + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA public key", ctx)); + } + + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, psa_key_type); + psa_set_key_bits(&attr, psa_key_bits); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_algorithm(&attr, psa_key_alg); + + psa_key_id_t key_id = 0; + psa_status_t status = psa_import_key(&attr, pub, pub_len, &key_id); + psa_reset_key_attributes(&attr); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx)); + case PSA_ERROR_INVALID_ARGUMENT: + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Couldn't get ECDSA public key", ctx)); + default: + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx)); + } + + term result = ERROR_ATOM; + bool success = false; + size_t sig_raw_size = PSA_ECDSA_SIGNATURE_SIZE(psa_key_bits); + void *sig_raw = NULL; + void *maybe_allocated_data = NULL; + + term data_term = argv[2]; + const void *data; + size_t data_len; + term iodata_handle_result = handle_iodata(data_term, &data, &data_len, &maybe_allocated_data); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + result = make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx); + goto cleanup; + } + + sig_raw = malloc(sig_raw_size); + if (IS_NULL_PTR(sig_raw)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + size_t sig_raw_len = 0; + + int ret = mbedtls_ecdsa_der_to_raw( + psa_key_bits, sig_der, sig_der_len, sig_raw, sig_raw_size, &sig_raw_len); + if (UNLIKELY(ret != 0 || sig_raw_len != sig_raw_size)) { + // an invalid signature doesn't raise error on OTP, but it just fails verify + result = FALSE_ATOM; + success = true; + goto cleanup; + } + + status = psa_verify_message(key_id, psa_key_alg, data, data_len, sig_raw, sig_raw_len); + switch (status) { + case PSA_SUCCESS: + result = TRUE_ATOM; + success = true; + break; + + case PSA_ERROR_INVALID_SIGNATURE: + result = FALSE_ATOM; + success = true; + break; + + case PSA_ERROR_NOT_SUPPORTED: + result + = make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx); + break; + + default: + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + } + +cleanup: + psa_destroy_key(key_id); + + free(maybe_allocated_data); + free(sig_raw); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + +#endif + static const AtomStringIntPair cmac_algorithm_bits_table[] = { { ATOM_STR("\xB", "aes_128_cbc"), 128 }, { ATOM_STR("\xB", "aes_128_ecb"), 128 }, @@ -1035,7 +1415,7 @@ static term nif_crypto_mac(Context *ctx, int argc, term argv[]) break; case PSA_KEY_TYPE_HMAC: { psa_algorithm_t sub_type_algo - = interop_atom_term_select_int(hmac_algorithm_table, sub_type_term, glb); + = interop_atom_term_select_int(psa_hash_algorithm_table, sub_type_term, glb); if (UNLIKELY(sub_type_algo == PSA_ALG_NONE)) { result = make_crypto_error(__FILE__, __LINE__, "Bad digest algorithm for HMAC", ctx); @@ -1205,6 +1585,18 @@ static const struct Nif crypto_compute_key_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_compute_key }; +#ifdef CRYPTO_SIGN_AVAILABLE +static const struct Nif crypto_sign_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_sign +}; +#endif +#ifdef CRYPTO_VERIFY_AVAILABLE +static const struct Nif crypto_verify_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_verify +}; +#endif static const struct Nif crypto_mac_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_mac @@ -1248,6 +1640,18 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_compute_key_nif; } +#ifdef CRYPTO_SIGN_AVAILABLE + if (strcmp("sign/4", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_sign_nif; + } +#endif +#ifdef CRYPTO_VERIFY_AVAILABLE + if (strcmp("verify/5", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_verify_nif; + } +#endif if (strcmp("mac/4", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_mac_nif; diff --git a/tests/erlang_tests/test_crypto_pk.erl b/tests/erlang_tests/test_crypto_pk.erl index 69c1be35eb..d3a6be7778 100644 --- a/tests/erlang_tests/test_crypto_pk.erl +++ b/tests/erlang_tests/test_crypto_pk.erl @@ -19,10 +19,23 @@ % -module(test_crypto_pk). --export([start/0, test_generate_and_compute_key/0]). +-export([ + start/0, + test_generate_and_compute_key/0, + test_sign_and_verify/0, + test_sign_bad_algorithm/0, + test_sign_malformed_key/0, + test_verify_bad_algorithm/0, + test_verify_malformed_key/0 +]). start() -> ok = mbedtls_conditional_run(test_generate_and_compute_key, 16#03000000), + ok = mbedtls_conditional_run(test_sign_and_verify, 16#03060100), + ok = mbedtls_conditional_run(test_sign_bad_algorithm, 16#03060100), + ok = mbedtls_conditional_run(test_sign_malformed_key, 16#03060100), + ok = mbedtls_conditional_run(test_verify_bad_algorithm, 16#03060100), + ok = mbedtls_conditional_run(test_verify_malformed_key, 16#03060100), 0. mbedtls_conditional_run(F, RVer) -> @@ -66,3 +79,103 @@ test_generate_and_compute_key() -> 32 = byte_size(ComputedKey2), ok. + +test_sign_and_verify() -> + Data = <<"Hello">>, + + {SECPPub, SECPPriv} = crypto:generate_key(ecdh, secp256r1), + Sig = crypto:sign(ecdsa, sha256, Data, [SECPPriv, secp256r1]), + Sig2 = crypto:sign(ecdsa, sha256, [<<"">>, Data, <<"">>], [SECPPriv, secp256r1]), + + false = crypto:verify(ecdsa, sha256, <<"Invalid">>, Sig, [SECPPub, secp256r1]), + false = crypto:verify(ecdsa, sha256, Data, <<"InvalidSig">>, [SECPPub, secp256r1]), + true = crypto:verify(ecdsa, sha256, Data, Sig, [SECPPub, secp256r1]), + true = crypto:verify(ecdsa, sha256, Data, Sig2, [SECPPub, secp256r1]), + true = crypto:verify(ecdsa, sha256, [<<"">>, Data, <<"">>], Sig, [SECPPub, secp256r1]), + + ok. + +test_sign_bad_algorithm() -> + {_Pub, Priv} = crypto:generate_key(ecdh, secp256r1), + Data = <<"Hello">>, + + exp_err = + try + crypto:sign(bad_algo, sha256, Data, [Priv, secp256r1]) + catch + error:{error, {File1, Line1}, "Invalid public key"} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + + ok. + +test_sign_malformed_key() -> + Data = <<"Hello">>, + + exp_err = + try + crypto:sign(ecdsa, sha256, Data, <<"bad_key">>) + catch + error:{badarg, {File1, Line1}, "Couldn't get ECDSA private key"} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + + exp_err = + try + crypto:sign(ecdsa, sha256, Data, []) + catch + error:{badarg, {File2, Line2}, "Couldn't get ECDSA private key"} when + is_list(File2) andalso is_integer(Line2) + -> + exp_err + end, + + ok. + +test_verify_bad_algorithm() -> + {Pub, Priv} = crypto:generate_key(ecdh, secp256r1), + Data = <<"Hello">>, + Sig = crypto:sign(ecdsa, sha256, Data, [Priv, secp256r1]), + + exp_err = + try + crypto:verify(bad_algo, sha256, Data, Sig, [Pub, secp256r1]) + catch + error:{error, {File1, Line1}, "Invalid public key"} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + + ok. + +test_verify_malformed_key() -> + {_Pub, Priv} = crypto:generate_key(ecdh, secp256r1), + Data = <<"Hello">>, + Sig = crypto:sign(ecdsa, sha256, Data, [Priv, secp256r1]), + + exp_err = + try + crypto:verify(ecdsa, sha256, Data, Sig, <<"bad_key">>) + catch + error:{badarg, {File1, Line1}, "Couldn't get ECDSA public key"} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + + exp_err = + try + crypto:verify(ecdsa, sha256, Data, Sig, []) + catch + error:{badarg, {File2, Line2}, "Couldn't get ECDSA public key"} when + is_list(File2) andalso is_integer(Line2) + -> + exp_err + end, + + ok. From be73ca6919188e25eaeb78c62b545c073a242d03 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 21 Feb 2026 18:40:36 +0100 Subject: [PATCH 04/13] WIP HASH Signed-off-by: Davide Bettio --- libs/estdlib/src/crypto.erl | 46 ++++ src/libAtomVM/globalcontext.c | 17 ++ src/libAtomVM/globalcontext.h | 8 + src/libAtomVM/otp_crypto.c | 204 ++++++++++++++++++ src/libAtomVM/otp_crypto.h | 3 + tests/erlang_tests/CMakeLists.txt | 2 + .../erlang_tests/test_crypto_hash_update.erl | 151 +++++++++++++ tests/test.c | 1 + 8 files changed, 432 insertions(+) create mode 100644 tests/erlang_tests/test_crypto_hash_update.erl diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index 69e21d8720..3809454036 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -22,6 +22,9 @@ -export([ hash/2, + hash_init/1, + hash_update/2, + hash_final/1, crypto_one_time/4, crypto_one_time/5, generate_key/2, @@ -36,6 +39,9 @@ -type hash_algorithm() :: md5 | sha | sha224 | sha256 | sha384 | sha512. -type digest() :: binary(). +-export_type([hash_state/0]). +-opaque hash_state() :: reference(). + -type cipher_no_iv() :: aes_128_ecb | aes_192_ecb @@ -111,6 +117,46 @@ hash(_Type, _Data) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Type the hash algorithm +%% @returns Returns an opaque hash state that can be passed to `hash_update/2' +%% and `hash_final/1'. +%% @doc Initialize a streaming hash computation. +%% @end +%%----------------------------------------------------------------------------- +-spec hash_init(Type :: hash_algorithm()) -> hash_state(). +hash_init(_Type) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param State an opaque hash state returned from `hash_init/1' or a +%% previous call to `hash_update/2' +%% @param Data the data to feed into the hash computation +%% @returns Returns a new opaque hash state with the supplied data folded in. +%% @doc Update a streaming hash computation with new data. +%% +%% The original `State' is not modified; a new state is returned so +%% the same state can be forked into independent hash computations. +%% @end +%%----------------------------------------------------------------------------- +-spec hash_update(State :: hash_state(), Data :: iolist()) -> hash_state(). +hash_update(_State, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param State an opaque hash state returned from `hash_init/1' or +%% `hash_update/2' +%% @returns Returns the final hash digest as a binary. +%% @doc Finalize a streaming hash computation and return the digest. +%% +%% The state is not consumed; calling `hash_final/1' again on the +%% same state will produce the same digest. +%% @end +%%----------------------------------------------------------------------------- +-spec hash_final(State :: hash_state()) -> digest(). +hash_final(_State) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Cipher a supported cipher %% @param Key the encryption / decryption key diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index 238be0d0e8..b171d4f518 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -32,6 +32,7 @@ #include "interop.h" #include "list.h" #include "mailbox.h" +#include "otp_crypto.h" #include "posix_nifs.h" #include "refc_binary.h" #include "resources.h" @@ -161,6 +162,22 @@ GlobalContext *globalcontext_new(void) } #endif +#ifdef MBEDTLS_PSA_CRYPTO_C + glb->psa_hash_op_resource_type = enif_init_resource_type(&env, "psa_hash_op", &psa_hash_op_resource_type_init, ERL_NIF_RT_CREATE, NULL); + if (IS_NULL_PTR(glb->psa_hash_op_resource_type)) { +#if HAVE_OPEN && HAVE_CLOSE + resource_type_destroy(glb->posix_fd_resource_type); +#endif +#ifndef AVM_NO_SMP + smp_rwlock_destroy(glb->modules_lock); +#endif + free(glb->modules_table); + atom_table_destroy(glb->atom_table); + free(glb); + return NULL; + } +#endif + sys_init_platform(glb); #ifndef AVM_NO_SMP diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index ff2a63a4cd..e87b46581e 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -42,6 +42,10 @@ #include "timer_list.h" #include "valueshashtable.h" +#ifdef ATOMVM_HAS_MBEDTLS +#include +#endif + #ifdef __cplusplus extern "C" { #endif @@ -179,6 +183,10 @@ struct GlobalContext ErlNifResourceType *posix_dir_resource_type; #endif +#ifdef MBEDTLS_PSA_CRYPTO_C + ErlNifResourceType *psa_hash_op_resource_type; +#endif + void *platform_data; }; diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index 441903dff3..f517e5b484 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -19,10 +19,12 @@ * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later */ +#include "smp.h" #include #include #include +#include #include #include #include @@ -1499,6 +1501,184 @@ static term nif_crypto_mac(Context *ctx, int argc, term argv[]) return result; } +struct HashState +{ + psa_algorithm_t psa_algo; + psa_hash_operation_t psa_op; +}; + +static void psa_hash_op_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct HashState *hash_state = (struct HashState *) obj; + psa_hash_abort(&hash_state->psa_op); +} + +const ErlNifResourceTypeInit psa_hash_op_resource_type_init = { + .members = 1, + .dtor = psa_hash_op_dtor +}; + +static term nif_crypto_hash_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + term hash_algo_term = argv[0]; + psa_algorithm_t hash_algo + = interop_atom_term_select_int(psa_hash_algorithm_table, hash_algo_term, glb); + if (UNLIKELY(hash_algo == PSA_ALG_NONE)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad digest type", ctx)); + } + + struct HashState *hash_obj + = enif_alloc_resource(glb->psa_hash_op_resource_type, sizeof(struct HashState)); + if (IS_NULL_PTR(hash_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + memset(hash_obj, 0, sizeof(struct HashState)); + hash_obj->psa_algo = hash_algo; + psa_status_t status = psa_hash_setup(&hash_obj->psa_op, hash_algo); + if (UNLIKELY(status != PSA_SUCCESS)) { + enif_release_resource(hash_obj); + switch (status) { + case PSA_ERROR_NOT_SUPPORTED: + RAISE_ERROR(make_crypto_error( + __FILE__, __LINE__, "Unsupported key type or parameter", ctx)); + default: + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx)); + } + } + + term obj = enif_make_resource(erl_nif_env_from_context(ctx), hash_obj); + enif_release_resource(hash_obj); + + return obj; +} + +static term nif_crypto_hash_update(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + void *psa_hash_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + glb->psa_hash_op_resource_type, &psa_hash_obj_ptr))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad state", ctx)); + } + struct HashState *hash_state = (struct HashState *) psa_hash_obj_ptr; + + bool success = false; + term result = ERROR_ATOM; + void *maybe_allocated_data = NULL; + struct HashState *new_hash_obj = NULL; + + size_t data_len; + term data_term = argv[1]; + const void *data; + term iodata_handle_result = handle_iodata(data_term, &data, &data_len, &maybe_allocated_data); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + result = BADARG_ATOM; + goto cleanup; + } + + new_hash_obj = enif_alloc_resource(glb->psa_hash_op_resource_type, sizeof(struct HashState)); + if (IS_NULL_PTR(new_hash_obj)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + memset(new_hash_obj, 0, sizeof(struct HashState)); + new_hash_obj->psa_algo = hash_state->psa_algo; + psa_status_t status = psa_hash_clone(&hash_state->psa_op, &new_hash_obj->psa_op); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + status = psa_hash_update(&new_hash_obj->psa_op, data, data_len); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + success = true; + result = enif_make_resource(erl_nif_env_from_context(ctx), new_hash_obj); + +cleanup: + free(maybe_allocated_data); + if (new_hash_obj) { + enif_release_resource(new_hash_obj); + } + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + +static term nif_crypto_hash_final(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + void *psa_hash_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + glb->psa_hash_op_resource_type, &psa_hash_obj_ptr))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad state", ctx)); + } + struct HashState *hash_state = (struct HashState *) psa_hash_obj_ptr; + + bool success = false; + term result = ERROR_ATOM; + + psa_algorithm_t psa_algo = hash_state->psa_algo; + psa_hash_operation_t psa_op = PSA_HASH_OPERATION_INIT; + psa_status_t status = psa_hash_clone(&hash_state->psa_op, &psa_op); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + size_t hash_size = PSA_HASH_LENGTH(psa_algo); + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(hash_size)) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + term hash_bin = term_create_uninitialized_binary(hash_size, &ctx->heap, glb); + void *hash_buf = (void *) term_binary_data(hash_bin); + + size_t hash_len = 0; + status = psa_hash_finish(&psa_op, hash_buf, hash_size, &hash_len); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + assert(hash_size == hash_len); + + success = true; + result = hash_bin; + +cleanup: + psa_hash_abort(&psa_op); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} #endif // not static since we are using it elsewhere to provide backward compatibility @@ -1601,6 +1781,18 @@ static const struct Nif crypto_mac_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_mac }; +static const struct Nif crypto_hash_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_hash_init +}; +static const struct Nif crypto_hash_update_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_hash_update +}; +static const struct Nif crypto_hash_final_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_hash_final +}; #endif static const struct Nif crypto_strong_rand_bytes_nif = { .base.type = NIFFunctionType, @@ -1656,6 +1848,18 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_mac_nif; } + if (strcmp("hash_init/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_hash_init_nif; + } + if (strcmp("hash_update/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_hash_update_nif; + } + if (strcmp("hash_final/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_hash_final_nif; + } #endif if (strcmp("strong_rand_bytes/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); diff --git a/src/libAtomVM/otp_crypto.h b/src/libAtomVM/otp_crypto.h index 277c8c960e..32a4db7f58 100644 --- a/src/libAtomVM/otp_crypto.h +++ b/src/libAtomVM/otp_crypto.h @@ -21,6 +21,7 @@ #ifndef _OTP_CRYPTO_H_ #define _OTP_CRYPTO_H_ +#include #include #ifdef __cplusplus @@ -29,6 +30,8 @@ extern "C" { const struct Nif *otp_crypto_nif_get_nif(const char *nifname); +extern const ErlNifResourceTypeInit psa_hash_op_resource_type_init; + #ifdef __cplusplus } #endif diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 29c18f77ca..91907e9a74 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -662,6 +662,7 @@ endif() compile_erlang(test_crypto_pk) compile_erlang(test_crypto_mac) +compile_erlang(test_crypto_hash_update) set(erlang_test_beams add.beam @@ -1202,6 +1203,7 @@ set(erlang_test_beams test_crypto_pk.beam test_crypto_mac.beam + test_crypto_hash_update.beam ) if(NOT AVM_DISABLE_JIT) diff --git a/tests/erlang_tests/test_crypto_hash_update.erl b/tests/erlang_tests/test_crypto_hash_update.erl new file mode 100644 index 0000000000..959128ee1e --- /dev/null +++ b/tests/erlang_tests/test_crypto_hash_update.erl @@ -0,0 +1,151 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_crypto_hash_update). +-export([ + start/0, + test_sha256/0, + test_sha256_list/0, + test_cloning_sha256/0, + test_sha256_empty/0, + test_md5/0, + test_badarg/0, + get_bad/0 +]). + +start() -> + ok = test_sha256(), + ok = test_sha256_list(), + ok = test_cloning_sha256(), + ok = test_sha256_empty(), + ok = test_md5(), + ok = test_badarg(), + 0. + +test_sha256() -> + HashInitialState = crypto:hash_init(sha256), + HashUpdatedState = crypto:hash_update(HashInitialState, <<"Hello">>), + <<24, 95, 141, 179, 34, 113, 254, 37, 245, 97, 166, 252, 147, 139, 46, 38, 67, 6, 236, 48, 78, + 218, 81, 128, 7, 209, 118, 72, 38, 56, 25, 105>> = crypto:hash_final(HashUpdatedState), + ok. + +test_sha256_list() -> + HashInitialState = crypto:hash_init(sha256), + HashUpdatedState = crypto:hash_update(HashInitialState, [$H, $e, <<"llo">>]), + <<24, 95, 141, 179, 34, 113, 254, 37, 245, 97, 166, 252, 147, 139, 46, 38, 67, 6, 236, 48, 78, + 218, 81, 128, 7, 209, 118, 72, 38, 56, 25, 105>> = crypto:hash_final(HashUpdatedState), + ok. + +test_cloning_sha256() -> + HashInitialState = crypto:hash_init(sha256), + HashUpdatedState = crypto:hash_update(HashInitialState, <<"hello: ">>), + + HashUpdatedStateA = crypto:hash_update(HashInitialState, <<"world">>), + HashUpdatedStateB = crypto:hash_update(HashInitialState, <<"everyone">>), + HashUpdatedStateC = crypto:hash_update(HashInitialState, <<"folks">>), + + <<72, 110, 164, 98, 36, 209, 187, 79, 182, 128, 243, 79, 124, 154, 217, 106, 143, 36, 236, 136, + 190, 115, 234, 142, 90, 108, 101, 38, 14, 156, 184, 167>> = crypto:hash_final( + HashUpdatedStateA + ), + <<93, 103, 153, 26, 233, 103, 153, 76, 148, 178, 226, 26, 77, 99, 156, 101, 157, 53, 122, 111, + 189, 63, 167, 67, 228, 246, 99, 138, 68, 13, 53, 199>> = crypto:hash_final( + HashUpdatedStateB + ), + <<95, 145, 188, 155, 205, 195, 48, 130, 185, 45, 86, 227, 186, 245, 8, 122, 110, 121, 122, 98, + 231, 149, 15, 149, 193, 222, 49, 69, 247, 55, 31, 112>> = crypto:hash_final( + HashUpdatedStateC + ), + <<154, 32, 219, 114, 31, 158, 11, 194, 61, 234, 117, 20, 94, 185, 94, 95, 170, 196, 55, 150, + 138, 129, 254, 106, 97, 237, 152, 248, 19, 101, 242, 235>> = crypto:hash_final( + HashUpdatedState + ), + + HashUpdatedStateB2 = crypto:hash_update(HashUpdatedStateB, <<"!!!">>), + + <<215, 154, 156, 137, 108, 16, 178, 36, 142, 177, 122, 249, 164, 173, 10, 35, 85, 104, 6, 237, + 194, 215, 157, 126, 198, 167, 31, 206, 247, 159, 70, 41>> = crypto:hash_final( + HashUpdatedStateB2 + ), + <<215, 154, 156, 137, 108, 16, 178, 36, 142, 177, 122, 249, 164, 173, 10, 35, 85, 104, 6, 237, + 194, 215, 157, 126, 198, 167, 31, 206, 247, 159, 70, 41>> = crypto:hash_final( + HashUpdatedStateB2 + ), + + ok. + +test_sha256_empty() -> + <<227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, + 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85>> = crypto:hash_final( + crypto:hash_init(sha256) + ), + + <<227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, + 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85>> = crypto:hash_final( + crypto:hash_update(crypto:hash_init(sha256), <<"">>) + ), + + ok. + +test_md5() -> + HashInitialState = crypto:hash_init(md5), + HashUpdatedState = crypto:hash_update(HashInitialState, <<0, 1, 2, 3, 4>>), + <<208, 83, 116, 220, 56, 29, 155, 82, 128, 100, 70, 167, 28, 142, 121, 177>> = crypto:hash_final( + HashUpdatedState + ), + ok. + +test_badarg() -> + exp_err = + try + crypto:hash_init(?MODULE:get_bad()) + catch + error:{badarg, {File1, Line1}, "Bad digest type"} when + is_list(File1) and is_integer(Line1) + -> + exp_err + end, + + HashInitialState = crypto:hash_init(sha256), + exp_err = + try + crypto:hash_update(HashInitialState, ?MODULE:get_bad()) + catch + error:badarg -> exp_err + end, + exp_err = + try + crypto:hash_update(?MODULE:get_bad(), <<"Hello">>) + catch + error:{badarg, {File2, Line2}, "Bad state"} when is_list(File2) and is_integer(Line2) -> + exp_err + end, + + exp_err = + try + crypto:hash_final(?MODULE:get_bad()) + catch + error:{badarg, {File3, Line3}, "Bad state"} when is_list(File3) and is_integer(Line3) -> + exp_err + end, + + ok. + +get_bad() -> foo. diff --git a/tests/test.c b/tests/test.c index 6a72c38ee2..21fd4bbd03 100644 --- a/tests/test.c +++ b/tests/test.c @@ -628,6 +628,7 @@ struct Test tests[] = { TEST_CASE(test_crypto_pk), TEST_CASE(test_crypto_mac), + TEST_CASE(test_crypto_hash_update), // TEST CRASHES HERE: TEST_CASE(memlimit), From c1ea551c023b3fb45a58f08cc24aa62de0c26bfa Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 22 Feb 2026 13:40:06 +0100 Subject: [PATCH 05/13] WIP crypto funcs Signed-off-by: Davide Bettio --- libs/estdlib/src/crypto.erl | 103 ++++- src/libAtomVM/globalcontext.c | 13 + src/libAtomVM/globalcontext.h | 1 + src/libAtomVM/otp_crypto.c | 492 ++++++++++++++++++++++ src/libAtomVM/otp_crypto.h | 1 + tests/erlang_tests/CMakeLists.txt | 2 + tests/erlang_tests/test_crypto_crypto.erl | 414 ++++++++++++++++++ tests/test.c | 1 + 8 files changed, 1026 insertions(+), 1 deletion(-) create mode 100644 tests/erlang_tests/test_crypto_crypto.erl diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index 3809454036..5a8e7796e3 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -27,6 +27,10 @@ hash_final/1, crypto_one_time/4, crypto_one_time/5, + crypto_init/3, + crypto_init/4, + crypto_update/2, + crypto_final/1, generate_key/2, compute_key/4, sign/4, @@ -42,6 +46,13 @@ -export_type([hash_state/0]). -opaque hash_state() :: reference(). +-export_type([crypto_state/0]). +%% Opaque mutable state for streaming cipher operations. +%% The state is mutated in place by crypto_update/2 and crypto_final/1. +-opaque crypto_state() :: reference(). + +%% Note: PKCS7 padding (`pkcs_padding`) is **not** supported for ECB ciphers +%% in AtomVM. Use a CBC cipher if you need PKCS7 padding. -type cipher_no_iv() :: aes_128_ecb | aes_192_ecb @@ -56,7 +67,10 @@ | aes_256_cfb128 | aes_128_ctr | aes_192_ctr - | aes_256_ctr. + | aes_256_ctr + | aes_128_ofb + | aes_192_ofb + | aes_256_ofb. -type padding() :: none | pkcs_padding. @@ -197,6 +211,93 @@ crypto_one_time(_Cipher, _Key, _Data, _FlagOrOptions) -> crypto_one_time(_Cipher, _Key, _IV, _Data, _FlagOrOptions) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Cipher a supported cipher (no IV required, e.g. ECB modes) +%% @param Key the encryption / decryption key +%% @param FlagOrOptions either `true` for encryption, `false` for decryption, +%% or a proplist with options such as `{encrypt, boolean()}` and +%% `{padding, padding()}`. +%% @returns Returns an opaque `crypto_state()` reference for use with +%% `crypto_update/2` and `crypto_final/1`. +%% @doc Initialize a streaming cipher operation for ciphers that do not use an IV. +%% +%% This is equivalent to `crypto_init(Cipher, Key, <<>>, FlagOrOptions)`. +%% @end +%%----------------------------------------------------------------------------- +-spec crypto_init( + Cipher :: cipher_no_iv(), + Key :: iodata(), + FlagOrOptions :: boolean() | crypto_opts() +) -> crypto_state(). +crypto_init(Cipher, Key, FlagOrOptions) -> + crypto_init(Cipher, Key, <<>>, FlagOrOptions). + +%%----------------------------------------------------------------------------- +%% @param Cipher a supported cipher +%% @param Key the encryption / decryption key +%% @param IV an initialization vector (use `<<>>` for ciphers that do not require one) +%% @param FlagOrOptions either `true` for encryption, `false` for decryption, +%% or a proplist with options such as `{encrypt, boolean()}` and +%% `{padding, padding()}`. +%% @returns Returns an opaque `crypto_state()` reference for use with +%% `crypto_update/2` and `crypto_final/1`. +%% @doc Initialize a streaming cipher operation. +%% +%% The returned state is **mutable**: `crypto_update/2` and `crypto_final/1` +%% mutate it in place. After `crypto_final/1' the state must not be reused. +%% +%% **AtomVM padding support:** +%% `{padding, pkcs_padding}` is supported **only with CBC ciphers** +%% (e.g. `aes_128_cbc`, `aes_192_cbc`, `aes_256_cbc`). Requesting +%% PKCS7 padding with ECB or any other cipher mode raises a `badarg` +%% error. This differs from OTP, which silently accepts the option +%% for non-CBC modes. +%% @end +%%----------------------------------------------------------------------------- +-spec crypto_init( + Cipher :: cipher_no_iv() | cipher_iv(), + Key :: iodata(), + IV :: iodata(), + FlagOrOptions :: boolean() | crypto_opts() +) -> crypto_state(). +crypto_init(_Cipher, _Key, _IV, _FlagOrOptions) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param State an opaque cipher state returned from `crypto_init/3' or +%% `crypto_init/4' +%% @param Data the data to encrypt or decrypt (iodata) +%% @returns Returns the encrypted/decrypted bytes produced so far as a binary. +%% @doc Feed data into a streaming cipher operation. +%% +%% For block ciphers the output may be shorter than the input if an +%% incomplete block is buffered internally by the PSA layer. +%% @end +%%----------------------------------------------------------------------------- +-spec crypto_update(State :: crypto_state(), Data :: iodata()) -> binary(). +crypto_update(_State, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param State an opaque cipher state returned from `crypto_init/3' or +%% `crypto_init/4' +%% @returns Returns any remaining bytes (e.g. a padded final block) as a binary, +%% or `<<>>` if there are none. +%% @doc Finalize a streaming cipher operation. +%% +%% For block ciphers with `{padding, none}` an error is raised if the +%% total data fed was not a multiple of the block size. +%% +%% **AtomVM limitation:** after `crypto_final/1' returns, the state is +%% permanently finalized. Calling `crypto_final/1' again, or calling +%% `crypto_update/2' on the same state, raises a `badarg' error. +%% OTP allows reuse of the state after `crypto_final/1'. +%% @end +%%----------------------------------------------------------------------------- +-spec crypto_final(State :: crypto_state()) -> binary(). +crypto_final(_State) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Type the key algorithm family %% @param Param curve/parameter selection diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index b171d4f518..5f8e1f272f 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -168,6 +168,19 @@ GlobalContext *globalcontext_new(void) #if HAVE_OPEN && HAVE_CLOSE resource_type_destroy(glb->posix_fd_resource_type); #endif +#ifndef AVM_NO_SMP + smp_rwlock_destroy(glb->modules_lock); +#endif + free(glb->modules_table); + atom_table_destroy(glb->atom_table); + free(glb); + return NULL; + } + glb->psa_cipher_op_resource_type = enif_init_resource_type(&env, "psa_cipher_op", &psa_cipher_op_resource_type_init, ERL_NIF_RT_CREATE, NULL); + if (IS_NULL_PTR(glb->psa_cipher_op_resource_type)) { +#if HAVE_OPEN && HAVE_CLOSE + resource_type_destroy(glb->posix_fd_resource_type); +#endif #ifndef AVM_NO_SMP smp_rwlock_destroy(glb->modules_lock); #endif diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index e87b46581e..216d450593 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -185,6 +185,7 @@ struct GlobalContext #ifdef MBEDTLS_PSA_CRYPTO_C ErlNifResourceType *psa_hash_op_resource_type; + ErlNifResourceType *psa_cipher_op_resource_type; #endif void *platform_data; diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index f517e5b484..5961756bc2 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -1679,6 +1679,474 @@ static term nif_crypto_hash_final(Context *ctx, int argc, term argv[]) return result; } + +// +// Streaming cipher (crypto_init / crypto_update / crypto_final) +// + +enum cipher_padding +{ + CipherPaddingDefault = 0, /* discard partial block on final, return <<>> */ + CipherPaddingNone = 1, /* error if partial block remains on final */ + CipherPaddingPkcs = 2 /* PKCS7-pad last block on final (CBC only, via PSA) */ +}; + +struct CipherState +{ + psa_cipher_operation_t psa_op; + psa_key_id_t key_id; + psa_algorithm_t psa_algo; + psa_key_type_t key_type; + bool encrypting; + bool finalized; /* true after crypto_final; further calls raise badarg */ + uint8_t block_size; /* 0 = stream cipher, 16 = AES block cipher */ + enum cipher_padding padding; +}; + +static void psa_cipher_op_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct CipherState *cipher_state = (struct CipherState *) obj; + psa_cipher_abort(&cipher_state->psa_op); + psa_destroy_key(cipher_state->key_id); +} + +const ErlNifResourceTypeInit psa_cipher_op_resource_type_init = { + .members = 1, + .dtor = psa_cipher_op_dtor +}; + +struct PsaCipherParams +{ + const char *atom_str; + psa_key_type_t key_type; + psa_algorithm_t algorithm; + uint16_t key_bits; + uint8_t block_size; /* 0 = stream cipher, 16 = AES block */ + uint8_t iv_len; /* 0 = no IV (ECB), 16 = standard AES IV */ +}; + +static const struct PsaCipherParams psa_cipher_table[] = { + /* ECB modes — block cipher, no IV */ + { ATOM_STR("\xB", "aes_128_ecb"), PSA_KEY_TYPE_AES, PSA_ALG_ECB_NO_PADDING, 128, 16, 0 }, + { ATOM_STR("\xB", "aes_192_ecb"), PSA_KEY_TYPE_AES, PSA_ALG_ECB_NO_PADDING, 192, 16, 0 }, + { ATOM_STR("\xB", "aes_256_ecb"), PSA_KEY_TYPE_AES, PSA_ALG_ECB_NO_PADDING, 256, 16, 0 }, + /* CBC modes — block cipher, with IV */ + { ATOM_STR("\xB", "aes_128_cbc"), PSA_KEY_TYPE_AES, PSA_ALG_CBC_NO_PADDING, 128, 16, 16 }, + { ATOM_STR("\xB", "aes_192_cbc"), PSA_KEY_TYPE_AES, PSA_ALG_CBC_NO_PADDING, 192, 16, 16 }, + { ATOM_STR("\xB", "aes_256_cbc"), PSA_KEY_TYPE_AES, PSA_ALG_CBC_NO_PADDING, 256, 16, 16 }, + /* CFB128 modes — stream-like, with IV */ + { ATOM_STR("\xE", "aes_128_cfb128"), PSA_KEY_TYPE_AES, PSA_ALG_CFB, 128, 0, 16 }, + { ATOM_STR("\xE", "aes_192_cfb128"), PSA_KEY_TYPE_AES, PSA_ALG_CFB, 192, 0, 16 }, + { ATOM_STR("\xE", "aes_256_cfb128"), PSA_KEY_TYPE_AES, PSA_ALG_CFB, 256, 0, 16 }, + /* CTR modes — stream cipher, with IV */ + { ATOM_STR("\xB", "aes_128_ctr"), PSA_KEY_TYPE_AES, PSA_ALG_CTR, 128, 0, 16 }, + { ATOM_STR("\xB", "aes_192_ctr"), PSA_KEY_TYPE_AES, PSA_ALG_CTR, 192, 0, 16 }, + { ATOM_STR("\xB", "aes_256_ctr"), PSA_KEY_TYPE_AES, PSA_ALG_CTR, 256, 0, 16 }, + /* OFB modes — stream-like, with IV */ + { ATOM_STR("\xB", "aes_128_ofb"), PSA_KEY_TYPE_AES, PSA_ALG_OFB, 128, 0, 16 }, + { ATOM_STR("\xB", "aes_192_ofb"), PSA_KEY_TYPE_AES, PSA_ALG_OFB, 192, 0, 16 }, + { ATOM_STR("\xB", "aes_256_ofb"), PSA_KEY_TYPE_AES, PSA_ALG_OFB, 256, 0, 16 }, +}; + +#define PSA_CIPHER_TABLE_LEN (sizeof(psa_cipher_table) / sizeof(psa_cipher_table[0])) + +static const struct PsaCipherParams *psa_cipher_table_lookup(GlobalContext *glb, term cipher_atom) +{ + for (size_t i = 0; i < PSA_CIPHER_TABLE_LEN; i++) { + AtomString atom_str = (AtomString) psa_cipher_table[i].atom_str; + if (globalcontext_is_term_equal_to_atom_string(glb, cipher_atom, atom_str)) { + return &psa_cipher_table[i]; + } + } + return NULL; +} + +/* + * Parse the FlagOrOptions argument of crypto_init/4. + * + * Accepts: + * - true / false (boolean shorthand for {encrypt, true/false}) + * - proplist with {encrypt, boolean()} and/or {padding, none|pkcs_padding} + * + * Returns OK_ATOM on success (with *encrypting and *padding set), + * or an error term suitable for RAISE_ERROR on failure. + */ +static term parse_cipher_flag_or_options(term flag_or_opts, bool *encrypting, + enum cipher_padding *padding, GlobalContext *glb, Context *ctx) +{ + if (flag_or_opts == TRUE_ATOM) { + *encrypting = true; + return OK_ATOM; + } + + if (flag_or_opts == FALSE_ATOM) { + *encrypting = false; + return OK_ATOM; + } + + if (!term_is_list(flag_or_opts)) { + return make_crypto_error( + __FILE__, __LINE__, "Options are not a boolean or a proper list", ctx); + } + + term t = flag_or_opts; + while (term_is_nonempty_list(t)) { + term head = term_get_list_head(t); + + if (UNLIKELY(!term_is_tuple(head) || term_get_tuple_arity(head) != 2)) { + return make_crypto_error(__FILE__, __LINE__, "Bad option format", ctx); + } + + term opt_key = term_get_tuple_element(head, 0); + term opt_val = term_get_tuple_element(head, 1); + + if (globalcontext_is_term_equal_to_atom_string(glb, opt_key, ATOM_STR("\x7", "encrypt"))) { + if (opt_val == TRUE_ATOM) { + *encrypting = true; + } else if (opt_val == FALSE_ATOM) { + *encrypting = false; + } else { + return make_crypto_error(__FILE__, __LINE__, "Bad encrypt option value", ctx); + } + } else if (globalcontext_is_term_equal_to_atom_string( + glb, opt_key, ATOM_STR("\x7", "padding"))) { + if (globalcontext_is_term_equal_to_atom_string(glb, opt_val, ATOM_STR("\x4", "none"))) { + *padding = CipherPaddingNone; + } else if (globalcontext_is_term_equal_to_atom_string( + glb, opt_val, ATOM_STR("\xC", "pkcs_padding"))) { + *padding = CipherPaddingPkcs; + } else { + return make_crypto_error(__FILE__, __LINE__, "Bad padding option value", ctx); + } + } else { + return make_crypto_error(__FILE__, __LINE__, "Unknown option", ctx); + } + + t = term_get_list_tail(t); + } + + if (UNLIKELY(!term_is_nil(t))) { + return make_crypto_error(__FILE__, __LINE__, "Options is not a proper list", ctx); + } + + return OK_ATOM; +} + +static term nif_crypto_crypto_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + /* 1. Look up cipher parameters */ + term cipher_atom = argv[0]; + const struct PsaCipherParams *cipher_params = psa_cipher_table_lookup(glb, cipher_atom); + if (IS_NULL_PTR(cipher_params)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unknown cipher", ctx)); + } + + /* 2. Validate key — must be a flat binary of the correct size */ + term key_term = argv[1]; + if (UNLIKELY(!term_is_binary(key_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad key", ctx)); + } + size_t key_len = term_binary_size(key_term); + if (UNLIKELY(key_len != (size_t) (cipher_params->key_bits / 8))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad key size", ctx)); + } + const uint8_t *key_data = (const uint8_t *) term_binary_data(key_term); + + /* 3. Validate IV — must be a flat binary of the correct size. + * For ciphers that do not use an IV (iv_len == 0, e.g. ECB) any binary + * is accepted and silently ignored, matching OTP behaviour. */ + term iv_term = argv[2]; + if (UNLIKELY(!term_is_binary(iv_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad iv type", ctx)); + } + size_t iv_len = term_binary_size(iv_term); + if (cipher_params->iv_len > 0 && UNLIKELY(iv_len != cipher_params->iv_len)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad iv size", ctx)); + } + + /* 4. Parse FlagOrOptions */ + bool encrypting = true; + enum cipher_padding padding = CipherPaddingDefault; + term parse_result = parse_cipher_flag_or_options(argv[3], &encrypting, &padding, glb, ctx); + if (UNLIKELY(parse_result != OK_ATOM)) { + RAISE_ERROR(parse_result); + } + + /* 4b. Resolve effective PSA algorithm based on padding option. + * PKCS7 padding is natively supported by PSA only for CBC; for any + * other cipher mode it is not available and we reject it early. */ + psa_algorithm_t effective_algo = cipher_params->algorithm; + if (padding == CipherPaddingPkcs) { + if (cipher_params->algorithm == PSA_ALG_CBC_NO_PADDING) { + effective_algo = PSA_ALG_CBC_PKCS7; + } else { + RAISE_ERROR(make_crypto_error( + __FILE__, __LINE__, "PKCS padding is supported only with CBC ciphers", ctx)); + } + } + + /* 5. Import key via PSA */ + bool success = false; + term result = ERROR_ATOM; + psa_key_id_t key_id = 0; + struct CipherState *cipher_obj = NULL; + + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, cipher_params->key_type); + psa_set_key_bits(&attr, cipher_params->key_bits); + psa_set_key_usage_flags(&attr, encrypting ? PSA_KEY_USAGE_ENCRYPT : PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attr, effective_algo); + + psa_status_t status = psa_import_key(&attr, key_data, key_len, &key_id); + psa_reset_key_attributes(&attr); + if (UNLIKELY(status != PSA_SUCCESS)) { + switch (status) { + case PSA_ERROR_NOT_SUPPORTED: + result = make_crypto_error(__FILE__, __LINE__, "Unsupported algorithm", ctx); + break; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + break; + } + goto cleanup; + } + + /* 6. Allocate and initialise the resource */ + cipher_obj = enif_alloc_resource(glb->psa_cipher_op_resource_type, sizeof(struct CipherState)); + if (IS_NULL_PTR(cipher_obj)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + memset(cipher_obj, 0, sizeof(struct CipherState)); + cipher_obj->key_id = key_id; + cipher_obj->psa_algo = effective_algo; + cipher_obj->key_type = cipher_params->key_type; + cipher_obj->encrypting = encrypting; + cipher_obj->block_size = cipher_params->block_size; + cipher_obj->padding = padding; + + /* 7. Set up the cipher operation */ + if (encrypting) { + status = psa_cipher_encrypt_setup(&cipher_obj->psa_op, key_id, effective_algo); + } else { + status = psa_cipher_decrypt_setup(&cipher_obj->psa_op, key_id, effective_algo); + } + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + /* 8. Set IV if this cipher requires one */ + if (cipher_params->iv_len > 0) { + const uint8_t *iv_data = (const uint8_t *) term_binary_data(iv_term); + status = psa_cipher_set_iv(&cipher_obj->psa_op, iv_data, iv_len); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + } + + /* 9. Transfer key ownership to the resource (cleared so cleanup won't + * destroy it — the destructor now owns it). */ + key_id = 0; + + success = true; + result = enif_make_resource(erl_nif_env_from_context(ctx), cipher_obj); + +cleanup: + if (key_id != 0) { + psa_destroy_key(key_id); + } + if (!IS_NULL_PTR(cipher_obj)) { + enif_release_resource(cipher_obj); + } + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + +static term nif_crypto_crypto_update(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + /* 1. Retrieve the cipher state from the resource reference */ + void *cipher_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + glb->psa_cipher_op_resource_type, &cipher_obj_ptr))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad State", ctx)); + } + struct CipherState *cipher_state = (struct CipherState *) cipher_obj_ptr; + + if (UNLIKELY(cipher_state->finalized)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, + "Bad state: AtomVM does not allow operations after crypto_final", ctx)); + } + + bool success = false; + term result = ERROR_ATOM; + void *maybe_allocated_data = NULL; + void *out_buf = NULL; + + /* 2. Handle iodata input */ + const void *data; + size_t data_len; + term iodata_result = handle_iodata(argv[1], &data, &data_len, &maybe_allocated_data); + if (UNLIKELY(iodata_result == BADARG_ATOM)) { + result = make_crypto_error(__FILE__, __LINE__, "expected binary", ctx); + goto cleanup; + } + if (UNLIKELY(iodata_result != OK_ATOM)) { + result = iodata_result; + goto cleanup; + } + + /* 3. Encrypt/decrypt via PSA — PSA handles internal block buffering */ + size_t out_size = PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE(data_len); + if (out_size == 0) { + out_size = 1; /* ensure valid malloc even for zero-length input */ + } + out_buf = malloc(out_size); + if (IS_NULL_PTR(out_buf)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + size_t out_len = 0; + psa_status_t status + = psa_cipher_update(&cipher_state->psa_op, data, data_len, out_buf, out_size, &out_len); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(out_len)) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + success = true; + result = term_from_literal_binary(out_buf, out_len, &ctx->heap, glb); + +cleanup: + free(maybe_allocated_data); + free(out_buf); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + +static term nif_crypto_crypto_final(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + /* 1. Retrieve the cipher state */ + void *cipher_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + glb->psa_cipher_op_resource_type, &cipher_obj_ptr))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad State", ctx)); + } + struct CipherState *cipher_state = (struct CipherState *) cipher_obj_ptr; + + if (UNLIKELY(cipher_state->finalized)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, + "Bad state: AtomVM does not allow calling crypto_final more than once", ctx)); + } + + bool success = false; + term result = ERROR_ATOM; + void *out_buf = NULL; + + /* 2. Finalise via PSA — this terminates the operation */ + size_t out_size = PSA_CIPHER_FINISH_OUTPUT_MAX_SIZE; + if (out_size == 0) { + out_size = 1; + } + out_buf = malloc(out_size); + if (IS_NULL_PTR(out_buf)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + size_t out_len = 0; + psa_status_t status = psa_cipher_finish(&cipher_state->psa_op, out_buf, out_size, &out_len); + + if (status == PSA_SUCCESS) { + if (UNLIKELY(memory_ensure_free_with_roots( + ctx, TERM_BINARY_HEAP_SIZE(out_len), 1, &argv[0], MEMORY_NO_SHRINK) + != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + cipher_state->finalized = true; + success = true; + result = term_from_literal_binary(out_buf, out_len, &ctx->heap, glb); + } else if (status == PSA_ERROR_INVALID_ARGUMENT) { + /* Block cipher with a partial last block: behaviour depends on padding. + * Note: CipherPaddingPkcs uses PSA_ALG_CBC_PKCS7 which PSA handles + * natively, so this branch should never be reached for PKCS padding. */ + psa_cipher_abort(&cipher_state->psa_op); + cipher_state->finalized = true; + switch (cipher_state->padding) { + case CipherPaddingNone: + result = make_crypto_error_tag( + __FILE__, __LINE__, "Padding 'none' but unfilled last block", ERROR_ATOM, ctx); + goto cleanup; + case CipherPaddingDefault: + /* Silently discard the partial block */ + if (UNLIKELY(memory_ensure_free_with_roots( + ctx, TERM_BINARY_HEAP_SIZE(0), 1, &argv[0], MEMORY_NO_SHRINK) + != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + success = true; + result = term_from_literal_binary("", 0, &ctx->heap, glb); + break; + case CipherPaddingPkcs: + /* Defensive: should be unreachable since PSA_ALG_CBC_PKCS7 + * handles padding natively and never returns INVALID_ARGUMENT + * for a partial block. */ + result + = make_crypto_error(__FILE__, __LINE__, "Unexpected PKCS padding error", ctx); + goto cleanup; + } + } else { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + +cleanup: + free(out_buf); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + #endif // not static since we are using it elsewhere to provide backward compatibility @@ -1793,6 +2261,18 @@ static const struct Nif crypto_hash_final_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_hash_final }; +static const struct Nif crypto_crypto_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_crypto_init +}; +static const struct Nif crypto_crypto_update_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_crypto_update +}; +static const struct Nif crypto_crypto_final_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_crypto_final +}; #endif static const struct Nif crypto_strong_rand_bytes_nif = { .base.type = NIFFunctionType, @@ -1860,6 +2340,18 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_hash_final_nif; } + if (strcmp("crypto_init/4", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_crypto_init_nif; + } + if (strcmp("crypto_update/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_crypto_update_nif; + } + if (strcmp("crypto_final/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_crypto_final_nif; + } #endif if (strcmp("strong_rand_bytes/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); diff --git a/src/libAtomVM/otp_crypto.h b/src/libAtomVM/otp_crypto.h index 32a4db7f58..f2a255824c 100644 --- a/src/libAtomVM/otp_crypto.h +++ b/src/libAtomVM/otp_crypto.h @@ -31,6 +31,7 @@ extern "C" { const struct Nif *otp_crypto_nif_get_nif(const char *nifname); extern const ErlNifResourceTypeInit psa_hash_op_resource_type_init; +extern const ErlNifResourceTypeInit psa_cipher_op_resource_type_init; #ifdef __cplusplus } diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 91907e9a74..6879338544 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -663,6 +663,7 @@ endif() compile_erlang(test_crypto_pk) compile_erlang(test_crypto_mac) compile_erlang(test_crypto_hash_update) +compile_erlang(test_crypto_crypto) set(erlang_test_beams add.beam @@ -1204,6 +1205,7 @@ set(erlang_test_beams test_crypto_pk.beam test_crypto_mac.beam test_crypto_hash_update.beam + test_crypto_crypto.beam ) if(NOT AVM_DISABLE_JIT) diff --git a/tests/erlang_tests/test_crypto_crypto.erl b/tests/erlang_tests/test_crypto_crypto.erl new file mode 100644 index 0000000000..8f87cb90d6 --- /dev/null +++ b/tests/erlang_tests/test_crypto_crypto.erl @@ -0,0 +1,414 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_crypto_crypto). +-export([ + start/0, + test_encrypt_aes128_ctr/0, + get_bad/0, + get_list/0 +]). + +start() -> + ok = test_encrypt_aes128_ctr(), + ok = test_encrypt_aes128_ctr_list(), + ok = test_decrypt_aes128_cbc(), + ok = test_encrypt_aes128_cbc_bool(), + ok = test_use_after_final(), + ok = test_ecb_less_than_one_block(), + ok = test_cbc_final_padding(), + ok = test_ecb_final_no_padding(), + ok = test_encrypt_aes256_ecb(), + ok = test_bad_key_size(), + ok = test_bad_iv_size(), + ok = test_bad_algo(), + ok = test_bad_key(), + ok = test_bad_iv(), + ok = test_bad_opt(), + ok = test_bad_padding_ecb(), + ok = test_bad_state(), + ok = test_bad_data(), + + 0. + +test_encrypt_aes128_ctr() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + EncryptState = crypto:crypto_init(aes_128_ctr, Key, IV, [{encrypt, true}]), + true = is_reference(EncryptState), + <<66, 241, 103, 217, 46, 78, 167, 42, 131, 175, 240, 120, 231, 114, 203, 123>> = crypto:crypto_update( + EncryptState, <<"Hello World !!!!">> + ), + <<74, 6, 128, 248, 9, 56, 37, 249, 232, 182, 153, 47, 106, 133, 46, 253>> = crypto:crypto_update( + EncryptState, <<"Hello World !!!!">> + ), + <<"">> = crypto:crypto_final(EncryptState), + ok. + +test_encrypt_aes128_ctr_list() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + EncryptState = crypto:crypto_init(aes_128_ctr, Key, IV, [{encrypt, true}]), + true = is_reference(EncryptState), + <<66, 241, 103, 217, 46, 78, 167, 42, 131, 175, 240, 120, 231, 114, 203, 123>> = crypto:crypto_update( + EncryptState, [$H, $e, <<"llo ">>, <<"World !!!!">>] + ), + <<74, 6, 128, 248, 9, 56, 37, 249, 232, 182, 153, 47, 106, 133, 46, 253>> = crypto:crypto_update( + EncryptState, [<<"Hello World ">>, <<"!!!!">>] + ), + <<"">> = crypto:crypto_final(EncryptState), + ok. + +test_decrypt_aes128_cbc() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + DecryptState = crypto:crypto_init(aes_128_ctr, Key, IV, [{encrypt, false}]), + true = is_reference(DecryptState), + <<"0123456789ABCDEF0123456789ABCDEF">> = crypto:crypto_update( + DecryptState, + <<58, 165, 57, 134, 117, 91, 198, 114, 201, 250, 213, 26, 133, 23, 175, 28, 50, 82, 222, + 167, 82, 45, 68, 161, 162, 227, 188, 77, 8, 224, 74, 154>> + ), + <<"">> = crypto:crypto_final(DecryptState), + ok. + +test_encrypt_aes128_cbc_bool() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + EncryptState = crypto:crypto_init(aes_128_ctr, Key, IV, true), + true = is_reference(EncryptState), + <<58, 165, 57, 134, 117, 91, 198, 114, 201, 250, 213, 26, 133, 23, 175, 28, 50, 82, 222, 167, + 82, 45, 68, 161, 162, 227, 188, 77, 8, 224, 74, 154>> = crypto:crypto_update( + EncryptState, <<"0123456789ABCDEF0123456789ABCDEF">> + ), + <<"">> = crypto:crypto_final(EncryptState), + + DecryptState = crypto:crypto_init(aes_128_ctr, Key, IV, false), + true = is_reference(DecryptState), + <<"0123456789ABCDEF0123456789ABCDEF">> = crypto:crypto_update( + DecryptState, + <<58, 165, 57, 134, 117, 91, 198, 114, 201, 250, 213, 26, 133, 23, 175, 28, 50, 82, 222, + 167, 82, 45, 68, 161, 162, 227, 188, 77, 8, 224, 74, 154>> + ), + <<"">> = crypto:crypto_final(DecryptState), + + ok. + +%% AtomVM does not allow reuse of a crypto state after crypto_final/1. +%% This test verifies that both crypto_update/2 and crypto_final/1 raise a +%% badarg error when called on a finalized state. +%% +%% On the BEAM, reuse after final is allowed, so this test is skipped there. +test_use_after_final() -> + case erlang:system_info(machine) of + "BEAM" -> + ok; + "ATOM" -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + State = crypto:crypto_init(aes_128_ctr, Key, IV, [{encrypt, true}]), + <<64, 225, 120, 193, 97, 38, 149, 41, 157, 172>> = crypto:crypto_update( + State, <<"Just Hello">> + ), + <<"">> = crypto:crypto_final(State), + + exp_err = + try + crypto:crypto_update(State, <<"Hello World !!!!">>) + catch + error:{badarg, {File1, Line1}, + "Bad state: AtomVM does not allow operations after crypto_final"} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + + exp_err = + try + crypto:crypto_final(State) + catch + error:{badarg, {File2, Line2}, + "Bad state: AtomVM does not allow calling crypto_final more than once"} when + is_list(File2) andalso is_integer(Line2) + -> + exp_err + end, + + ok + end. + +test_ecb_less_than_one_block() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + EncryptState = crypto:crypto_init(aes_128_ecb, Key, IV, [{encrypt, true}]), + true = is_reference(EncryptState), + <<"">> = crypto:crypto_update(EncryptState, <<"01234567">>), + <<165, 186, 43, 98, 128, 212, 51, 188, 207, 230, 160, 70, 28, 184, 140, 78>> = crypto:crypto_update( + EncryptState, <<"89ABCDEF">> + ), + <<"">> = crypto:crypto_final(EncryptState), + ok. + +%% PKCS7 padding is supported only with CBC ciphers via the PSA_ALG_CBC_PKCS7 +%% algorithm. Feed 14 bytes in two updates (7 + 7); PSA buffers the incomplete +%% block and on crypto_final emits the PKCS7-padded ciphertext (one 16-byte +%% block with 2 bytes of padding value 0x02). +%% +%% Expected output was computed with OTP crypto on the BEAM. +test_cbc_final_padding() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + EncryptState = crypto:crypto_init(aes_128_cbc, Key, IV, [ + {padding, pkcs_padding}, {encrypt, true} + ]), + true = is_reference(EncryptState), + <<"">> = crypto:crypto_update(EncryptState, <<"1234567">>), + <<"">> = crypto:crypto_update(EncryptState, <<"89ABCDE">>), + <<45, 86, 231, 103, 240, 183, 169, 144, 1, 145, 121, 129, 65, 23, 60, 94>> = + crypto:crypto_final(EncryptState), + ok. + +test_ecb_final_no_padding() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + EncryptState = crypto:crypto_init(aes_128_ecb, Key, IV, [{padding, none}, {encrypt, true}]), + true = is_reference(EncryptState), + <<"">> = crypto:crypto_update(EncryptState, <<"1234567">>), + <<"">> = crypto:crypto_update(EncryptState, <<"89ABCDE">>), + exp_err = + try + crypto:crypto_final(EncryptState) + catch + error:{error, {File1, Line1}, "Padding 'none' but unfilled last block"} when + is_list(File1) and is_integer(Line1) + -> + exp_err + end, + ok. + +test_encrypt_aes256_ecb() -> + Key = + <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15>>, + IV = <<>>, + EncryptState = crypto:crypto_init(aes_256_ecb, Key, IV, [{encrypt, true}]), + true = is_reference(EncryptState), + <<35, 71, 82, 126, 155, 68, 42, 51, 62, 71, 70, 111, 54, 5, 236, 105>> = crypto:crypto_update( + EncryptState, <<"Hello World 1234">> + ), + <<100, 132, 105, 177, 37, 34, 214, 229, 166, 252, 187, 188, 14, 140, 71, 253>> = crypto:crypto_update( + EncryptState, <<"Hello World ????">> + ), + <<"">> = crypto:crypto_final(EncryptState), + ok. + +test_bad_key_size() -> + Key15Bytes = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + exp_err = + try + crypto:crypto_init(aes_128_ctr, Key15Bytes, IV, [{encrypt, true}]) + catch + error:{badarg, {File1, Line1}, "Bad key size"} when + is_list(File1) and is_integer(Line1) + -> + exp_err + end, + + Key17Bytes = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16>>, + exp_err = + try + crypto:crypto_init(aes_128_ctr, Key17Bytes, IV, [{encrypt, true}]) + catch + error:{badarg, {File2, Line2}, "Bad key size"} when + is_list(File2) and is_integer(Line2) + -> + exp_err + end, + + ok. + +test_bad_iv_size() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV15Bytes = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14>>, + try + crypto:crypto_init(aes_128_ctr, Key, IV15Bytes, [{encrypt, false}]) + catch + error:{badarg, {File1, Line1}, "Bad iv size"} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + + IV17Bytes = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16>>, + try + crypto:crypto_init(aes_128_ctr, Key, IV17Bytes, [{encrypt, false}]) + catch + error:{badarg, {File2, Line2}, "Bad iv size"} when + is_list(File2) andalso is_integer(Line2) + -> + exp_err + end, + + ok. + +test_bad_algo() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + try + crypto:crypto_init(?MODULE:get_bad(), Key, IV, [{encrypt, true}]) + catch + error:{badarg, {File, Line}, "Unknown cipher"} when + is_list(File) andalso is_integer(Line) + -> + exp_err + end, + + ok. + +test_bad_key() -> + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + try + crypto:crypto_init(aes_128_ctr, ?MODULE:get_bad(), IV, [{encrypt, true}]) + catch + error:{badarg, {File1, Line1}, "Bad key"} when is_list(File1) andalso is_integer(Line1) -> + exp_err + end, + + try + crypto:crypto_init(aes_128_ctr, ?MODULE:get_list(), IV, [{encrypt, true}]) + catch + error:{badarg, {File2, Line2}, "Bad key"} when is_list(File2) andalso is_integer(Line2) -> + exp_err + end, + + ok. + +test_bad_iv() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + try + crypto:crypto_init(aes_128_ctr, Key, ?MODULE:get_bad(), [{encrypt, true}]) + catch + error:{badarg, {File1, Line1}, "Bad iv type"} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + + try + crypto:crypto_init(aes_128_ctr, Key, ?MODULE:get_list(), [{encrypt, true}]) + catch + error:{badarg, {File2, Line2}, "Bad iv type"} when + is_list(File2) andalso is_integer(Line2) + -> + exp_err + end, + + ok. + +test_bad_opt() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + + % We don't care for the following error messages, OTP is very specific, but we don't have to + + try + crypto:crypto_init(?MODULE:get_bad(), Key, IV, [{encrypt, true}, ?MODULE:get_bad()]) + catch + error:{badarg, {File1, Line1}, _DontCare1} when is_list(File1) andalso is_integer(Line1) -> + exp_err + end, + + try + crypto:crypto_init(?MODULE:get_bad(), Key, IV, [{encrypt, true}, {?MODULE:get_bad(), true}]) + catch + error:{badarg, {File2, Line2}, _DontCare2} when is_list(File2) andalso is_integer(Line2) -> + exp_err + end, + + try + crypto:crypto_init(?MODULE:get_bad(), Key, IV, [{encrypt, ?MODULE:get_bad()}]) + catch + error:{badarg, {File3, Line3}, _DontCare3} when is_list(File3) andalso is_integer(Line3) -> + exp_err + end, + + ok. + +%% AtomVM supports PKCS7 padding only with CBC ciphers. Requesting +%% pkcs_padding with ECB (or any non-CBC cipher) raises a badarg error. +%% This differs from OTP which silently accepts the option for ECB. +test_bad_padding_ecb() -> + case erlang:system_info(machine) of + "BEAM" -> + ok; + "ATOM" -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<>>, + exp_err = + try + crypto:crypto_init(aes_128_ecb, Key, IV, [ + {padding, pkcs_padding}, {encrypt, true} + ]) + catch + error:{badarg, {File, Line}, + "PKCS padding is supported only with CBC ciphers"} when + is_list(File) andalso is_integer(Line) + -> + exp_err + end, + ok + end. + +test_bad_state() -> + try + crypto:crypto_update(?MODULE:get_bad(), <<"Test">>) + catch + error:{badarg, {File1, Line1}, "Bad State"} when is_list(File1) andalso is_integer(Line1) -> + exp_err + end, + + try + crypto:crypto_final(?MODULE:get_bad()) + catch + error:{badarg, {File2, Line2}, "Bad State"} when is_list(File2) andalso is_integer(Line2) -> + exp_err + end, + + ok. + +test_bad_data() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + EncryptState = crypto:crypto_init(aes_128_ctr, Key, IV, [{encrypt, true}]), + try + crypto:crypto_update(EncryptState, ?MODULE:get_bad()) + catch + error:{badarg, {File, Line}, "expected binary"} when + is_list(File) andalso is_integer(Line) + -> + exp_err + end, + + ok. + +get_bad() -> foo. + +get_list() -> [<<"0123456789">>, <<"ABCDEF">>]. diff --git a/tests/test.c b/tests/test.c index 21fd4bbd03..a9a766d066 100644 --- a/tests/test.c +++ b/tests/test.c @@ -629,6 +629,7 @@ struct Test tests[] = { TEST_CASE(test_crypto_pk), TEST_CASE(test_crypto_mac), TEST_CASE(test_crypto_hash_update), + TEST_CASE(test_crypto_crypto), // TEST CRASHES HERE: TEST_CASE(memlimit), From 2c4421868454e117eb1b5237a0a3a24c8b421fb6 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 4 Mar 2026 15:35:30 +0100 Subject: [PATCH 06/13] WIP test_crypto_aead tests --- libs/estdlib/src/crypto.erl | 64 +++++ src/libAtomVM/otp_crypto.c | 285 ++++++++++++++++++ tests/erlang_tests/CMakeLists.txt | 2 + tests/erlang_tests/test_crypto_aead.erl | 366 ++++++++++++++++++++++++ tests/test.c | 1 + 5 files changed, 718 insertions(+) create mode 100644 tests/erlang_tests/test_crypto_aead.erl diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index 5a8e7796e3..f7c8679b0b 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -27,6 +27,8 @@ hash_final/1, crypto_one_time/4, crypto_one_time/5, + crypto_one_time_aead/6, + crypto_one_time_aead/7, crypto_init/3, crypto_init/4, crypto_update/2, @@ -72,6 +74,15 @@ | aes_192_ofb | aes_256_ofb. +-type cipher_aead() :: + aes_128_gcm + | aes_192_gcm + | aes_256_gcm + | aes_128_ccm + | aes_192_ccm + | aes_256_ccm + | chacha20_poly1305. + -type padding() :: none | pkcs_padding. -type crypto_opt() :: {encrypt, boolean()} | {padding, padding()}. @@ -211,6 +222,59 @@ crypto_one_time(_Cipher, _Key, _Data, _FlagOrOptions) -> crypto_one_time(_Cipher, _Key, _IV, _Data, _FlagOrOptions) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Cipher an AEAD cipher +%% @param Key the encryption key +%% @param IV nonce / initialization vector +%% @param InText plaintext to encrypt (iodata) +%% @param AAD additional authenticated data (iodata) +%% @param EncFlag `true` for encryption +%% @returns Returns `{CipherText, Tag}` on success. +%% @doc Encrypt data using an AEAD cipher with the default tag length. +%% +%% Supported ciphers: `aes_128_gcm`, `aes_192_gcm`, `aes_256_gcm`, +%% `aes_128_ccm`, `aes_192_ccm`, `aes_256_ccm`, `chacha20_poly1305`. +%% @end +%%----------------------------------------------------------------------------- +-spec crypto_one_time_aead( + Cipher :: cipher_aead(), + Key :: iodata(), + IV :: iodata(), + InText :: iodata(), + AAD :: iodata(), + EncFlag :: true +) -> {binary(), binary()}. +crypto_one_time_aead(_Cipher, _Key, _IV, _InText, _AAD, _EncFlag) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Cipher an AEAD cipher +%% @param Key the encryption / decryption key +%% @param IV nonce / initialization vector +%% @param InText plaintext (encrypt) or ciphertext (decrypt) (iodata) +%% @param AAD additional authenticated data (iodata) +%% @param TagOrTagLength integer tag length when encrypting, tag binary when decrypting +%% @param EncFlag `true` for encryption, `false` for decryption +%% @returns Encryption: `{CipherText, Tag}`. Decryption: plaintext binary, or `error` on +%% authentication failure. +%% @doc Encrypt or decrypt data using an AEAD cipher. +%% +%% When decrypting, authentication failure returns the atom `error` +%% rather than raising an exception, matching OTP behaviour. +%% @end +%%----------------------------------------------------------------------------- +-spec crypto_one_time_aead( + Cipher :: cipher_aead(), + Key :: iodata(), + IV :: iodata(), + InText :: iodata(), + AAD :: iodata(), + TagOrTagLength :: non_neg_integer() | binary(), + EncFlag :: boolean() +) -> {binary(), binary()} | binary() | error. +crypto_one_time_aead(_Cipher, _Key, _IV, _InText, _AAD, _TagOrTagLength, _EncFlag) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Cipher a supported cipher (no IV required, e.g. ECB modes) %% @param Key the encryption / decryption key diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index 5961756bc2..582b21f37c 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -2147,6 +2147,279 @@ static term nif_crypto_crypto_final(Context *ctx, int argc, term argv[]) return result; } +struct PsaAeadParams +{ + AtomString atom_str; + psa_key_type_t key_type; + psa_algorithm_t algorithm; + uint16_t key_bits; + uint8_t default_tag_len; +}; + +static const struct PsaAeadParams psa_aead_table[] = { + { ATOM_STR("\xB", "aes_128_gcm"), PSA_KEY_TYPE_AES, PSA_ALG_GCM, 128, 16 }, + { ATOM_STR("\xB", "aes_192_gcm"), PSA_KEY_TYPE_AES, PSA_ALG_GCM, 192, 16 }, + { ATOM_STR("\xB", "aes_256_gcm"), PSA_KEY_TYPE_AES, PSA_ALG_GCM, 256, 16 }, + { ATOM_STR("\xB", "aes_128_ccm"), PSA_KEY_TYPE_AES, PSA_ALG_CCM, 128, 16 }, + { ATOM_STR("\xB", "aes_192_ccm"), PSA_KEY_TYPE_AES, PSA_ALG_CCM, 192, 16 }, + { ATOM_STR("\xB", "aes_256_ccm"), PSA_KEY_TYPE_AES, PSA_ALG_CCM, 256, 16 }, + { ATOM_STR("\x11", "chacha20_poly1305"), PSA_KEY_TYPE_CHACHA20, PSA_ALG_CHACHA20_POLY1305, 256, + 16 }, +}; + +#define PSA_AEAD_TABLE_LEN (sizeof(psa_aead_table) / sizeof(psa_aead_table[0])) + +static const struct PsaAeadParams *psa_aead_table_lookup(GlobalContext *glb, term cipher_atom) +{ + for (size_t i = 0; i < PSA_AEAD_TABLE_LEN; i++) { + AtomString atom_str = psa_aead_table[i].atom_str; + if (globalcontext_is_term_equal_to_atom_string(glb, cipher_atom, atom_str)) { + return &psa_aead_table[i]; + } + } + return NULL; +} + +static term nif_crypto_crypto_one_time_aead(Context *ctx, int argc, term argv[]) +{ + do_psa_init(); + + GlobalContext *glb = ctx->global; + + term cipher_atom = argv[0]; + const struct PsaAeadParams *aead_params = psa_aead_table_lookup(glb, cipher_atom); + if (IS_NULL_PTR(aead_params)) { + size_t needed = TUPLE_SIZE(2) + (strlen("Unknown cipher") * CONS_SIZE); + if (UNLIKELY(memory_ensure_free(ctx, needed) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term desc = interop_bytes_to_list("Unknown cipher", strlen("Unknown cipher"), &ctx->heap); + term err_t = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(err_t, 0, BADARG_ATOM); + term_put_tuple_element(err_t, 1, desc); + RAISE_ERROR(err_t); + } + + bool encrypting; + term enc_flag_term; + term tag_or_len_term = UNDEFINED_ATOM; + + if (argc == 6) { + enc_flag_term = argv[5]; + } else { + tag_or_len_term = argv[5]; + enc_flag_term = argv[6]; + } + + if (enc_flag_term == TRUE_ATOM) { + encrypting = true; + } else if (enc_flag_term == FALSE_ATOM) { + encrypting = false; + } else { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "EncFlag must be a boolean", ctx)); + } + + size_t tag_len = aead_params->default_tag_len; + const void *tag_data = NULL; + size_t tag_data_len = 0; + + if (argc == 7) { + if (encrypting) { + if (UNLIKELY(!term_is_int(tag_or_len_term))) { + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "TagLength must be an integer", ctx)); + } + avm_int_t requested = term_to_int(tag_or_len_term); + if (UNLIKELY(requested < 0)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad tag length", ctx)); + } + tag_len = requested; + } else { + if (UNLIKELY(!term_is_binary(tag_or_len_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Tag must be a binary", ctx)); + } + tag_data = term_binary_data(tag_or_len_term); + tag_data_len = term_binary_size(tag_or_len_term); + tag_len = tag_data_len; + } + } + + psa_algorithm_t psa_algo; + if (tag_len != aead_params->default_tag_len) { + psa_algo = PSA_ALG_AEAD_WITH_SHORTENED_TAG(aead_params->algorithm, tag_len); + } else { + psa_algo = aead_params->algorithm; + } + + term key_term = argv[1]; + if (UNLIKELY(!term_is_binary(key_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad key", ctx)); + } + size_t key_len = term_binary_size(key_term); + if (UNLIKELY(key_len != (size_t) (aead_params->key_bits / 8))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad key size", ctx)); + } + const void *key_data = term_binary_data(key_term); + + term iv_term = argv[2]; + if (UNLIKELY(!term_is_binary(iv_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad iv", ctx)); + } + const void *iv_data = term_binary_data(iv_term); + size_t iv_len = term_binary_size(iv_term); + + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, aead_params->key_type); + psa_set_key_bits(&attr, aead_params->key_bits); + psa_set_key_usage_flags(&attr, encrypting ? PSA_KEY_USAGE_ENCRYPT : PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attr, psa_algo); + + psa_key_id_t key_id = 0; + psa_status_t status = psa_import_key(&attr, key_data, key_len, &key_id); + psa_reset_key_attributes(&attr); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx)); + default: + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx)); + } + + bool success = false; + term result = ERROR_ATOM; + void *maybe_allocated_intext = NULL; + void *maybe_allocated_aad = NULL; + void *out_buf = NULL; + + const void *intext_data; + size_t intext_len; + term iodata_result = handle_iodata(argv[3], &intext_data, &intext_len, &maybe_allocated_intext); + if (UNLIKELY(iodata_result != OK_ATOM)) { + result = make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx); + goto cleanup; + } + + const void *aad_data; + size_t aad_len; + iodata_result = handle_iodata(argv[4], &aad_data, &aad_len, &maybe_allocated_aad); + if (UNLIKELY(iodata_result != OK_ATOM)) { + result = make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx); + goto cleanup; + } + + if (encrypting) { + size_t out_size = PSA_AEAD_ENCRYPT_OUTPUT_SIZE(aead_params->key_type, psa_algo, intext_len); + out_buf = malloc(out_size); + if (IS_NULL_PTR(out_buf)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + size_t out_len = 0; + status = psa_aead_encrypt(key_id, psa_algo, iv_data, iv_len, aad_data, aad_len, intext_data, + intext_len, out_buf, out_size, &out_len); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + result = make_crypto_error( + __FILE__, __LINE__, "Unsupported algorithm or parameters", ctx); + goto cleanup; + case PSA_ERROR_INVALID_ARGUMENT: + result = make_crypto_error(__FILE__, __LINE__, "Invalid argument", ctx); + goto cleanup; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + size_t ct_len = out_len - tag_len; + + if (UNLIKELY( + memory_ensure_free(ctx, + TERM_BINARY_HEAP_SIZE(ct_len) + TERM_BINARY_HEAP_SIZE(tag_len) + TUPLE_SIZE(2)) + != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + term ct_bin = term_from_literal_binary(out_buf, ct_len, &ctx->heap, glb); + term tag_bin + = term_from_literal_binary((uint8_t *) out_buf + ct_len, tag_len, &ctx->heap, glb); + success = true; + result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, ct_bin); + term_put_tuple_element(result, 1, tag_bin); + + } else { + size_t ct_len = intext_len; + size_t combined_len = ct_len + tag_data_len; + void *combined_buf = malloc(combined_len); + if (IS_NULL_PTR(combined_buf)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + memcpy(combined_buf, intext_data, ct_len); + memcpy((uint8_t *) combined_buf + ct_len, tag_data, tag_data_len); + + size_t pt_size + = PSA_AEAD_DECRYPT_OUTPUT_SIZE(aead_params->key_type, psa_algo, combined_len); + out_buf = malloc(pt_size + 1); // +1 to ensure valid malloc even for 0 + if (IS_NULL_PTR(out_buf)) { + free(combined_buf); + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + size_t pt_len = 0; + status = psa_aead_decrypt(key_id, psa_algo, iv_data, iv_len, aad_data, aad_len, + combined_buf, combined_len, out_buf, pt_size, &pt_len); + free(combined_buf); + + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_INVALID_SIGNATURE: + // Authentication failed, return atom `error`, no exception + success = true; + result = ERROR_ATOM; + goto cleanup; + case PSA_ERROR_NOT_SUPPORTED: + result = make_crypto_error( + __FILE__, __LINE__, "Unsupported algorithm or parameters", ctx); + goto cleanup; + case PSA_ERROR_INVALID_ARGUMENT: + result = make_crypto_error(__FILE__, __LINE__, "Invalid argument", ctx); + goto cleanup; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(pt_len)) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + success = true; + result = term_from_literal_binary(out_buf, pt_len, &ctx->heap, glb); + } + +cleanup: + psa_destroy_key(key_id); + free(maybe_allocated_intext); + free(maybe_allocated_aad); + free(out_buf); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + #endif // not static since we are using it elsewhere to provide backward compatibility @@ -2273,6 +2546,10 @@ static const struct Nif crypto_crypto_final_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_crypto_final }; +static const struct Nif crypto_crypto_one_time_aead_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_crypto_one_time_aead +}; #endif static const struct Nif crypto_strong_rand_bytes_nif = { .base.type = NIFFunctionType, @@ -2352,6 +2629,14 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_crypto_final_nif; } + if (strcmp("crypto_one_time_aead/6", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_crypto_one_time_aead_nif; + } + if (strcmp("crypto_one_time_aead/7", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_crypto_one_time_aead_nif; + } #endif if (strcmp("strong_rand_bytes/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 6879338544..d49fc51fd9 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -664,6 +664,7 @@ compile_erlang(test_crypto_pk) compile_erlang(test_crypto_mac) compile_erlang(test_crypto_hash_update) compile_erlang(test_crypto_crypto) +compile_erlang(test_crypto_aead) set(erlang_test_beams add.beam @@ -1206,6 +1207,7 @@ set(erlang_test_beams test_crypto_mac.beam test_crypto_hash_update.beam test_crypto_crypto.beam + test_crypto_aead.beam ) if(NOT AVM_DISABLE_JIT) diff --git a/tests/erlang_tests/test_crypto_aead.erl b/tests/erlang_tests/test_crypto_aead.erl new file mode 100644 index 0000000000..73fdb86f04 --- /dev/null +++ b/tests/erlang_tests/test_crypto_aead.erl @@ -0,0 +1,366 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_crypto_aead). +-export([ + start/0, + get_bad/0, + get_list/0 +]). + +start() -> + ok = test_aes_128_gcm(), + ok = test_aes_128_gcm_custom_tag(), + ok = test_aes_192_gcm(), + ok = test_aes_256_gcm(), + ok = test_chacha20_poly1305(), + ok = test_aes_128_ccm(), + ok = test_aes_128_ccm_short_tag(), + ok = test_aes_256_ccm(), + ok = test_gcm_empty_aad(), + ok = test_gcm_empty_plaintext(), + ok = test_gcm_single_byte(), + ok = test_gcm_multi_block(), + ok = test_gcm_iolist_input(), + ok = test_gcm_wrong_tag(), + ok = test_gcm_wrong_aad(), + ok = test_ccm_wrong_tag(), + ok = test_bad_cipher(), + ok = test_bad_key_size(), + ok = test_bad_key(), + ok = test_bad_iv(), + ok = test_bad_text(), + ok = test_bad_aad(), + ok = test_bad_enc_flag(), + 0. + +test_aes_128_gcm() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + AAD = <<"additional data">>, + ExpCT = <<219, 9, 203, 162, 9, 59, 160, 59, 57, 190, 5, 171>>, + ExpTag = <<217, 60, 70, 220, 33, 186, 23, 31, 40, 99, 108, 203, 171, 45, 198, 199>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, Text, AAD, true), + Text = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, ExpCT, AAD, ExpTag, false), + ok. + +test_aes_128_gcm_custom_tag() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + AAD = <<"additional data">>, + ExpCT = <<219, 9, 203, 162, 9, 59, 160, 59, 57, 190, 5, 171>>, + ExpTag12 = <<217, 60, 70, 220, 33, 186, 23, 31, 40, 99, 108, 203>>, + + {ExpCT, ExpTag12} = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, Text, AAD, 12, true), + Text = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, ExpCT, AAD, ExpTag12, false), + ok. + +test_aes_192_gcm() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + AAD = <<"additional data">>, + ExpCT = <<174, 156, 78, 247, 246, 153, 158, 97, 162, 95, 184, 163>>, + ExpTag = <<200, 59, 103, 157, 222, 78, 200, 253, 99, 56, 127, 114, 136, 106, 166, 184>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(aes_192_gcm, Key, IV, Text, AAD, true), + Text = crypto:crypto_one_time_aead(aes_192_gcm, Key, IV, ExpCT, AAD, ExpTag, false), + ok. + +test_aes_256_gcm() -> + Key = + <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + AAD = <<"additional data">>, + ExpCT = <<15, 103, 186, 119, 170, 197, 149, 116, 255, 45, 243, 170>>, + ExpTag = <<197, 30, 63, 146, 222, 109, 48, 198, 91, 203, 80, 4, 171, 113, 230, 145>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(aes_256_gcm, Key, IV, Text, AAD, true), + Text = crypto:crypto_one_time_aead(aes_256_gcm, Key, IV, ExpCT, AAD, ExpTag, false), + ok. + +test_chacha20_poly1305() -> + Key = + <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + AAD = <<"additional data">>, + ExpCT = <<193, 158, 100, 108, 70, 55, 242, 47, 197, 239, 91, 210>>, + ExpTag = <<97, 69, 212, 254, 35, 192, 149, 166, 45, 215, 96, 33, 97, 153, 201, 7>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(chacha20_poly1305, Key, IV, Text, AAD, true), + Text = crypto:crypto_one_time_aead(chacha20_poly1305, Key, IV, ExpCT, AAD, ExpTag, false), + ok. + +test_aes_128_ccm() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + AAD = <<"additional data">>, + ExpCT = <<123, 112, 159, 11, 179, 160, 147, 222, 3, 127, 173, 193>>, + ExpTag = <<185, 41, 226, 20, 214, 206, 126, 253, 73, 241, 14, 245, 208, 205, 103, 221>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(aes_128_ccm, Key, IV, Text, AAD, 16, true), + Text = crypto:crypto_one_time_aead(aes_128_ccm, Key, IV, ExpCT, AAD, ExpTag, false), + ok. + +test_aes_128_ccm_short_tag() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + AAD = <<"additional data">>, + ExpCT = <<123, 112, 159, 11, 179, 160, 147, 222, 3, 127, 173, 193>>, + ExpTag8 = <<24, 105, 116, 10, 159, 68, 212, 36>>, + + {ExpCT, ExpTag8} = crypto:crypto_one_time_aead(aes_128_ccm, Key, IV, Text, AAD, 8, true), + Text = crypto:crypto_one_time_aead(aes_128_ccm, Key, IV, ExpCT, AAD, ExpTag8, false), + ok. + +test_aes_256_ccm() -> + Key = + <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + AAD = <<"additional data">>, + ExpCT = <<194, 176, 212, 122, 81, 15, 158, 248, 222, 165, 213, 2>>, + ExpTag = <<8, 177, 253, 246, 77, 128, 145, 236, 35, 24, 242, 144, 95, 121, 102, 135>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(aes_256_ccm, Key, IV, Text, AAD, 16, true), + Text = crypto:crypto_one_time_aead(aes_256_ccm, Key, IV, ExpCT, AAD, ExpTag, false), + ok. + +test_gcm_empty_aad() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Text = <<"Hello World!">>, + ExpCT = <<219, 9, 203, 162, 9, 59, 160, 59, 57, 190, 5, 171>>, + ExpTag = <<75, 143, 238, 84, 229, 251, 14, 134, 50, 98, 23, 182, 171, 214, 143, 119>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, Text, <<>>, true), + Text = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, ExpCT, <<>>, ExpTag, false), + ok. + +test_gcm_empty_plaintext() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + AAD = <<"additional data">>, + ExpTag = <<255, 87, 83, 41, 97, 2, 247, 149, 52, 155, 82, 96, 180, 3, 95, 179>>, + + {<<>>, ExpTag} = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, <<>>, AAD, true), + <<>> = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, <<>>, AAD, ExpTag, false), + ok. + +test_gcm_single_byte() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + ExpCT = <<185>>, + ExpTag = <<223, 155, 197, 124, 191, 43, 44, 98, 180, 217, 233, 184, 221, 58, 44, 192>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, <<42>>, <<>>, true), + <<42>> = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, ExpCT, <<>>, ExpTag, false), + ok. + +test_gcm_multi_block() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + AAD = <<"additional data">>, + Text = + <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47>>, + ExpCT = + <<147, 109, 165, 205, 98, 30, 241, 83, 67, 219, 107, 129, 58, 174, 126, 7, 163, 55, 8, 245, + 71, 248, 235, 225, 254, 56, 235, 54, 8, 89, 188, 115, 165, 133, 249, 212, 208, 165, 145, + 196, 104, 221, 35, 204, 236, 164, 249, 189>>, + ExpTag = <<50, 121, 28, 32, 128, 57, 158, 87, 158, 161, 22, 187, 55, 32, 98, 230>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, Text, AAD, true), + Text = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, ExpCT, AAD, ExpTag, false), + ok. + +test_gcm_iolist_input() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + ExpCT = <<219, 9, 203, 162, 9, 59, 160, 59, 57, 190, 5, 171>>, + ExpTag = <<217, 60, 70, 220, 33, 186, 23, 31, 40, 99, 108, 203, 171, 45, 198, 199>>, + + {ExpCT, ExpTag} = crypto:crypto_one_time_aead( + aes_128_gcm, + Key, + IV, + [<<"Hello">>, <<" World!">>], + [<<"additional">>, <<" data">>], + true + ), + ok. + +test_gcm_wrong_tag() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + CT = <<218, 9, 203, 162, 9, 59, 160, 59, 57, 190, 5, 171>>, + AAD = <<"additional data">>, + WrongTag = <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, + + error = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, CT, AAD, WrongTag, false), + ok. + +test_gcm_wrong_aad() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + CT = <<219, 9, 203, 162, 9, 59, 160, 59, 57, 190, 5, 171>>, + Tag = <<217, 60, 70, 220, 33, 186, 23, 31, 40, 99, 108, 203, 171, 45, 198, 199>>, + + error = crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, CT, <<"wrong aad">>, Tag, false), + ok. + +test_ccm_wrong_tag() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + CT = <<123, 112, 159, 11, 179, 160, 147, 222, 3, 127, 173, 193>>, + AAD = <<"additional data">>, + WrongTag = <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, + + error = crypto:crypto_one_time_aead(aes_128_ccm, Key, IV, CT, AAD, WrongTag, false), + ok. + +test_bad_cipher() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + + exp_err = + try + crypto:crypto_one_time_aead(?MODULE:get_bad(), Key, IV, <<"test">>, <<>>, true) + catch + error:{badarg, _Desc} -> + exp_err + end, + ok. + +test_bad_key_size() -> + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + Key15 = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14>>, + Key17 = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16>>, + + exp_err = + try + crypto:crypto_one_time_aead(aes_128_gcm, Key15, IV, <<"test">>, <<>>, true) + catch + error:{badarg, {File1, Line1}, _Desc1} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + + exp_err = + try + crypto:crypto_one_time_aead(aes_128_gcm, Key17, IV, <<"test">>, <<>>, true) + catch + error:{badarg, {File2, Line2}, _Desc2} when + is_list(File2) andalso is_integer(Line2) + -> + exp_err + end, + ok. + +test_bad_key() -> + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + + exp_err = + try + crypto:crypto_one_time_aead(aes_128_gcm, ?MODULE:get_bad(), IV, <<"test">>, <<>>, true) + catch + error:{badarg, {File1, Line1}, _Desc1} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + ok. + +test_bad_iv() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + + exp_err = + try + crypto:crypto_one_time_aead(aes_128_gcm, Key, ?MODULE:get_bad(), <<"test">>, <<>>, true) + catch + error:{badarg, {File1, Line1}, _Desc1} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + ok. + +test_bad_text() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + + exp_err = + try + crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, ?MODULE:get_bad(), <<>>, true) + catch + error:{badarg, {File1, Line1}, _Desc1} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + ok. + +test_bad_aad() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + + exp_err = + try + crypto:crypto_one_time_aead(aes_128_gcm, Key, IV, <<"test">>, ?MODULE:get_bad(), true) + catch + error:{badarg, {File1, Line1}, _Desc1} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + ok. + +test_bad_enc_flag() -> + Key = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + IV = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11>>, + + exp_err = + try + crypto:crypto_one_time_aead( + aes_128_gcm, Key, IV, <<"test">>, <<>>, 16, ?MODULE:get_bad() + ) + catch + error:{badarg, {File1, Line1}, _Desc1} when + is_list(File1) andalso is_integer(Line1) + -> + exp_err + end, + ok. + +get_bad() -> foo. + +get_list() -> [<<"0123456789">>, <<"ABCDEF">>]. diff --git a/tests/test.c b/tests/test.c index a9a766d066..f167622fb3 100644 --- a/tests/test.c +++ b/tests/test.c @@ -630,6 +630,7 @@ struct Test tests[] = { TEST_CASE(test_crypto_mac), TEST_CASE(test_crypto_hash_update), TEST_CASE(test_crypto_crypto), + TEST_CASE(test_crypto_aead), // TEST CRASHES HERE: TEST_CASE(memlimit), From 73164325235dee9bdf6daea10433fbadeee902d3 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 6 Mar 2026 22:48:48 +0100 Subject: [PATCH 07/13] pbkdf2_hmac tests Signed-off-by: Davide Bettio --- libs/estdlib/src/crypto.erl | 24 +++ src/libAtomVM/otp_crypto.c | 134 ++++++++++++ tests/erlang_tests/CMakeLists.txt | 2 + .../erlang_tests/test_crypto_pbkdf2_hmac.erl | 191 ++++++++++++++++++ tests/test.c | 1 + 5 files changed, 352 insertions(+) create mode 100644 tests/erlang_tests/test_crypto_pbkdf2_hmac.erl diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index f7c8679b0b..26cd5ff893 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -38,6 +38,7 @@ sign/4, verify/5, mac/4, + pbkdf2_hmac/5, strong_rand_bytes/1, info_lib/0 ]). @@ -488,6 +489,29 @@ verify(_Algorithm, _DigestType, _Data, _Signature, _Key) -> mac(_Type, _SubType, _Key, _Data) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param DigestType hash algorithm (`sha`, `sha256`, etc.) +%% @param Password password bytes (iodata) +%% @param Salt salt bytes (iodata) +%% @param Iterations iteration count (positive integer) +%% @param KeyLen desired output length in bytes (positive integer) +%% @returns Returns the derived key as a binary of `KeyLen' bytes. +%% @doc Derive a key using PBKDF2-HMAC (RFC 8018 §5.2). +%% +%% Uses the AtomVM PSA backend with `PSA_ALG_PBKDF2_HMAC`. +%% Supported digest types are the same as for `hash/2`. +%% @end +%%----------------------------------------------------------------------------- +-spec pbkdf2_hmac( + DigestType :: hash_algorithm(), + Password :: iodata(), + Salt :: iodata(), + Iterations :: pos_integer(), + KeyLen :: pos_integer() +) -> binary(). +pbkdf2_hmac(_DigestType, _Password, _Salt, _Iterations, _KeyLen) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param N desired length of cryptographically secure random data %% @returns Returns Cryptographically secure random data of length `N' diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index 582b21f37c..a1856d0a1b 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -46,6 +46,11 @@ #include #endif +#ifdef MBEDTLS_PKCS5_C +#include +#include +#endif + // #define ENABLE_TRACE #include "term.h" #include "trace.h" @@ -981,6 +986,20 @@ static const AtomStringIntPair psa_hash_algorithm_table[] = { SELECT_INT_DEFAULT(PSA_ALG_NONE) }; +#ifdef MBEDTLS_PKCS5_C +static const AtomStringIntPair md_hash_algorithm_table[] = { + { ATOM_STR("\x3", "sha"), MBEDTLS_MD_SHA1 }, + { ATOM_STR("\x6", "sha224"), MBEDTLS_MD_SHA224 }, + { ATOM_STR("\x6", "sha256"), MBEDTLS_MD_SHA256 }, + { ATOM_STR("\x6", "sha384"), MBEDTLS_MD_SHA384 }, + { ATOM_STR("\x6", "sha512"), MBEDTLS_MD_SHA512 }, + { ATOM_STR("\x3", "md5"), MBEDTLS_MD_MD5 }, + { ATOM_STR("\x9", "ripemd160"), MBEDTLS_MD_RIPEMD160 }, + + SELECT_INT_DEFAULT(MBEDTLS_MD_NONE) +}; +#endif + #ifdef HAVE_MBEDTLS_ECDSA_RAW_TO_DER #define CRYPTO_SIGN_AVAILABLE 1 @@ -1501,6 +1520,109 @@ static term nif_crypto_mac(Context *ctx, int argc, term argv[]) return result; } +#ifdef MBEDTLS_PKCS5_C +static term nif_crypto_pbkdf2_hmac(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + + term digest_type_term = argv[0]; + // argv[1] is password, argv[2] is salt, argv[3] is iterations, argv[4] is key_len + + bool success = false; + term result = ERROR_ATOM; + void *maybe_allocated_password = NULL; + void *maybe_allocated_salt = NULL; + void *dk_out = NULL; + + mbedtls_md_type_t md_type + = interop_atom_term_select_int(md_hash_algorithm_table, digest_type_term, glb); + if (UNLIKELY(md_type == MBEDTLS_MD_NONE)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unknown digest type", ctx)); + } + + term password_term = argv[1]; + const void *password; + size_t password_len; + term iodata_handle_result + = handle_iodata(password_term, &password, &password_len, &maybe_allocated_password); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx)); + } + + term salt_term = argv[2]; + const void *salt; + size_t salt_len; + iodata_handle_result = handle_iodata(salt_term, &salt, &salt_len, &maybe_allocated_salt); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + result = make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx); + goto cleanup; + } + + term iterations_term = argv[3]; + if (UNLIKELY(!term_is_any_integer(iterations_term))) { + result + = make_crypto_error(__FILE__, __LINE__, "Iterations must be a positive integer", ctx); + goto cleanup; + } + avm_int64_t iterations = term_maybe_unbox_int64(iterations_term); + if (UNLIKELY(iterations <= 0)) { + result + = make_crypto_error(__FILE__, __LINE__, "Iterations must be a positive integer", ctx); + goto cleanup; + } + + term key_len_term = argv[4]; + if (UNLIKELY(!term_is_any_integer(key_len_term))) { + result = make_crypto_error(__FILE__, __LINE__, "KeyLen must be a positive integer", ctx); + goto cleanup; + } + avm_int64_t key_len = term_maybe_unbox_int64(key_len_term); + if (UNLIKELY(key_len <= 0)) { + result = make_crypto_error(__FILE__, __LINE__, "KeyLen must be a positive integer", ctx); + goto cleanup; + } + + dk_out = malloc((size_t) key_len); + if (IS_NULL_PTR(dk_out)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + int ret = mbedtls_pkcs5_pbkdf2_hmac_ext(md_type, (const unsigned char *) password, password_len, + (const unsigned char *) salt, salt_len, (unsigned int) iterations, (uint32_t) key_len, + (unsigned char *) dk_out); + if (UNLIKELY(ret != 0)) { + result = make_crypto_error(__FILE__, __LINE__, "Key derivation failed", ctx); + goto cleanup; + } + + if (UNLIKELY( + memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE((size_t) key_len)) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + success = true; + result = term_from_literal_binary(dk_out, (size_t) key_len, &ctx->heap, glb); + +cleanup: + if (dk_out) { + memset(dk_out, 0, (size_t) key_len); + } + free(dk_out); + free(maybe_allocated_password); + free(maybe_allocated_salt); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} +#endif + struct HashState { psa_algorithm_t psa_algo; @@ -2551,6 +2673,12 @@ static const struct Nif crypto_crypto_one_time_aead_nif = { .nif_ptr = nif_crypto_crypto_one_time_aead }; #endif +#ifdef MBEDTLS_PKCS5_C +static const struct Nif crypto_pbkdf2_hmac_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_pbkdf2_hmac +}; +#endif static const struct Nif crypto_strong_rand_bytes_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_strong_rand_bytes @@ -2637,6 +2765,12 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_crypto_one_time_aead_nif; } +#endif +#ifdef MBEDTLS_PKCS5_C + if (strcmp("pbkdf2_hmac/5", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_pbkdf2_hmac_nif; + } #endif if (strcmp("strong_rand_bytes/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index d49fc51fd9..1ffdc89d7b 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -664,6 +664,7 @@ compile_erlang(test_crypto_pk) compile_erlang(test_crypto_mac) compile_erlang(test_crypto_hash_update) compile_erlang(test_crypto_crypto) +compile_erlang(test_crypto_pbkdf2_hmac) compile_erlang(test_crypto_aead) set(erlang_test_beams @@ -1207,6 +1208,7 @@ set(erlang_test_beams test_crypto_mac.beam test_crypto_hash_update.beam test_crypto_crypto.beam + test_crypto_pbkdf2_hmac.beam test_crypto_aead.beam ) diff --git a/tests/erlang_tests/test_crypto_pbkdf2_hmac.erl b/tests/erlang_tests/test_crypto_pbkdf2_hmac.erl new file mode 100644 index 0000000000..6905c27466 --- /dev/null +++ b/tests/erlang_tests/test_crypto_pbkdf2_hmac.erl @@ -0,0 +1,191 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_crypto_pbkdf2_hmac). +-export([ + start/0, + get_bad/0 +]). + +start() -> + %% RFC 6070 PBKDF2-HMAC-SHA1 test vectors + ok = test_pbkdf2_hmac_sha1_rfc6070_1(), + ok = test_pbkdf2_hmac_sha1_rfc6070_2(), + ok = test_pbkdf2_hmac_sha1_rfc6070_3(), + ok = test_pbkdf2_hmac_sha1_rfc6070_4(), + %% SHA-256 test vectors + ok = test_pbkdf2_hmac_sha256_1(), + ok = test_pbkdf2_hmac_sha256_2(), + %% Result is a binary of the requested length + ok = test_result_is_binary(), + ok = test_keylen_truncation(), + %% Error cases + ok = test_bad_digest(), + ok = test_bad_pass(), + ok = test_bad_salt(), + ok = test_bad_iter(), + ok = test_bad_keylen(), + + 0. + +%% RFC 6070, test case 1: +%% P = "password", S = "salt", c = 1, dkLen = 20 +%% DK = 0c 60 c8 0f 96 1f 0e 71 f3 a9 b5 24 af 60 12 06 2f e0 37 a6 +test_pbkdf2_hmac_sha1_rfc6070_1() -> + DK = crypto:pbkdf2_hmac(sha, <<"password">>, <<"salt">>, 1, 20), + <<16#0c, 16#60, 16#c8, 16#0f, 16#96, 16#1f, 16#0e, 16#71, 16#f3, 16#a9, 16#b5, 16#24, 16#af, + 16#60, 16#12, 16#06, 16#2f, 16#e0, 16#37, 16#a6>> = DK, + ok. + +%% RFC 6070, test case 2: +%% P = "password", S = "salt", c = 2, dkLen = 20 +%% DK = ea 6c 01 4d c7 2d 6f 8c cd 1e d9 2a ce 1d 41 f0 d8 de 89 57 +test_pbkdf2_hmac_sha1_rfc6070_2() -> + DK = crypto:pbkdf2_hmac(sha, <<"password">>, <<"salt">>, 2, 20), + <<16#ea, 16#6c, 16#01, 16#4d, 16#c7, 16#2d, 16#6f, 16#8c, 16#cd, 16#1e, 16#d9, 16#2a, 16#ce, + 16#1d, 16#41, 16#f0, 16#d8, 16#de, 16#89, 16#57>> = DK, + ok. + +%% RFC 6070, test case 3: +%% P = "password", S = "salt", c = 4096, dkLen = 20 +%% DK = 4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1 +test_pbkdf2_hmac_sha1_rfc6070_3() -> + DK = crypto:pbkdf2_hmac(sha, <<"password">>, <<"salt">>, 4096, 20), + <<16#4b, 16#00, 16#79, 16#01, 16#b7, 16#65, 16#48, 16#9a, 16#be, 16#ad, 16#49, 16#d9, 16#26, + 16#f7, 16#21, 16#d0, 16#65, 16#a4, 16#29, 16#c1>> = DK, + ok. + +%% RFC 6070, test case 4: +%% P = "passwordPASSWORDpassword", S = "saltSALTsaltSALTsaltSALTsaltSALTsalt" +%% c = 4096, dkLen = 25 +%% DK = 3d 2e ec 4f e4 1c 84 9b 80 c8 d8 36 62 c0 e4 4a 8b 29 1a 96 4c f2 f0 70 38 +test_pbkdf2_hmac_sha1_rfc6070_4() -> + DK = crypto:pbkdf2_hmac( + sha, + <<"passwordPASSWORDpassword">>, + <<"saltSALTsaltSALTsaltSALTsaltSALTsalt">>, + 4096, + 25 + ), + <<16#3d, 16#2e, 16#ec, 16#4f, 16#e4, 16#1c, 16#84, 16#9b, 16#80, 16#c8, 16#d8, 16#36, 16#62, + 16#c0, 16#e4, 16#4a, 16#8b, 16#29, 16#1a, 16#96, 16#4c, 16#f2, 16#f0, 16#70, 16#38>> = DK, + ok. + +%% PBKDF2-HMAC-SHA256: +%% P = "passwd", S = "salt", c = 1, dkLen = 64 +%% (computed with OTP crypto on the BEAM) +%% DK = 55 ac 04 6e 56 e3 08 9f ec 16 91 c2 25 44 b6 05 +%% f9 41 85 21 6d de 04 65 e6 8b 9d 57 c2 0d ac bc +%% 49 ca 9c cc f1 79 b6 45 99 16 64 b3 9d 77 ef 31 +%% 7c 71 b8 45 b1 e3 0b d5 09 11 20 41 d3 a1 97 83 +test_pbkdf2_hmac_sha256_1() -> + DK = crypto:pbkdf2_hmac(sha256, <<"passwd">>, <<"salt">>, 1, 64), + <<16#55, 16#ac, 16#04, 16#6e, 16#56, 16#e3, 16#08, 16#9f, 16#ec, 16#16, 16#91, 16#c2, 16#25, + 16#44, 16#b6, 16#05, 16#f9, 16#41, 16#85, 16#21, 16#6d, 16#de, 16#04, 16#65, 16#e6, 16#8b, + 16#9d, 16#57, 16#c2, 16#0d, 16#ac, 16#bc, 16#49, 16#ca, 16#9c, 16#cc, 16#f1, 16#79, 16#b6, + 16#45, 16#99, 16#16, 16#64, 16#b3, 16#9d, 16#77, 16#ef, 16#31, 16#7c, 16#71, 16#b8, 16#45, + 16#b1, 16#e3, 16#0b, 16#d5, 16#09, 16#11, 16#20, 16#41, 16#d3, 16#a1, 16#97, 16#83>> = DK, + ok. + +%% PBKDF2-HMAC-SHA256, test vector from RFC 7914 section 11: +%% P = "Password", S = "NaCl", c = 80000, dkLen = 64 +%% DK = 4d dc d8 f6 0b 98 be 21 83 0c ee 5e f2 27 01 f9 +%% 64 1a 44 18 d0 4c 04 14 ae ff 08 87 6b 34 ab 56 +%% a1 d4 25 a1 22 58 33 54 9a db 84 1b 51 c9 b3 17 +%% 6a 27 2b de bb a1 d0 78 47 8f 62 b3 97 f3 3c 8d +test_pbkdf2_hmac_sha256_2() -> + DK = crypto:pbkdf2_hmac(sha256, <<"Password">>, <<"NaCl">>, 80000, 64), + <<16#4d, 16#dc, 16#d8, 16#f6, 16#0b, 16#98, 16#be, 16#21, 16#83, 16#0c, 16#ee, 16#5e, 16#f2, + 16#27, 16#01, 16#f9, 16#64, 16#1a, 16#44, 16#18, 16#d0, 16#4c, 16#04, 16#14, 16#ae, 16#ff, + 16#08, 16#87, 16#6b, 16#34, 16#ab, 16#56, 16#a1, 16#d4, 16#25, 16#a1, 16#22, 16#58, 16#33, + 16#54, 16#9a, 16#db, 16#84, 16#1b, 16#51, 16#c9, 16#b3, 16#17, 16#6a, 16#27, 16#2b, 16#de, + 16#bb, 16#a1, 16#d0, 16#78, 16#47, 16#8f, 16#62, 16#b3, 16#97, 16#f3, 16#3c, 16#8d>> = DK, + ok. + +%% Verify the result is a binary of exactly KeyLen bytes. +test_result_is_binary() -> + DK = crypto:pbkdf2_hmac(sha256, <<"pass">>, <<"salt">>, 1, 32), + true = is_binary(DK), + 32 = byte_size(DK), + ok. + +%% Verify KeyLen is respected: requesting fewer bytes gives a prefix of the +%% full derived key. +test_keylen_truncation() -> + DK16 = crypto:pbkdf2_hmac(sha256, <<"pass">>, <<"salt">>, 1, 16), + DK32 = crypto:pbkdf2_hmac(sha256, <<"pass">>, <<"salt">>, 1, 32), + 16 = byte_size(DK16), + 32 = byte_size(DK32), + %% The first 16 bytes of the 32-byte key must equal the 16-byte key. + <> = DK32, + DK16 = Prefix, + ok. + +%% Passing a bad (non-atom) digest argument must raise an error. +test_bad_digest() -> + try + crypto:pbkdf2_hmac(?MODULE:get_bad(), <<"password">>, <<"salt">>, 1, 20) + catch + error:_ -> + exp_err + end, + ok. + +%% Passing a non-binary password must raise an error. +test_bad_pass() -> + try + crypto:pbkdf2_hmac(sha256, ?MODULE:get_bad(), <<"salt">>, 1, 20) + catch + error:_ -> + exp_err + end, + ok. + +%% Passing a non-binary salt must raise an error. +test_bad_salt() -> + try + crypto:pbkdf2_hmac(sha256, <<"password">>, ?MODULE:get_bad(), 1, 20) + catch + error:_ -> + exp_err + end, + ok. + +%% Passing zero iterations must raise an error (Iter must be pos_integer()). +test_bad_iter() -> + try + crypto:pbkdf2_hmac(sha256, <<"password">>, <<"salt">>, 0, 20) + catch + error:_ -> + exp_err + end, + ok. + +%% Passing zero key length must raise an error (KeyLen must be pos_integer()). +test_bad_keylen() -> + try + crypto:pbkdf2_hmac(sha256, <<"password">>, <<"salt">>, 1, 0) + catch + error:_ -> + exp_err + end, + ok. + +get_bad() -> foo. diff --git a/tests/test.c b/tests/test.c index f167622fb3..b81e0262f5 100644 --- a/tests/test.c +++ b/tests/test.c @@ -631,6 +631,7 @@ struct Test tests[] = { TEST_CASE(test_crypto_hash_update), TEST_CASE(test_crypto_crypto), TEST_CASE(test_crypto_aead), + TEST_CASE(test_crypto_pbkdf2_hmac), // TEST CRASHES HERE: TEST_CASE(memlimit), From b468a2d8e0f43129636f6b955a3560db36ac080c Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 6 Mar 2026 23:41:33 +0100 Subject: [PATCH 08/13] add hash_equals Signed-off-by: Davide Bettio --- libs/estdlib/src/crypto.erl | 16 +++ src/libAtomVM/otp_crypto.c | 40 ++++++++ tests/erlang_tests/CMakeLists.txt | 2 + tests/erlang_tests/test_crypto_misc.erl | 124 ++++++++++++++++++++++++ tests/test.c | 1 + 5 files changed, 183 insertions(+) create mode 100644 tests/erlang_tests/test_crypto_misc.erl diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index 26cd5ff893..bb3a89de99 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -39,6 +39,7 @@ verify/5, mac/4, pbkdf2_hmac/5, + hash_equals/2, strong_rand_bytes/1, info_lib/0 ]). @@ -512,6 +513,21 @@ mac(_Type, _SubType, _Key, _Data) -> pbkdf2_hmac(_DigestType, _Password, _Salt, _Iterations, _KeyLen) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Mac1 first MAC or hash binary +%% @param Mac2 second MAC or hash binary +%% @returns Returns `true' if the two binaries are equal, `false' otherwise. +%% @doc Compare two MACs or hashes for equality in constant time. +%% +%% Both arguments must be binaries of the same length; otherwise +%% an error is raised. The comparison is performed in constant time +%% to prevent timing side-channel attacks. +%% @end +%%----------------------------------------------------------------------------- +-spec hash_equals(Mac1 :: binary(), Mac2 :: binary()) -> boolean(). +hash_equals(_Mac1, _Mac2) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param N desired length of cryptographically secure random data %% @returns Returns Cryptographically secure random data of length `N' diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index a1856d0a1b..c65e310a65 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -51,6 +51,8 @@ #include #endif +#include + // #define ENABLE_TRACE #include "term.h" #include "trace.h" @@ -1623,6 +1625,36 @@ static term nif_crypto_pbkdf2_hmac(Context *ctx, int argc, term argv[]) } #endif +static term nif_crypto_hash_equals(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term mac1_term = argv[0]; + term mac2_term = argv[1]; + + if (UNLIKELY(!term_is_binary(mac1_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Expected a binary", ctx)); + } + if (UNLIKELY(!term_is_binary(mac2_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Expected a binary", ctx)); + } + + size_t mac1_len = term_binary_size(mac1_term); + size_t mac2_len = term_binary_size(mac2_term); + + if (UNLIKELY(mac1_len != mac2_len)) { + RAISE_ERROR( + make_crypto_error(__FILE__, __LINE__, "Binaries must have the same length", ctx)); + } + + const void *mac1 = term_binary_data(mac1_term); + const void *mac2 = term_binary_data(mac2_term); + + int cmp = mbedtls_ct_memcmp(mac1, mac2, mac1_len); + + return cmp == 0 ? TRUE_ATOM : FALSE_ATOM; +} + struct HashState { psa_algorithm_t psa_algo; @@ -2679,6 +2711,10 @@ static const struct Nif crypto_pbkdf2_hmac_nif = { .nif_ptr = nif_crypto_pbkdf2_hmac }; #endif +static const struct Nif crypto_hash_equals_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_hash_equals +}; static const struct Nif crypto_strong_rand_bytes_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_strong_rand_bytes @@ -2772,6 +2808,10 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) return &crypto_pbkdf2_hmac_nif; } #endif + if (strcmp("hash_equals/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_hash_equals_nif; + } if (strcmp("strong_rand_bytes/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_strong_rand_bytes_nif; diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 1ffdc89d7b..a287f51b1f 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -665,6 +665,7 @@ compile_erlang(test_crypto_mac) compile_erlang(test_crypto_hash_update) compile_erlang(test_crypto_crypto) compile_erlang(test_crypto_pbkdf2_hmac) +compile_erlang(test_crypto_misc) compile_erlang(test_crypto_aead) set(erlang_test_beams @@ -1209,6 +1210,7 @@ set(erlang_test_beams test_crypto_hash_update.beam test_crypto_crypto.beam test_crypto_pbkdf2_hmac.beam + test_crypto_misc.beam test_crypto_aead.beam ) diff --git a/tests/erlang_tests/test_crypto_misc.erl b/tests/erlang_tests/test_crypto_misc.erl new file mode 100644 index 0000000000..fcfe7ea5e3 --- /dev/null +++ b/tests/erlang_tests/test_crypto_misc.erl @@ -0,0 +1,124 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_crypto_misc). +-export([start/0, get_bad/0]). + +start() -> + %% crypto:hash_equals/2 + ok = test_hash_equals_equal(), + ok = test_hash_equals_not_equal(), + ok = test_hash_equals_empty(), + ok = test_hash_equals_single_byte_equal(), + ok = test_hash_equals_single_byte_not_equal(), + ok = test_hash_equals_differs_at_start(), + ok = test_hash_equals_differs_at_end(), + ok = test_hash_equals_returns_bool(), + %% Error cases + ok = test_hash_equals_bad_first_arg(), + ok = test_hash_equals_bad_second_arg(), + ok = test_hash_equals_different_lengths(), + + 0. + +%% Two identical binaries must be equal. +test_hash_equals_equal() -> + true = crypto:hash_equals( + <<16#0c, 16#60, 16#c8, 16#0f, 16#96, 16#1f, 16#0e, 16#71, 16#f3, 16#a9, 16#b5, 16#24, 16#af, + 16#60, 16#12, 16#06, 16#2f, 16#e0, 16#37, 16#a6>>, + <<16#0c, 16#60, 16#c8, 16#0f, 16#96, 16#1f, 16#0e, 16#71, 16#f3, 16#a9, 16#b5, 16#24, 16#af, + 16#60, 16#12, 16#06, 16#2f, 16#e0, 16#37, 16#a6>> + ), + ok. + +%% Two different binaries of the same length must not be equal. +test_hash_equals_not_equal() -> + false = crypto:hash_equals( + <<16#0c, 16#60, 16#c8, 16#0f, 16#96, 16#1f, 16#0e, 16#71, 16#f3, 16#a9, 16#b5, 16#24, 16#af, + 16#60, 16#12, 16#06, 16#2f, 16#e0, 16#37, 16#a6>>, + <<16#ea, 16#6c, 16#01, 16#4d, 16#c7, 16#2d, 16#6f, 16#8c, 16#cd, 16#1e, 16#d9, 16#2a, 16#ce, + 16#1d, 16#41, 16#f0, 16#d8, 16#de, 16#89, 16#57>> + ), + ok. + +%% Two empty binaries must be equal. +test_hash_equals_empty() -> + true = crypto:hash_equals(<<>>, <<>>), + ok. + +%% Single-byte equal. +test_hash_equals_single_byte_equal() -> + true = crypto:hash_equals(<<16#ab>>, <<16#ab>>), + ok. + +%% Single-byte not equal. +test_hash_equals_single_byte_not_equal() -> + false = crypto:hash_equals(<<16#ab>>, <<16#cd>>), + ok. + +%% Difference only in the first byte. +test_hash_equals_differs_at_start() -> + false = crypto:hash_equals(<<16#ff, 16#00, 16#00>>, <<16#00, 16#00, 16#00>>), + ok. + +%% Difference only in the last byte. +test_hash_equals_differs_at_end() -> + false = crypto:hash_equals(<<16#00, 16#00, 16#ff>>, <<16#00, 16#00, 16#00>>), + ok. + +%% Result must be a boolean atom, not an integer. +test_hash_equals_returns_bool() -> + R1 = crypto:hash_equals(<<"abc">>, <<"abc">>), + R2 = crypto:hash_equals(<<"abc">>, <<"xyz">>), + true = is_boolean(R1), + true = is_boolean(R2), + ok. + +%% Non-binary first argument must raise an error. +test_hash_equals_bad_first_arg() -> + try + crypto:hash_equals(?MODULE:get_bad(), <<"hello">>) + catch + error:_ -> + exp_err + end, + ok. + +%% Non-binary second argument must raise an error. +test_hash_equals_bad_second_arg() -> + try + crypto:hash_equals(<<"hello">>, ?MODULE:get_bad()) + catch + error:_ -> + exp_err + end, + ok. + +%% Binaries of different lengths must raise an error. +test_hash_equals_different_lengths() -> + try + crypto:hash_equals(<<"hello">>, <<"hi">>) + catch + error:_ -> + exp_err + end, + ok. + +get_bad() -> foo. diff --git a/tests/test.c b/tests/test.c index b81e0262f5..27e46581a8 100644 --- a/tests/test.c +++ b/tests/test.c @@ -632,6 +632,7 @@ struct Test tests[] = { TEST_CASE(test_crypto_crypto), TEST_CASE(test_crypto_aead), TEST_CASE(test_crypto_pbkdf2_hmac), + TEST_CASE(test_crypto_misc), // TEST CRASHES HERE: TEST_CASE(memlimit), From 6baa9314e8e17c691497d928309b41765576d52b Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 7 Mar 2026 00:35:37 +0100 Subject: [PATCH 09/13] mac_update Signed-off-by: Davide Bettio --- tests/erlang_tests/test_crypto_mac.erl | 224 ++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 1 deletion(-) diff --git a/tests/erlang_tests/test_crypto_mac.erl b/tests/erlang_tests/test_crypto_mac.erl index a68b8860f2..8531128224 100644 --- a/tests/erlang_tests/test_crypto_mac.erl +++ b/tests/erlang_tests/test_crypto_mac.erl @@ -19,13 +19,32 @@ % -module(test_crypto_mac). --export([start/0, test_hmac/0, test_cmac/0, test_hmac_iolist/0, test_cmac_iolist/0]). +-export([ + start/0, + test_hmac/0, + test_cmac/0, + test_hmac_iolist/0, + test_cmac_iolist/0, + test_hmac_update/0, + test_hmac_update_iolist/0, + test_cmac_update/0, + test_cmac_update_iolist/0, + test_mac_finalN/0, + test_mac_update_badarg/0, + get_bad/0 +]). start() -> ok = mbedtls_conditional_run(test_hmac, 16#03000000), ok = mbedtls_conditional_run(test_cmac, 16#03000000), ok = mbedtls_conditional_run(test_hmac_iolist, 16#03000000), ok = mbedtls_conditional_run(test_cmac_iolist, 16#03000000), + ok = mbedtls_conditional_run(test_hmac_update, 16#03000000), + ok = mbedtls_conditional_run(test_hmac_update_iolist, 16#03000000), + ok = mbedtls_conditional_run(test_cmac_update, 16#03000000), + ok = mbedtls_conditional_run(test_cmac_update_iolist, 16#03000000), + ok = mbedtls_conditional_run(test_mac_finalN, 16#03000000), + ok = mbedtls_conditional_run(test_mac_update_badarg, 16#03000000), 0. mbedtls_conditional_run(F, RVer) -> @@ -159,6 +178,209 @@ test_cmac_iolist() -> ok. +test_hmac_update() -> + %% Single update: equivalent to mac/4 with full data + MacInitState0 = crypto:mac_init(hmac, sha256, <<"Hello">>), + MacUpdatedState0 = crypto:mac_update(MacInitState0, <<"Data">>), + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, 176>> = crypto:mac_final(MacUpdatedState0), + + %% Two updates: "Da" + "ta" must give the same result as "Data" + MacInitState1 = crypto:mac_init(hmac, sha256, <<"Hello">>), + MacUpdatedState1 = crypto:mac_update(MacInitState1, <<"Da">>), + MacUpdatedState2 = crypto:mac_update(MacUpdatedState1, <<"ta">>), + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, 176>> = crypto:mac_final(MacUpdatedState2), + + %% Three updates: "He" + "llo" + " World" + MacInitState2 = crypto:mac_init(hmac, sha256, <<"Hello">>), + MacUpdatedState3 = crypto:mac_update(MacInitState2, <<"He">>), + MacUpdatedState4 = crypto:mac_update(MacUpdatedState3, <<"llo">>), + MacUpdatedState5 = crypto:mac_update(MacUpdatedState4, <<" World">>), + <<99, 67, 211, 45, 120, 45, 165, 143, 115, 74, 108, 211, 235, 13, 148, 227, 194, 92, 12, 108, + 123, 27, 186, 81, 200, 116, 209, 8, 129, 63, 62, 109>> = crypto:mac_final(MacUpdatedState5), + + %% Empty update: equivalent to mac/4 with empty data + MacInitState3 = crypto:mac_init(hmac, sha256, <<"Hello">>), + MacUpdatedState6 = crypto:mac_update(MacInitState3, <<"">>), + <<153, 146, 251, 20, 217, 139, 50, 190, 240, 28, 191, 144, 120, 206, 138, 44, 47, 139, 14, 233, + 146, 3, 76, 170, 214, 207, 208, 7, 109, 0, 155, 23>> = crypto:mac_final(MacUpdatedState6), + + %% mac_final can be called on init state directly (no update) + MacInitState4 = crypto:mac_init(hmac, sha256, <<"Hello">>), + <<153, 146, 251, 20, 217, 139, 50, 190, 240, 28, 191, 144, 120, 206, 138, 44, 47, 139, 14, 233, + 146, 3, 76, 170, 214, 207, 208, 7, 109, 0, 155, 23>> = crypto:mac_final(MacInitState4), + + ok. + +test_hmac_update_iolist() -> + %% iolist key in mac_init, binary data in mac_update + MacInitState0 = crypto:mac_init(hmac, sha256, [$H, <<"ell">>, <<"o">>]), + MacUpdatedState0 = crypto:mac_update(MacInitState0, <<"Data">>), + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, 176>> = crypto:mac_final(MacUpdatedState0), + + %% binary key in mac_init, iolist data in mac_update + MacInitState1 = crypto:mac_init(hmac, sha256, <<"Hello">>), + MacUpdatedState1 = crypto:mac_update(MacInitState1, [$D, <<"a">>, <<"ta">>]), + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27, 68, 220, 32, 133, + 108, 193, 194, 77, 15, 26, 51, 8, 197, 95, 122, 176>> = crypto:mac_final(MacUpdatedState1), + + %% iolist data split across two updates + MacInitState2 = crypto:mac_init(hmac, sha256, <<"Hello">>), + MacUpdatedState2 = crypto:mac_update(MacInitState2, <<"He">>), + MacUpdatedState3 = crypto:mac_update(MacUpdatedState2, [<<"llo">>, " World"]), + <<99, 67, 211, 45, 120, 45, 165, 143, 115, 74, 108, 211, 235, 13, 148, 227, 194, 92, 12, 108, + 123, 27, 186, 81, 200, 116, 209, 8, 129, 63, 62, 109>> = crypto:mac_final(MacUpdatedState3), + + ok. + +test_cmac_update() -> + Key16 = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + + %% Single update: equivalent to mac/4 with full data + MacInitState0 = crypto:mac_init(cmac, aes_128_cbc, Key16), + MacUpdatedState0 = crypto:mac_update(MacInitState0, <<"Data">>), + <<227, 175, 5, 166, 7, 167, 180, 81, 30, 6, 147, 30, 211, 6, 207, 186>> = crypto:mac_final( + MacUpdatedState0 + ), + + %% Two updates: "Da" + "ta" + MacInitState1 = crypto:mac_init(cmac, aes_128_cbc, Key16), + MacUpdatedState1 = crypto:mac_update(MacInitState1, <<"Da">>), + MacUpdatedState2 = crypto:mac_update(MacUpdatedState1, <<"ta">>), + <<227, 175, 5, 166, 7, 167, 180, 81, 30, 6, 147, 30, 211, 6, 207, 186>> = crypto:mac_final( + MacUpdatedState2 + ), + + %% Three updates: "More" + " " + "Data" + MacInitState2 = crypto:mac_init(cmac, aes_128_cbc, Key16), + MacUpdatedState3 = crypto:mac_update(MacInitState2, <<"More">>), + MacUpdatedState4 = crypto:mac_update(MacUpdatedState3, <<" ">>), + MacUpdatedState5 = crypto:mac_update(MacUpdatedState4, <<"Data">>), + <<19, 247, 81, 225, 123, 105, 6, 29, 226, 176, 251, 80, 224, 17, 174, 122>> = crypto:mac_final( + MacUpdatedState5 + ), + + %% Empty update + MacInitState3 = crypto:mac_init(cmac, aes_128_cbc, Key16), + MacUpdatedState6 = crypto:mac_update(MacInitState3, <<"">>), + <<151, 221, 110, 90, 136, 44, 189, 86, 76, 57, 174, 125, 28, 90, 49, 170>> = crypto:mac_final( + MacUpdatedState6 + ), + + ok. + +test_cmac_update_iolist() -> + %% iolist key in mac_init, binary data in mac_update + MacInitState0 = crypto:mac_init( + cmac, aes_128_cbc, [<<0, 1, 2, 3>>, <<4, 5, 6, 7>>, <<8, 9, 10, 11, 12, 13, 14, 15>>] + ), + MacUpdatedState0 = crypto:mac_update(MacInitState0, <<"Data">>), + <<227, 175, 5, 166, 7, 167, 180, 81, 30, 6, 147, 30, 211, 6, 207, 186>> = crypto:mac_final( + MacUpdatedState0 + ), + + %% binary key in mac_init, iolist data across two updates + Key16 = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + MacInitState1 = crypto:mac_init(cmac, aes_128_cbc, Key16), + MacUpdatedState1 = crypto:mac_update(MacInitState1, [$D, <<"ata">>]), + <<227, 175, 5, 166, 7, 167, 180, 81, 30, 6, 147, 30, 211, 6, 207, 186>> = crypto:mac_final( + MacUpdatedState1 + ), + + ok. + +test_mac_finalN() -> + %% hmac sha256 truncated to 16 bytes + MacInitState0 = crypto:mac_init(hmac, sha256, <<"Hello">>), + MacUpdatedState0 = crypto:mac_update(MacInitState0, <<"Data">>), + Mac16 = crypto:mac_finalN(MacUpdatedState0, 16), + 16 = byte_size(Mac16), + <<211, 117, 58, 171, 240, 87, 74, 125, 159, 217, 148, 133, 209, 234, 203, 27>> = Mac16, + + %% cmac aes_128_cbc truncated to 8 bytes + Key16 = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>, + MacInitState1 = crypto:mac_init(cmac, aes_128_cbc, Key16), + MacUpdatedState1 = crypto:mac_update(MacInitState1, <<"Data">>), + Mac8 = crypto:mac_finalN(MacUpdatedState1, 8), + 8 = byte_size(Mac8), + <<227, 175, 5, 166, 7, 167, 180, 81>> = Mac8, + + ok. + +test_mac_update_badarg() -> + %% mac_init with unknown mac type + exp_err = + try + crypto:mac_init(?MODULE:get_bad(), sha256, <<"key">>) + catch + error:{badarg, {File1, Line1}, "Unknown mac algorithm"} when + is_list(File1) and is_integer(Line1) + -> + exp_err + end, + + %% mac_init with bad digest algorithm for hmac + exp_err = + try + crypto:mac_init(hmac, ?MODULE:get_bad(), <<"key">>) + catch + error:{badarg, {File2, Line2}, "Bad digest algorithm for HMAC"} when + is_list(File2) and is_integer(Line2) + -> + exp_err + end, + + %% mac_update with bad state (not a mac_state) + exp_err = + try + crypto:mac_update(?MODULE:get_bad(), <<"Data">>) + catch + error:{badarg, {File3, Line3}, "Bad ref"} when + is_list(File3) and is_integer(Line3) + -> + exp_err + end, + + %% mac_update with bad data (not iodata) + MacInitState = crypto:mac_init(hmac, sha256, <<"Hello">>), + exp_err = + try + crypto:mac_update(MacInitState, ?MODULE:get_bad()) + catch + error:{badarg, {File4, Line4}, "Bad text"} when + is_list(File4) and is_integer(Line4) + -> + exp_err + end, + + %% mac_final with bad state + exp_err = + try + crypto:mac_final(?MODULE:get_bad()) + catch + error:{badarg, {File5, Line5}, "Bad ref"} when + is_list(File5) and is_integer(Line5) + -> + exp_err + end, + + %% mac_finalN with bad state + exp_err = + try + crypto:mac_finalN(?MODULE:get_bad(), 16) + catch + error:{badarg, {File6, Line6}, "Bad ref"} when + is_list(File6) and is_integer(Line6) + -> + exp_err + end, + + ok. + +get_bad() -> foo. + expect_error(Fun) -> try Fun() of Res -> {unexpected, Res} From 8ffd236496aa98180b40014d059602ca07bf9665 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 7 Mar 2026 13:58:28 +0100 Subject: [PATCH 10/13] mac update Signed-off-by: Davide Bettio --- libs/estdlib/src/crypto.erl | 50 ++++++ src/libAtomVM/globalcontext.c | 13 ++ src/libAtomVM/globalcontext.h | 1 + src/libAtomVM/otp_crypto.c | 305 ++++++++++++++++++++++++++++++++++ src/libAtomVM/otp_crypto.h | 1 + 5 files changed, 370 insertions(+) diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index bb3a89de99..d7601035d2 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -38,6 +38,10 @@ sign/4, verify/5, mac/4, + mac_init/3, + mac_update/2, + mac_final/1, + mac_finalN/2, pbkdf2_hmac/5, hash_equals/2, strong_rand_bytes/1, @@ -132,6 +136,8 @@ -type mac_subtype() :: cmac_subtype() | hash_algorithm() | ripemd160. +-opaque mac_state() :: reference(). + %%----------------------------------------------------------------------------- %% @param Type the hash algorithm %% @param Data the data to hash @@ -490,6 +496,50 @@ verify(_Algorithm, _DigestType, _Data, _Signature, _Key) -> mac(_Type, _SubType, _Key, _Data) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Type MAC algorithm family (`cmac` or `hmac`) +%% @param SubType MAC subtype (cipher for CMAC, digest for HMAC) +%% @param Key MAC key bytes (iodata) +%% @returns Returns an opaque MAC state for use with mac_update/2 and mac_final/1. +%% @doc Initialize a streaming MAC operation. +%% @end +%%----------------------------------------------------------------------------- +-spec mac_init(Type :: mac_type(), SubType :: mac_subtype(), Key :: iodata()) -> mac_state(). +mac_init(_Type, _SubType, _Key) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param State MAC state from mac_init/3 or a previous mac_update/2 +%% @param Data data to add (iodata) +%% @returns Returns an updated MAC state. +%% @doc Add data to a streaming MAC calculation. +%% @end +%%----------------------------------------------------------------------------- +-spec mac_update(State :: mac_state(), Data :: iodata()) -> mac_state(). +mac_update(_State, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param State MAC state from mac_init/3 or mac_update/2 +%% @returns Returns the computed MAC as a binary. +%% @doc Finalize a streaming MAC operation and return the MAC value. +%% @end +%%----------------------------------------------------------------------------- +-spec mac_final(State :: mac_state()) -> binary(). +mac_final(_State) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param State MAC state from mac_init/3 or mac_update/2 +%% @param MacLength desired output length in bytes +%% @returns Returns the computed MAC truncated to MacLength bytes. +%% @doc Finalize a streaming MAC operation with a custom output length. +%% @end +%%----------------------------------------------------------------------------- +-spec mac_finalN(State :: mac_state(), MacLength :: pos_integer()) -> binary(). +mac_finalN(_State, _MacLength) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param DigestType hash algorithm (`sha`, `sha256`, etc.) %% @param Password password bytes (iodata) diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index 5f8e1f272f..20f28538c6 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -181,6 +181,19 @@ GlobalContext *globalcontext_new(void) #if HAVE_OPEN && HAVE_CLOSE resource_type_destroy(glb->posix_fd_resource_type); #endif +#ifndef AVM_NO_SMP + smp_rwlock_destroy(glb->modules_lock); +#endif + free(glb->modules_table); + atom_table_destroy(glb->atom_table); + free(glb); + return NULL; + } + glb->psa_mac_op_resource_type = enif_init_resource_type(&env, "psa_mac_op", &psa_mac_op_resource_type_init, ERL_NIF_RT_CREATE, NULL); + if (IS_NULL_PTR(glb->psa_mac_op_resource_type)) { +#if HAVE_OPEN && HAVE_CLOSE + resource_type_destroy(glb->posix_fd_resource_type); +#endif #ifndef AVM_NO_SMP smp_rwlock_destroy(glb->modules_lock); #endif diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index 216d450593..cb6867a082 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -186,6 +186,7 @@ struct GlobalContext #ifdef MBEDTLS_PSA_CRYPTO_C ErlNifResourceType *psa_hash_op_resource_type; ErlNifResourceType *psa_cipher_op_resource_type; + ErlNifResourceType *psa_mac_op_resource_type; #endif void *platform_data; diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index c65e310a65..cabbd2dabe 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -1674,6 +1674,279 @@ const ErlNifResourceTypeInit psa_hash_op_resource_type_init = { .dtor = psa_hash_op_dtor }; +struct MacState +{ + psa_mac_operation_t psa_op; + psa_key_id_t key_id; + psa_algorithm_t psa_algo; + psa_key_type_t psa_key_type; + size_t key_bit_size; +}; + +static void psa_mac_op_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct MacState *mac_state = (struct MacState *) obj; + psa_mac_abort(&mac_state->psa_op); + psa_destroy_key(mac_state->key_id); +} + +const ErlNifResourceTypeInit psa_mac_op_resource_type_init = { + .members = 1, + .dtor = psa_mac_op_dtor +}; + +static term nif_crypto_mac_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + term mac_type_term = argv[0]; + term sub_type_term = argv[1]; + // argv[2] is key + + bool success = false; + term result = ERROR_ATOM; + void *maybe_allocated_key = NULL; + struct MacState *mac_obj = NULL; + + term key_term = argv[2]; + const void *key; + size_t key_len; + term iodata_handle_result = handle_iodata(key_term, &key, &key_len, &maybe_allocated_key); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx)); + } + + psa_key_type_t psa_key_type = interop_atom_term_select_int(mac_key_table, mac_type_term, glb); + psa_algorithm_t psa_key_algo; + psa_key_bits_t key_bit_size; + switch (psa_key_type) { + case PSA_KEY_TYPE_AES: + psa_key_algo = PSA_ALG_CMAC; + key_bit_size + = interop_atom_term_select_int(cmac_algorithm_bits_table, sub_type_term, glb); + if (UNLIKELY(key_bit_size == 0)) { + result = make_crypto_error(__FILE__, __LINE__, "Unknown cipher", ctx); + goto cleanup; + } + if (UNLIKELY(key_bit_size != key_len * 8)) { + result = make_crypto_error(__FILE__, __LINE__, "Bad key size", ctx); + goto cleanup; + } + break; + case PSA_KEY_TYPE_HMAC: { + psa_algorithm_t sub_type_algo + = interop_atom_term_select_int(psa_hash_algorithm_table, sub_type_term, glb); + if (UNLIKELY(sub_type_algo == PSA_ALG_NONE)) { + result = make_crypto_error( + __FILE__, __LINE__, "Bad digest algorithm for HMAC", ctx); + goto cleanup; + } + psa_key_algo = PSA_ALG_HMAC(sub_type_algo); + key_bit_size = key_len * 8; + } break; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unknown mac algorithm", ctx); + goto cleanup; + } + + psa_key_id_t key_id = 0; + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, psa_key_type); + psa_set_key_bits(&attr, key_bit_size); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE); + psa_set_key_algorithm(&attr, psa_key_algo); + + psa_status_t status = psa_import_key(&attr, key, key_len, &key_id); + psa_reset_key_attributes(&attr); + switch (status) { + case PSA_SUCCESS: + break; + case PSA_ERROR_NOT_SUPPORTED: + result = make_crypto_error(__FILE__, __LINE__, "Unsupported algorithm", ctx); + goto cleanup; + default: + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + mac_obj = enif_alloc_resource(glb->psa_mac_op_resource_type, sizeof(struct MacState)); + if (IS_NULL_PTR(mac_obj)) { + psa_destroy_key(key_id); + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + memset(mac_obj, 0, sizeof(struct MacState)); + mac_obj->key_id = key_id; + mac_obj->psa_algo = psa_key_algo; + mac_obj->psa_key_type = psa_key_type; + mac_obj->key_bit_size = key_bit_size; + + status = psa_mac_sign_setup(&mac_obj->psa_op, key_id, psa_key_algo); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + success = true; + result = enif_make_resource(erl_nif_env_from_context(ctx), mac_obj); + +cleanup: + if (mac_obj) { + enif_release_resource(mac_obj); + } + free(maybe_allocated_key); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + +static term nif_crypto_mac_update(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + void *psa_mac_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + glb->psa_mac_op_resource_type, &psa_mac_obj_ptr))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad ref", ctx)); + } + struct MacState *mac_state = (struct MacState *) psa_mac_obj_ptr; + + void *maybe_allocated_data = NULL; + size_t data_len; + term data_term = argv[1]; + const void *data; + term iodata_handle_result = handle_iodata(data_term, &data, &data_len, &maybe_allocated_data); + if (UNLIKELY(iodata_handle_result != OK_ATOM)) { + free(maybe_allocated_data); + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad text", ctx)); + } + + psa_status_t status = psa_mac_update(&mac_state->psa_op, data, data_len); + free(maybe_allocated_data); + if (UNLIKELY(status != PSA_SUCCESS)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx)); + } + + return argv[0]; +} + +static term nif_crypto_mac_final(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + void *psa_mac_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + glb->psa_mac_op_resource_type, &psa_mac_obj_ptr))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad ref", ctx)); + } + struct MacState *mac_state = (struct MacState *) psa_mac_obj_ptr; + + bool success = false; + term result = ERROR_ATOM; + + size_t mac_size = PSA_MAC_LENGTH(mac_state->psa_key_type, mac_state->key_bit_size, mac_state->psa_algo); + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(mac_size)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term mac_bin = term_create_uninitialized_binary(mac_size, &ctx->heap, glb); + void *mac_buf = (void *) term_binary_data(mac_bin); + + size_t mac_len = 0; + psa_status_t status = psa_mac_sign_finish(&mac_state->psa_op, mac_buf, mac_size, &mac_len); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + success = true; + result = mac_bin; + +cleanup: + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + +static term nif_crypto_mac_finalN(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + do_psa_init(); + + GlobalContext *glb = ctx->global; + + void *psa_mac_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + glb->psa_mac_op_resource_type, &psa_mac_obj_ptr))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad ref", ctx)); + } + struct MacState *mac_state = (struct MacState *) psa_mac_obj_ptr; + + avm_int_t requested_len; + if (UNLIKELY(!term_is_integer(argv[1]))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad length", ctx)); + } + requested_len = term_to_int(argv[1]); + if (UNLIKELY(requested_len <= 0)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad length", ctx)); + } + + bool success = false; + term result = ERROR_ATOM; + + size_t mac_size = PSA_MAC_LENGTH(mac_state->psa_key_type, mac_state->key_bit_size, mac_state->psa_algo); + uint8_t *mac_buf = malloc(mac_size); + if (IS_NULL_PTR(mac_buf)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + size_t mac_len = 0; + psa_status_t status = psa_mac_sign_finish(&mac_state->psa_op, mac_buf, mac_size, &mac_len); + if (UNLIKELY(status != PSA_SUCCESS)) { + result = make_crypto_error(__FILE__, __LINE__, "Unexpected error", ctx); + goto cleanup; + } + + size_t out_len = (size_t) requested_len < mac_len ? (size_t) requested_len : mac_len; + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(out_len)) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto cleanup; + } + + success = true; + result = term_from_literal_binary(mac_buf, out_len, &ctx->heap, glb); + +cleanup: + memset(mac_buf, 0, mac_size); + free(mac_buf); + + if (UNLIKELY(!success)) { + RAISE_ERROR(result); + } + + return result; +} + static term nif_crypto_hash_init(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -2676,6 +2949,22 @@ static const struct Nif crypto_mac_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_mac }; +static const struct Nif crypto_mac_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_mac_init +}; +static const struct Nif crypto_mac_update_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_mac_update +}; +static const struct Nif crypto_mac_final_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_mac_final +}; +static const struct Nif crypto_mac_finalN_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_mac_finalN +}; static const struct Nif crypto_hash_init_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_hash_init @@ -2769,6 +3058,22 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_mac_nif; } + if (strcmp("mac_init/3", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_mac_init_nif; + } + if (strcmp("mac_update/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_mac_update_nif; + } + if (strcmp("mac_final/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_mac_final_nif; + } + if (strcmp("mac_finalN/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_mac_finalN_nif; + } if (strcmp("hash_init/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_hash_init_nif; diff --git a/src/libAtomVM/otp_crypto.h b/src/libAtomVM/otp_crypto.h index f2a255824c..514a6fc39b 100644 --- a/src/libAtomVM/otp_crypto.h +++ b/src/libAtomVM/otp_crypto.h @@ -32,6 +32,7 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname); extern const ErlNifResourceTypeInit psa_hash_op_resource_type_init; extern const ErlNifResourceTypeInit psa_cipher_op_resource_type_init; +extern const ErlNifResourceTypeInit psa_mac_op_resource_type_init; #ifdef __cplusplus } From eb874f691227fd76aa7d3c58302708ee7ec8a3a0 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 7 Mar 2026 14:05:03 +0100 Subject: [PATCH 11/13] fixup! add hash_equals --- src/libAtomVM/otp_crypto.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index cabbd2dabe..eacbd64a15 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -51,7 +51,12 @@ #include #endif +// mbedtls_ct_memcmp is available in 2.28.x+ and 3.1.x+ (absent in 3.0.x) +#if (MBEDTLS_VERSION_NUMBER >= 0x021C0000 && MBEDTLS_VERSION_NUMBER < 0x03000000) \ + || MBEDTLS_VERSION_NUMBER >= 0x03010000 #include +#define AVM_HAVE_MBEDTLS_CT_MEMCMP 1 +#endif // #define ENABLE_TRACE #include "term.h" @@ -1650,7 +1655,18 @@ static term nif_crypto_hash_equals(Context *ctx, int argc, term argv[]) const void *mac1 = term_binary_data(mac1_term); const void *mac2 = term_binary_data(mac2_term); +#ifdef AVM_HAVE_MBEDTLS_CT_MEMCMP int cmp = mbedtls_ct_memcmp(mac1, mac2, mac1_len); +#else + // Constant-time fallback for older mbedTLS (< 2.28.0 or 3.0.x) + const unsigned char *pa = (const unsigned char *) mac1; + const unsigned char *pb = (const unsigned char *) mac2; + unsigned char diff = 0; + for (size_t i = 0; i < mac1_len; i++) { + diff |= (unsigned char) (pa[i] ^ pb[i]); + } + int cmp = (int) diff; +#endif return cmp == 0 ? TRUE_ATOM : FALSE_ATOM; } From 7c72497cc53a031a6e01a2e19f3cef5426abfcb2 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 7 Mar 2026 14:10:42 +0100 Subject: [PATCH 12/13] fixup! pbkdf2_hmac tests --- src/libAtomVM/otp_crypto.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index eacbd64a15..0cb3995281 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -1597,9 +1597,28 @@ static term nif_crypto_pbkdf2_hmac(Context *ctx, int argc, term argv[]) goto cleanup; } +#if MBEDTLS_VERSION_NUMBER >= 0x03030000 + // mbedtls_pkcs5_pbkdf2_hmac_ext is available since 3.3.0 int ret = mbedtls_pkcs5_pbkdf2_hmac_ext(md_type, (const unsigned char *) password, password_len, (const unsigned char *) salt, salt_len, (unsigned int) iterations, (uint32_t) key_len, (unsigned char *) dk_out); +#else + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(md_type); + int ret; + if (UNLIKELY(md_info == NULL)) { + ret = MBEDTLS_ERR_PKCS5_BAD_INPUT_DATA; + } else { + mbedtls_md_context_t md_ctx; + mbedtls_md_init(&md_ctx); + ret = mbedtls_md_setup(&md_ctx, md_info, 1); + if (ret == 0) { + ret = mbedtls_pkcs5_pbkdf2_hmac(&md_ctx, (const unsigned char *) password, password_len, + (const unsigned char *) salt, salt_len, (unsigned int) iterations, + (uint32_t) key_len, (unsigned char *) dk_out); + } + mbedtls_md_free(&md_ctx); + } +#endif if (UNLIKELY(ret != 0)) { result = make_crypto_error(__FILE__, __LINE__, "Key derivation failed", ctx); goto cleanup; From 594250161ff30da0275ee8ee766847aa86068613 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 7 Mar 2026 21:27:20 +0100 Subject: [PATCH 13/13] libsodium Signed-off-by: Davide Bettio --- CMakeLists.txt | 1 + libs/estdlib/src/crypto.erl | 61 +++-- src/libAtomVM/CMakeLists.txt | 16 ++ src/libAtomVM/otp_crypto.c | 380 +++++++++++++++++++++++++- tests/erlang_tests/test_crypto_pk.erl | 82 +++++- 5 files changed, 503 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b950dc3f5b..bdb786df5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) option(AVM_BUILD_RUNTIME_ONLY "Only build the AtomVM runtime" OFF) option(COVERAGE "Build for code coverage" OFF) option(AVM_PRINT_PROCESS_CRASH_DUMPS "Print crash reports when processes die with non-standard reasons" ON) +option(AVM_USE_LIBSODIUM "Enable optional libsodium backend for unsupported PSA/mbedTLS algorithms" OFF) # JIT & execution of precompiled code if(NOT Erlang_VERSION VERSION_GREATER_EQUAL "23") diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index d7601035d2..405fc8505e 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -124,6 +124,15 @@ -type ecdsa_private_key() :: [binary() | ecdsa_curve()]. -type ecdsa_public_key() :: [binary() | ecdsa_curve()]. +%% Supported EdDSA curve (Ed25519 via libsodium when AVM_USE_LIBSODIUM is enabled). +-type eddsa_curve() :: ed25519. +-type eddsa_private_key() :: [binary() | eddsa_curve()]. +-type eddsa_public_key() :: [binary() | eddsa_curve()]. + +%% DigestType accepted by sign/4 and verify/5. +%% For ECDSA use a hash_algorithm(); for EdDSA use the atom `none`. +-type sign_digest_type() :: hash_algorithm() | none. + -type mac_type() :: cmac | hmac. -type cmac_subtype() :: @@ -418,55 +427,59 @@ compute_key(_Type, _OtherPublicKey, _MyPrivateKey, _Param) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- -%% @param Algorithm signing algorithm (AtomVM supports `ecdsa`) -%% @param DigestType hash algorithm identifier +%% @param Algorithm signing algorithm (`ecdsa` or `eddsa`) +%% @param DigestType hash algorithm identifier, or `none` for EdDSA %% @param Data message bytes (iodata) %% @param Key signing key material -%% @returns Returns a DER-encoded signature as a binary. -%% @doc Create a digital signature using the AtomVM PSA backend. +%% @returns Returns the signature as a binary. +%% @doc Create a digital signature using the AtomVM crypto backend. %% %% AtomVM currently supports: -%% * `Algorithm = ecdsa` -%% * `Key = [PrivateKeyBin, Curve]` where `Curve` is one of +%% * `Algorithm = ecdsa`, `DigestType = hash_algorithm()`, +%% `Key = [PrivateKeyBin, Curve]` where `Curve` is one of %% `secp256k1 | secp256r1 | secp384r1 | secp521r1 | -%% brainpoolP256r1 | brainpoolP384r1 | brainpoolP512r1` -%% -%% The signature is returned in **DER** form. +%% brainpoolP256r1 | brainpoolP384r1 | brainpoolP512r1`. +%% The signature is returned in **DER** form. +%% * `Algorithm = eddsa`, `DigestType = none`, +%% `Key = [PrivateKeyBin, ed25519]` (requires AVM_USE_LIBSODIUM). +%% The signature is returned as a raw 64-byte binary. %% @end %%----------------------------------------------------------------------------- -spec sign( - Algorithm :: ecdsa, - DigestType :: hash_algorithm(), + Algorithm :: ecdsa | eddsa, + DigestType :: sign_digest_type(), Data :: iodata(), - Key :: ecdsa_private_key() + Key :: ecdsa_private_key() | eddsa_private_key() ) -> binary(). sign(_Algorithm, _DigestType, _Data, _Key) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- -%% @param Algorithm verification algorithm (AtomVM supports `ecdsa`) -%% @param DigestType hash algorithm identifier +%% @param Algorithm verification algorithm (`ecdsa` or `eddsa`) +%% @param DigestType hash algorithm identifier, or `none` for EdDSA %% @param Data message bytes (iodata) -%% @param Signature DER-encoded signature +%% @param Signature signature binary %% @param Key verification key material %% @returns Returns `true` if the signature is valid, otherwise `false`. -%% @doc Verify a digital signature using the AtomVM PSA backend. +%% @doc Verify a digital signature using the AtomVM crypto backend. %% %% AtomVM currently supports: -%% * `Algorithm = ecdsa` -%% * `Key = [PublicKeyBin, Curve]` where `Curve` is one of +%% * `Algorithm = ecdsa`, `DigestType = hash_algorithm()`, +%% `Key = [PublicKeyBin, Curve]` where `Curve` is one of %% `secp256k1 | secp256r1 | secp384r1 | secp521r1 | -%% brainpoolP256r1 | brainpoolP384r1 | brainpoolP512r1` -%% -%% Invalid DER signatures yield `false` (not an exception). +%% brainpoolP256r1 | brainpoolP384r1 | brainpoolP512r1`. +%% Invalid or malformed DER signatures yield `false` (not an exception). +%% * `Algorithm = eddsa`, `DigestType = none`, +%% `Key = [PublicKeyBin, ed25519]` (requires AVM_USE_LIBSODIUM). +%% A wrong-length signature binary yields `false` (not an exception). %% @end %%----------------------------------------------------------------------------- -spec verify( - Algorithm :: ecdsa, - DigestType :: hash_algorithm(), + Algorithm :: ecdsa | eddsa, + DigestType :: sign_digest_type(), Data :: iodata(), Signature :: binary(), - Key :: ecdsa_public_key() + Key :: ecdsa_public_key() | eddsa_public_key() ) -> boolean(). verify(_Algorithm, _DigestType, _Data, _Signature, _Key) -> erlang:nif_error(undefined). diff --git a/src/libAtomVM/CMakeLists.txt b/src/libAtomVM/CMakeLists.txt index 8fdb354b65..41df19dd37 100644 --- a/src/libAtomVM/CMakeLists.txt +++ b/src/libAtomVM/CMakeLists.txt @@ -147,6 +147,22 @@ if (ADVANCED_TRACING) endif() target_link_libraries(libAtomVM PUBLIC m) + +if (AVM_USE_LIBSODIUM) + target_compile_definitions(libAtomVM PUBLIC AVM_HAVE_LIBSODIUM) + + if (ESP_PLATFORM) + target_link_libraries(libAtomVM PUBLIC idf::libsodium) + else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBSODIUM REQUIRED libsodium) + + target_include_directories(libAtomVM PUBLIC ${LIBSODIUM_INCLUDE_DIRS}) + target_link_directories(libAtomVM PUBLIC ${LIBSODIUM_LIBRARY_DIRS}) + target_link_libraries(libAtomVM PUBLIC ${LIBSODIUM_LIBRARIES}) + endif() +endif() + include(CheckCSourceCompiles) set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -Werror=unknown-pragmas") check_c_source_compiles(" diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index 0cb3995281..e8fcd349ea 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -51,6 +51,10 @@ #include #endif +#ifdef AVM_HAVE_LIBSODIUM +#include +#endif + // mbedtls_ct_memcmp is available in 2.28.x+ and 3.1.x+ (absent in 3.0.x) #if (MBEDTLS_VERSION_NUMBER >= 0x021C0000 && MBEDTLS_VERSION_NUMBER < 0x03000000) \ || MBEDTLS_VERSION_NUMBER >= 0x03010000 @@ -67,6 +71,16 @@ #define HAVE_MBEDTLS_ECDSA_DER_TO_RAW 1 #endif +#if defined(HAVE_MBEDTLS_ECDSA_RAW_TO_DER) \ + || (defined(AVM_HAVE_LIBSODIUM) && defined(MBEDTLS_PSA_CRYPTO_C)) +#define CRYPTO_SIGN_AVAILABLE 1 +#endif + +#if defined(HAVE_MBEDTLS_ECDSA_DER_TO_RAW) \ + || (defined(AVM_HAVE_LIBSODIUM) && defined(MBEDTLS_PSA_CRYPTO_C)) +#define CRYPTO_VERIFY_AVAILABLE 1 +#endif + #define MAX_MD_SIZE 64 enum crypto_algorithm @@ -656,17 +670,294 @@ static void do_psa_init(void) } } +#ifdef AVM_HAVE_LIBSODIUM +static void do_sodium_init(void) +{ + if (UNLIKELY(sodium_init() < 0)) { + abort(); + } +} + +static term sodium_try_generate_key( + Context *ctx, enum pk_type_t key_type, enum pk_param_t pk_param, bool *is_handled) +{ + GlobalContext *glb = ctx->global; + + if (key_type == Eddsa && pk_param == Ed25519) { + *is_handled = true; + + unsigned char seed[crypto_sign_SEEDBYTES]; + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + unsigned char sk[crypto_sign_SECRETKEYBYTES]; + + do_sodium_init(); + randombytes_buf(seed, sizeof seed); + + if (UNLIKELY(crypto_sign_seed_keypair(pk, sk, seed) != 0)) { + sodium_memzero(seed, sizeof seed); + sodium_memzero(sk, sizeof sk); + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "libsodium Ed25519 keygen failed", ctx)); + } + + if (UNLIKELY(memory_ensure_free(ctx, + TERM_BINARY_HEAP_SIZE(sizeof pk) + TERM_BINARY_HEAP_SIZE(sizeof seed) + + TUPLE_SIZE(2)) + != MEMORY_GC_OK)) { + sodium_memzero(seed, sizeof seed); + sodium_memzero(sk, sizeof sk); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term pub_term = term_from_literal_binary(pk, sizeof pk, &ctx->heap, glb); + term priv_term = term_from_literal_binary(seed, sizeof seed, &ctx->heap, glb); + + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, pub_term); + term_put_tuple_element(result, 1, priv_term); + + sodium_memzero(seed, sizeof seed); + sodium_memzero(sk, sizeof sk); + return result; + } + + if ((key_type == Eddh || key_type == Ecdh) && pk_param == X25519) { + *is_handled = true; + + unsigned char sk[crypto_scalarmult_SCALARBYTES]; + unsigned char pk[crypto_scalarmult_BYTES]; + + do_sodium_init(); + randombytes_buf(sk, sizeof sk); + + if (UNLIKELY(crypto_scalarmult_base(pk, sk) != 0)) { + sodium_memzero(sk, sizeof sk); + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "libsodium X25519 keygen failed", ctx)); + } + + if (UNLIKELY(memory_ensure_free(ctx, + TERM_BINARY_HEAP_SIZE(sizeof pk) + TERM_BINARY_HEAP_SIZE(sizeof sk) + + TUPLE_SIZE(2)) + != MEMORY_GC_OK)) { + sodium_memzero(sk, sizeof sk); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term pub_term = term_from_literal_binary(pk, sizeof pk, &ctx->heap, glb); + term priv_term = term_from_literal_binary(sk, sizeof sk, &ctx->heap, glb); + + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, pub_term); + term_put_tuple_element(result, 1, priv_term); + + sodium_memzero(sk, sizeof sk); + return result; + } + + *is_handled = false; + return term_invalid_term(); +} + +static term sodium_try_compute_key( + Context *ctx, enum pk_type_t key_type, enum pk_param_t pk_param, + term pub_key_term, term priv_key_term, bool *is_handled) +{ + GlobalContext *glb = ctx->global; + + if (!((key_type == Eddh || key_type == Ecdh) && pk_param == X25519)) { + *is_handled = false; + return term_invalid_term(); + } + + *is_handled = true; + + if (UNLIKELY(!term_is_binary(pub_key_term) || !term_is_binary(priv_key_term))) { + RAISE_ERROR(BADARG_ATOM); + } + + const unsigned char *pub = (const unsigned char *) term_binary_data(pub_key_term); + size_t pub_len = term_binary_size(pub_key_term); + const unsigned char *priv = (const unsigned char *) term_binary_data(priv_key_term); + size_t priv_len = term_binary_size(priv_key_term); + + if (UNLIKELY(pub_len != crypto_scalarmult_BYTES + || priv_len != crypto_scalarmult_SCALARBYTES)) { + RAISE_ERROR(BADARG_ATOM); + } + + unsigned char shared[crypto_scalarmult_BYTES]; + + do_sodium_init(); + if (UNLIKELY(crypto_scalarmult(shared, priv, pub) != 0)) { + sodium_memzero(shared, sizeof shared); + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Invalid X25519 public key", ctx)); + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(sizeof shared)) != MEMORY_GC_OK)) { + sodium_memzero(shared, sizeof shared); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term result = term_from_literal_binary(shared, sizeof shared, &ctx->heap, glb); + sodium_memzero(shared, sizeof shared); + return result; +} + +static term sodium_try_sign( + Context *ctx, term alg_term, term digest_term, term data_term, term key_term, bool *is_handled) +{ + GlobalContext *glb = ctx->global; + + if (!globalcontext_is_term_equal_to_atom_string(glb, alg_term, ATOM_STR("\x5", "eddsa"))) { + *is_handled = false; + return term_invalid_term(); + } + + *is_handled = true; + + if (UNLIKELY( + !globalcontext_is_term_equal_to_atom_string(glb, digest_term, ATOM_STR("\x4", "none")))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad digest type", ctx)); + } + + if (UNLIKELY(!term_is_nonempty_list(key_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get Ed25519 private key", ctx)); + } + + term priv_term = term_get_list_head(key_term); + term tail = term_get_list_tail(key_term); + if (UNLIKELY(!term_is_binary(priv_term) || !term_is_nonempty_list(tail))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get Ed25519 private key", ctx)); + } + + term curve_term = term_get_list_head(tail); + if (UNLIKELY(!globalcontext_is_term_equal_to_atom_string( + glb, curve_term, ATOM_STR("\x7", "ed25519")))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get Ed25519 private key", ctx)); + } + + const unsigned char *seed = (const unsigned char *) term_binary_data(priv_term); + size_t seed_len = term_binary_size(priv_term); + if (UNLIKELY(seed_len != crypto_sign_SEEDBYTES)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get Ed25519 private key", ctx)); + } + + void *maybe_allocated_data = NULL; + const void *data = NULL; + size_t data_len = 0; + term iodata_result = handle_iodata(data_term, &data, &data_len, &maybe_allocated_data); + if (UNLIKELY(iodata_result != OK_ATOM)) { + free(maybe_allocated_data); + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx)); + } + + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + unsigned char sk[crypto_sign_SECRETKEYBYTES]; + unsigned char sig[crypto_sign_BYTES]; + unsigned long long sig_len = 0; + + do_sodium_init(); + + if (UNLIKELY(crypto_sign_seed_keypair(pk, sk, seed) != 0 + || crypto_sign_detached(sig, &sig_len, data, (unsigned long long) data_len, sk) + != 0)) { + free(maybe_allocated_data); + sodium_memzero(sk, sizeof sk); + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "libsodium Ed25519 sign failed", ctx)); + } + + free(maybe_allocated_data); + sodium_memzero(sk, sizeof sk); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BINARY_HEAP_SIZE(crypto_sign_BYTES)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + return term_from_literal_binary(sig, crypto_sign_BYTES, &ctx->heap, glb); +} + +static term sodium_try_verify( + Context *ctx, term alg_term, term digest_term, term data_term, + term sig_term, term key_term, bool *is_handled) +{ + GlobalContext *glb = ctx->global; + + if (!globalcontext_is_term_equal_to_atom_string(glb, alg_term, ATOM_STR("\x5", "eddsa"))) { + *is_handled = false; + return term_invalid_term(); + } + + *is_handled = true; + + if (UNLIKELY( + !globalcontext_is_term_equal_to_atom_string(glb, digest_term, ATOM_STR("\x4", "none")))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Bad digest type", ctx)); + } + + if (UNLIKELY(!term_is_binary(sig_term) + || term_binary_size(sig_term) != crypto_sign_BYTES)) { + return FALSE_ATOM; + } + + if (UNLIKELY(!term_is_nonempty_list(key_term))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get Ed25519 public key", ctx)); + } + + term pub_term = term_get_list_head(key_term); + term tail = term_get_list_tail(key_term); + if (UNLIKELY(!term_is_binary(pub_term) || !term_is_nonempty_list(tail))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get Ed25519 public key", ctx)); + } + + term curve_term = term_get_list_head(tail); + if (UNLIKELY(!globalcontext_is_term_equal_to_atom_string( + glb, curve_term, ATOM_STR("\x7", "ed25519")))) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get Ed25519 public key", ctx)); + } + + const unsigned char *pub = (const unsigned char *) term_binary_data(pub_term); + size_t pub_len = term_binary_size(pub_term); + if (UNLIKELY(pub_len != crypto_sign_PUBLICKEYBYTES)) { + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Couldn't get Ed25519 public key", ctx)); + } + + void *maybe_allocated_data = NULL; + const void *data = NULL; + size_t data_len = 0; + term iodata_result = handle_iodata(data_term, &data, &data_len, &maybe_allocated_data); + if (UNLIKELY(iodata_result != OK_ATOM)) { + free(maybe_allocated_data); + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Expected a binary or a list", ctx)); + } + + const unsigned char *sig = (const unsigned char *) term_binary_data(sig_term); + + do_sodium_init(); + int rc = crypto_sign_verify_detached(sig, data, (unsigned long long) data_len, pub); + + free(maybe_allocated_data); + return (rc == 0) ? TRUE_ATOM : FALSE_ATOM; +} +#endif /* AVM_HAVE_LIBSODIUM */ + static term nif_crypto_generate_key(Context *ctx, int argc, term argv[]) { UNUSED(argc); - do_psa_init(); - GlobalContext *glb = ctx->global; enum pk_type_t key_type = interop_atom_term_select_int(pk_type_table, argv[0], glb); enum pk_param_t pk_param = interop_atom_term_select_int(pk_param_table, argv[1], glb); +#ifdef AVM_HAVE_LIBSODIUM + bool sodium_handled; + term sodium_result = sodium_try_generate_key(ctx, key_type, pk_param, &sodium_handled); + if (sodium_handled) { + return sodium_result; + } +#endif + + do_psa_init(); + psa_key_type_t psa_key_type; size_t psa_key_bits; switch (key_type) { @@ -827,13 +1118,21 @@ static term nif_crypto_compute_key(Context *ctx, int argc, term argv[]) { UNUSED(argc); - do_psa_init(); - GlobalContext *glb = ctx->global; enum pk_type_t key_type = interop_atom_term_select_int(pk_type_table, argv[0], glb); enum pk_param_t pk_param = interop_atom_term_select_int(pk_param_table, argv[3], glb); +#ifdef AVM_HAVE_LIBSODIUM + bool sodium_handled; + term sodium_result = sodium_try_compute_key(ctx, key_type, pk_param, argv[1], argv[2], &sodium_handled); + if (sodium_handled) { + return sodium_result; + } +#endif + + do_psa_init(); + psa_algorithm_t psa_algo; psa_key_type_t psa_key_type; size_t psa_key_bits; @@ -1007,14 +1306,21 @@ static const AtomStringIntPair md_hash_algorithm_table[] = { }; #endif -#ifdef HAVE_MBEDTLS_ECDSA_RAW_TO_DER - -#define CRYPTO_SIGN_AVAILABLE 1 +#ifdef CRYPTO_SIGN_AVAILABLE static term nif_crypto_sign(Context *ctx, int argc, term argv[]) { UNUSED(argc); +#ifdef AVM_HAVE_LIBSODIUM + bool sodium_handled; + term sodium_result = sodium_try_sign(ctx, argv[0], argv[1], argv[2], argv[3], &sodium_handled); + if (sodium_handled) { + return sodium_result; + } +#endif + +#ifdef HAVE_MBEDTLS_ECDSA_RAW_TO_DER do_psa_init(); GlobalContext *glb = ctx->global; @@ -1192,18 +1498,29 @@ static term nif_crypto_sign(Context *ctx, int argc, term argv[]) } return result; +#else + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx)); +#endif /* HAVE_MBEDTLS_ECDSA_RAW_TO_DER */ } -#endif - -#ifdef HAVE_MBEDTLS_ECDSA_DER_TO_RAW +#endif /* CRYPTO_SIGN_AVAILABLE */ -#define CRYPTO_VERIFY_AVAILABLE 1 +#ifdef CRYPTO_VERIFY_AVAILABLE static term nif_crypto_verify(Context *ctx, int argc, term argv[]) { UNUSED(argc); +#ifdef AVM_HAVE_LIBSODIUM + bool sodium_handled; + term sodium_result + = sodium_try_verify(ctx, argv[0], argv[1], argv[2], argv[3], argv[4], &sodium_handled); + if (sodium_handled) { + return sodium_result; + } +#endif + +#ifdef HAVE_MBEDTLS_ECDSA_DER_TO_RAW do_psa_init(); GlobalContext *glb = ctx->global; @@ -1371,9 +1688,12 @@ static term nif_crypto_verify(Context *ctx, int argc, term argv[]) } return result; +#else + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "Unsupported key type or parameter", ctx)); +#endif /* HAVE_MBEDTLS_ECDSA_DER_TO_RAW */ } -#endif +#endif /* CRYPTO_VERIFY_AVAILABLE */ static const AtomStringIntPair cmac_algorithm_bits_table[] = { { ATOM_STR("\xB", "aes_128_cbc"), 128 }, @@ -2931,12 +3251,31 @@ term nif_crypto_info_lib(Context *ctx, int argc, term argv[]) mbedtls_version_get_string_full(version_string); size_t version_string_len = strlen(version_string); +#ifdef AVM_HAVE_LIBSODIUM + const char *libsodium_str = "libsodium"; + size_t libsodium_len = strlen("libsodium"); + const char *libsodium_version_str = sodium_version_string(); + size_t libsodium_version_len = strlen(libsodium_version_str); + int libsodium_major = sodium_library_version_major(); + int libsodium_minor = sodium_library_version_minor(); + int64_t libsodium_version_number = (int64_t) libsodium_major * 10000 + libsodium_minor; + + if (UNLIKELY(memory_ensure_free(ctx, + LIST_SIZE(2, TUPLE_SIZE(3)) + TERM_BINARY_HEAP_SIZE(mbedtls_len) + + TERM_BINARY_HEAP_SIZE(version_string_len) + BOXED_INT64_SIZE + + TERM_BINARY_HEAP_SIZE(libsodium_len) + + TERM_BINARY_HEAP_SIZE(libsodium_version_len) + BOXED_INT64_SIZE) + != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } +#else if (UNLIKELY(memory_ensure_free(ctx, LIST_SIZE(1, TUPLE_SIZE(3)) + TERM_BINARY_HEAP_SIZE(mbedtls_len) + TERM_BINARY_HEAP_SIZE(version_string_len) + BOXED_INT64_SIZE) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } +#endif term mbedtls_term = term_from_literal_binary(mbedtls_str, mbedtls_len, &ctx->heap, ctx->global); term version_term = term_make_maybe_boxed_int64(MBEDTLS_VERSION_NUMBER, &ctx->heap); @@ -2948,7 +3287,24 @@ term nif_crypto_info_lib(Context *ctx, int argc, term argv[]) term_put_tuple_element(mbedtls_tuple, 1, version_term); term_put_tuple_element(mbedtls_tuple, 2, version_string_term); +#ifdef AVM_HAVE_LIBSODIUM + term libsodium_term + = term_from_literal_binary(libsodium_str, libsodium_len, &ctx->heap, ctx->global); + term libsodium_version_term + = term_make_maybe_boxed_int64(libsodium_version_number, &ctx->heap); + term libsodium_version_string_term = term_from_literal_binary( + libsodium_version_str, libsodium_version_len, &ctx->heap, ctx->global); + + term libsodium_tuple = term_alloc_tuple(3, &ctx->heap); + term_put_tuple_element(libsodium_tuple, 0, libsodium_term); + term_put_tuple_element(libsodium_tuple, 1, libsodium_version_term); + term_put_tuple_element(libsodium_tuple, 2, libsodium_version_string_term); + + term list = term_list_prepend(mbedtls_tuple, term_nil(), &ctx->heap); + return term_list_prepend(libsodium_tuple, list, &ctx->heap); +#else return term_list_prepend(mbedtls_tuple, term_nil(), &ctx->heap); +#endif } static const struct Nif crypto_hash_nif = { diff --git a/tests/erlang_tests/test_crypto_pk.erl b/tests/erlang_tests/test_crypto_pk.erl index d3a6be7778..8f5d9a1dc1 100644 --- a/tests/erlang_tests/test_crypto_pk.erl +++ b/tests/erlang_tests/test_crypto_pk.erl @@ -26,7 +26,10 @@ test_sign_bad_algorithm/0, test_sign_malformed_key/0, test_verify_bad_algorithm/0, - test_verify_malformed_key/0 + test_verify_malformed_key/0, + test_ed25519_generate_key/0, + test_ed25519_sign_and_verify/0, + test_ed25519_verify_bad_sig/0 ]). start() -> @@ -36,6 +39,9 @@ start() -> ok = mbedtls_conditional_run(test_sign_malformed_key, 16#03060100), ok = mbedtls_conditional_run(test_verify_bad_algorithm, 16#03060100), ok = mbedtls_conditional_run(test_verify_malformed_key, 16#03060100), + ok = libsodium_conditional_run(test_ed25519_generate_key), + ok = libsodium_conditional_run(test_ed25519_sign_and_verify), + ok = libsodium_conditional_run(test_ed25519_verify_bad_sig), 0. mbedtls_conditional_run(F, RVer) -> @@ -57,6 +63,25 @@ find_openssl_or_mbedtls_ver([{<<"mbedtls">>, Ver, _} | _T], RVer) when Ver >= RV find_openssl_or_mbedtls_ver([_ | T], RVer) -> find_openssl_or_mbedtls_ver(T, RVer). +libsodium_conditional_run(F) -> + Info = crypto:info_lib(), + case has_libsodium_or_openssl(Info) of + true -> + ?MODULE:F(); + false -> + erlang:display({skipped, ?MODULE, F}), + ok + end. + +has_libsodium_or_openssl([]) -> + false; +has_libsodium_or_openssl([{<<"OpenSSL">>, _, _} | _T]) -> + true; +has_libsodium_or_openssl([{<<"libsodium">>, _, _} | _T]) -> + true; +has_libsodium_or_openssl([_ | T]) -> + has_libsodium_or_openssl(T). + test_generate_and_compute_key() -> {Pub, Priv} = crypto:generate_key(eddh, x25519), true = is_binary(Pub), @@ -179,3 +204,58 @@ test_verify_malformed_key() -> end, ok. + +test_ed25519_generate_key() -> + {Pub, Priv} = crypto:generate_key(eddsa, ed25519), + true = is_binary(Pub), + 32 = byte_size(Pub), + true = is_binary(Priv), + 32 = byte_size(Priv), + + % Two independently generated key pairs must differ. + {Pub2, Priv2} = crypto:generate_key(eddsa, ed25519), + true = Pub =/= Pub2, + true = Priv =/= Priv2, + + ok. + +test_ed25519_sign_and_verify() -> + Data = <<"Hello, Ed25519!">>, + + {Pub, Priv} = crypto:generate_key(eddsa, ed25519), + Sig = crypto:sign(eddsa, none, Data, [Priv, ed25519]), + + % Signature must be the raw 64-byte Ed25519 detached signature. + true = is_binary(Sig), + 64 = byte_size(Sig), + + % Positive: correct public key and correct data. + true = crypto:verify(eddsa, none, Data, Sig, [Pub, ed25519]), + + % Signing an iolist must produce the same result as signing the flattened binary. + Sig2 = crypto:sign(eddsa, none, [<<"Hello,">>, <<" Ed25519!">>], [Priv, ed25519]), + true = crypto:verify(eddsa, none, Data, Sig2, [Pub, ed25519]), + + % Negative: different data must not verify. + false = crypto:verify(eddsa, none, <<"Wrong data">>, Sig, [Pub, ed25519]), + + % Negative: wrong public key must not verify. + {Pub2, _Priv2} = crypto:generate_key(eddsa, ed25519), + false = crypto:verify(eddsa, none, Data, Sig, [Pub2, ed25519]), + + ok. + +test_ed25519_verify_bad_sig() -> + Data = <<"Hello, Ed25519!">>, + + {Pub, _Priv} = crypto:generate_key(eddsa, ed25519), + + % A wrong-length signature yields false, not an exception. + false = crypto:verify(eddsa, none, Data, <<"not_64_bytes">>, [Pub, ed25519]), + + % A 64-byte all-zero signature is syntactically valid in length but + % cryptographically invalid and must yield false. + ZeroSig = binary:copy(<<0>>, 64), + false = crypto:verify(eddsa, none, Data, ZeroSig, [Pub, ed25519]), + + ok.