Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions libs/estdlib/src/crypto.erl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
pbkdf2_hmac/5,
hash_equals/2,
strong_rand_bytes/1,
mlkem768_encapsulate/1,
info_lib/0
]).

Expand Down Expand Up @@ -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'
Expand Down
65 changes: 65 additions & 0 deletions src/libAtomVM/otp_crypto.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@
#include <sodium.h>
#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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 11 additions & 2 deletions src/platforms/generic_unix/lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
37 changes: 36 additions & 1 deletion tests/erlang_tests/test_crypto_pk.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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() ->
Expand All @@ -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() ->
Expand Down Expand Up @@ -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.
Loading