diff --git a/CHANGELOG.md b/CHANGELOG.md index 120e307cbd..91031113c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `onRunTrackedJs`, `onGetTrackedObjects` and `onTrackedObjectDelete` hooks, which embedders may override to customize what tracking means - Added `string:to_integer/1` +- Added `crypto:mlkem768_encapsulate/1`, requiring libsodium 1.0.22 or later +- Added `AVM_STATIC_LIBSODIUM` CMake option to statically link libsodium ### Changed - `erlang:process_info/2` now accepts only pids of local processes, as Erlang/OTP does: diff --git a/CMakeLists.txt b/CMakeLists.txt index 85e8ea201d..0da5ec51b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ 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 Ed25519 curve" OFF) +option(AVM_STATIC_LIBSODIUM "Static link libsodium." OFF) option(AVM_MINIMAL_OPCODES "Reduce VM size by excluding opcodes for optional compiler flags (no_bs_match, no_ssa_opt_bs_ensure)" OFF) # JIT & execution of precompiled code diff --git a/libs/estdlib/src/crypto.erl b/libs/estdlib/src/crypto.erl index 7b5c69ac41..573b9e0500 100644 --- a/libs/estdlib/src/crypto.erl +++ b/libs/estdlib/src/crypto.erl @@ -45,6 +45,7 @@ pbkdf2_hmac/5, hash_equals/2, strong_rand_bytes/1, + mlkem768_encapsulate/1, info_lib/0 ]). @@ -424,6 +425,26 @@ generate_key(_Type, _Param) -> compute_key(_Type, _OtherPublicKey, _MyPrivateKey, _Param) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param PublicKey the ML-KEM-768 encapsulation (public) key, 1184 bytes +%% @returns `{Ciphertext, SharedSecret}' where `Ciphertext' is 1088 bytes and +%% `SharedSecret' is 32 bytes +%% @doc ML-KEM-768 (FIPS 203) key encapsulation. +%% +%% Encapsulates a freshly generated shared secret to `PublicKey', +%% returning the ciphertext to send to the key's owner and the shared +%% secret. Used to implement post-quantum hybrid SSH key exchange +%% (`mlkem768x25519-sha256'). +%% +%% Only available when AtomVM was built with a libsodium that provides +%% ML-KEM (>= 1.0.22); otherwise this raises. +%% @end +%%----------------------------------------------------------------------------- +-spec mlkem768_encapsulate(PublicKey :: binary()) -> + {Ciphertext :: binary(), SharedSecret :: binary()}. +mlkem768_encapsulate(_PublicKey) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Algorithm signing algorithm (`ecdsa' or `eddsa') %% @param DigestType hash algorithm identifier for `ecdsa', or `none' for `eddsa' diff --git a/src/libAtomVM/otp_crypto.c b/src/libAtomVM/otp_crypto.c index 224c93a432..24f4ee833d 100644 --- a/src/libAtomVM/otp_crypto.c +++ b/src/libAtomVM/otp_crypto.c @@ -63,6 +63,13 @@ #include #endif +// ML-KEM-768 (FIPS 203) key encapsulation is available in libsodium >= 1.0.22. +// The header exposes the sizes as preprocessor macros, so their presence is a +// reliable compile-time feature test. +#if defined(HAVE_LIBSODIUM) && defined(crypto_kem_mlkem768_PUBLICKEYBYTES) +#define CRYPTO_MLKEM768_AVAILABLE 1 +#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 @@ -3248,6 +3255,52 @@ term nif_crypto_strong_rand_bytes(Context *ctx, int argc, term argv[]) return out_bin; } +#ifdef CRYPTO_MLKEM768_AVAILABLE +// crypto:mlkem768_encapsulate(PublicKey) -> {Ciphertext, SharedSecret} +// ML-KEM-768 (FIPS 203) encapsulation: given a 1184-byte encapsulation key, +// produce a 1088-byte ciphertext and the 32-byte shared secret. Used by the +// post-quantum hybrid SSH key exchange (mlkem768x25519-sha256). +static term nif_crypto_mlkem768_encapsulate(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + GlobalContext *glb = ctx->global; + + term pk_term = argv[0]; + VALIDATE_VALUE(pk_term, term_is_binary); + if (UNLIKELY(term_binary_size(pk_term) != crypto_kem_mlkem768_PUBLICKEYBYTES)) { + RAISE_ERROR(BADARG_ATOM); + } + const unsigned char *pk = (const unsigned char *) term_binary_data(pk_term); + + unsigned char ct[crypto_kem_mlkem768_CIPHERTEXTBYTES]; + unsigned char ss[crypto_kem_mlkem768_SHAREDSECRETBYTES]; + + do_sodium_init(); + if (UNLIKELY(crypto_kem_mlkem768_enc(ct, ss, pk) != 0)) { + sodium_memzero(ss, sizeof ss); + RAISE_ERROR(make_crypto_error(__FILE__, __LINE__, "ML-KEM-768 encapsulation failed", ctx)); + } + + if (UNLIKELY(memory_ensure_free(ctx, + TERM_BINARY_HEAP_SIZE(sizeof ct) + TERM_BINARY_HEAP_SIZE(sizeof ss) + + TUPLE_SIZE(2)) + != MEMORY_GC_OK)) { + sodium_memzero(ss, sizeof ss); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term ct_term = term_from_literal_binary(ct, sizeof ct, &ctx->heap, glb); + term ss_term = term_from_literal_binary(ss, sizeof ss, &ctx->heap, glb); + + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, ct_term); + term_put_tuple_element(result, 1, ss_term); + + sodium_memzero(ss, sizeof ss); + return result; +} +#endif + static const char *get_mbedtls_version_string_full(char *buf, size_t buf_size) { #if defined(MBEDTLS_VERSION_C) @@ -3434,6 +3487,12 @@ static const struct Nif crypto_strong_rand_bytes_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_strong_rand_bytes }; +#ifdef CRYPTO_MLKEM768_AVAILABLE +static const struct Nif crypto_mlkem768_encapsulate_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_crypto_mlkem768_encapsulate +}; +#endif static const struct Nif crypto_info_lib = { .base.type = NIFFunctionType, .nif_ptr = nif_crypto_info_lib @@ -3547,6 +3606,12 @@ const struct Nif *otp_crypto_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_strong_rand_bytes_nif; } +#ifdef CRYPTO_MLKEM768_AVAILABLE + if (strcmp("mlkem768_encapsulate/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &crypto_mlkem768_encapsulate_nif; + } +#endif if (strcmp("info_lib/0", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &crypto_info_lib; diff --git a/src/platforms/generic_unix/lib/CMakeLists.txt b/src/platforms/generic_unix/lib/CMakeLists.txt index 4ec0e9e527..1874b64541 100644 --- a/src/platforms/generic_unix/lib/CMakeLists.txt +++ b/src/platforms/generic_unix/lib/CMakeLists.txt @@ -116,8 +116,17 @@ if (AVM_USE_LIBSODIUM) pkg_check_modules(LIBSODIUM REQUIRED libsodium) target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${LIBSODIUM_INCLUDE_DIRS}) - target_link_directories(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${LIBSODIUM_LIBRARY_DIRS}) - target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${LIBSODIUM_LIBRARIES}) + if (AVM_STATIC_LIBSODIUM) + find_library(LIBSODIUM_STATIC_LIB NAMES libsodium.a PATHS ${LIBSODIUM_LIBRARY_DIRS}) + if (LIBSODIUM_STATIC_LIB STREQUAL "LIBSODIUM_STATIC_LIB-NOTFOUND") + message(FATAL_ERROR "AVM_STATIC_LIBSODIUM=ON but libsodium.a was not found in ${LIBSODIUM_LIBRARY_DIRS}") + endif() + message(STATUS "Found static libsodium ${LIBSODIUM_STATIC_LIB}") + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${LIBSODIUM_STATIC_LIB}) + else() + target_link_directories(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${LIBSODIUM_LIBRARY_DIRS}) + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${LIBSODIUM_LIBRARIES}) + endif() target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC HAVE_LIBSODIUM) endif() diff --git a/tests/erlang_tests/test_crypto_pk.erl b/tests/erlang_tests/test_crypto_pk.erl index 7c0bafc910..8241b807a9 100644 --- a/tests/erlang_tests/test_crypto_pk.erl +++ b/tests/erlang_tests/test_crypto_pk.erl @@ -34,7 +34,8 @@ test_ed25519_verify_malformed_key/0, test_ed25519_sign_bad_digest/0, test_ed25519_verify_bad_digest/0, - test_x25519_mutual_key_agreement/0 + test_x25519_mutual_key_agreement/0, + test_mlkem768_encapsulate/0 ]). start() -> @@ -57,6 +58,8 @@ start() -> ok = libsodium_conditional_run(test_ed25519_sign_bad_digest), ok = libsodium_conditional_run(test_ed25519_verify_bad_digest), ok = libsodium_conditional_run(test_x25519_mutual_key_agreement), + % libsodium_conditional_run/1 also passes for OpenSSL; gated inside instead. + ok = test_mlkem768_encapsulate(), 0. otp_version() -> @@ -425,3 +428,35 @@ test_x25519_mutual_key_agreement() -> 32 = byte_size(ThirdShared), ok. + +test_mlkem768_encapsulate() -> + Pk = <<0:(1184 * 8)>>, + case mlkem768_available(Pk) of + false -> + ok; + true -> + {Ct, Ss} = crypto:mlkem768_encapsulate(Pk), + 1088 = byte_size(Ct), + 32 = byte_size(Ss), + %% Encapsulation is randomized. + {Ct2, Ss2} = crypto:mlkem768_encapsulate(Pk), + true = (Ct =/= Ct2), + true = (Ss =/= Ss2), + ok = expect_badarg(fun() -> crypto:mlkem768_encapsulate(<<0:8>>) end), + ok + end. + +%% Only undef means ML-KEM is absent; anything else must propagate. +mlkem768_available(Pk) -> + try crypto:mlkem768_encapsulate(Pk) of + {_Ct, _Ss} -> true + catch + error:undef -> false + end. + +expect_badarg(F) -> + try F() of + _ -> error + catch + error:badarg -> ok + end.