From 71cc08277608b13481d06e7d90c9acc094a3330d Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 4 Nov 2023 07:26:32 +0100 Subject: [PATCH] Add minimal support for ssl module using Mbed TLS and sockets Add support for ssl client in binary and passive modes, with no certificate verification Add APIs to otp_socket so it can be called from ssl bio callbacks Fix a bug in lwIP otp_socket's recv revealed by ssl tests Fix a bug in BSD otp_socket's recvfrom revealed by refactoring Fix a bug in esp32 tests where main context and its resources were not properly destroyed Update documentation and workflows to reflect the requirement on Mbed TLS Fix exported types of inet module Signed-off-by: Paul Guyot --- .github/workflows/build-and-test-macos.yaml | 2 +- .../workflows/build-and-test-on-freebsd.yaml | 4 +- .github/workflows/build-and-test-other.yaml | 2 +- .github/workflows/build-and-test.yaml | 4 +- CHANGELOG.md | 1 + CMakeModules/MbedTLS.cmake | 97 +++ README.Md | 1 + doc/src/build-instructions.md | 1 + libs/estdlib/src/CMakeLists.txt | 1 + libs/estdlib/src/gen_tcp.erl | 2 +- libs/estdlib/src/gen_udp.erl | 6 +- libs/estdlib/src/inet.erl | 13 +- libs/estdlib/src/ssl.erl | 397 +++++++++ src/libAtomVM/globalcontext.c | 8 + src/libAtomVM/otp_socket.c | 478 ++++++----- src/libAtomVM/otp_socket.h | 50 ++ src/libAtomVM/otp_ssl.c | 795 ++++++++++++++++++ src/libAtomVM/otp_ssl.h | 38 + .../components/avm_builtins/CMakeLists.txt | 5 + .../esp32/components/avm_builtins/Kconfig | 4 + .../avm_builtins/otp_ssl_platform.c | 31 + .../esp32/components/avm_sys/CMakeLists.txt | 1 + .../test/main/test_erl_sources/CMakeLists.txt | 3 + .../test/main/test_erl_sources/test_ssl.erl | 162 ++++ src/platforms/esp32/test/main/test_main.c | 22 + src/platforms/generic_unix/lib/CMakeLists.txt | 14 + .../generic_unix/lib/platform_nifs.c | 13 +- src/platforms/generic_unix/lib/sys.c | 7 + src/platforms/rp2040/src/lib/CMakeLists.txt | 7 +- src/platforms/rp2040/src/lib/mbedtls_config.h | 93 ++ .../rp2040/src/lib/otp_ssl_platform.c | 25 + tests/CMakeLists.txt | 8 + tests/libs/estdlib/CMakeLists.txt | 1 + tests/libs/estdlib/test_ssl.erl | 80 ++ tests/libs/estdlib/tests.erl | 2 +- 35 files changed, 2157 insertions(+), 221 deletions(-) create mode 100644 CMakeModules/MbedTLS.cmake create mode 100644 libs/estdlib/src/ssl.erl create mode 100644 src/libAtomVM/otp_ssl.c create mode 100644 src/libAtomVM/otp_ssl.h create mode 100644 src/platforms/esp32/components/avm_builtins/otp_ssl_platform.c create mode 100644 src/platforms/esp32/test/main/test_erl_sources/test_ssl.erl create mode 100644 src/platforms/rp2040/src/lib/mbedtls_config.h create mode 100644 src/platforms/rp2040/src/lib/otp_ssl_platform.c create mode 100644 tests/libs/estdlib/test_ssl.erl diff --git a/.github/workflows/build-and-test-macos.yaml b/.github/workflows/build-and-test-macos.yaml index 63ce352dc..bc0b85325 100644 --- a/.github/workflows/build-and-test-macos.yaml +++ b/.github/workflows/build-and-test-macos.yaml @@ -44,7 +44,7 @@ jobs: submodules: 'recursive' - name: "Install deps" - run: brew install gperf doxygen erlang@${{ matrix.otp }} ninja + run: brew install gperf doxygen erlang@${{ matrix.otp }} ninja mbedtls # Builder info - name: "System info" diff --git a/.github/workflows/build-and-test-on-freebsd.yaml b/.github/workflows/build-and-test-on-freebsd.yaml index f7e2ff595..6ef6828d8 100644 --- a/.github/workflows/build-and-test-on-freebsd.yaml +++ b/.github/workflows/build-and-test-on-freebsd.yaml @@ -57,7 +57,7 @@ jobs: echo "%%" echo "**freebsd-version:**" freebsd-version - sudo pkg install -y cmake gperf erlang elixir + sudo pkg install -y cmake gperf erlang elixir mbedtls echo "**uname:**" uname -a echo "**C Compiler version:**" @@ -73,7 +73,7 @@ jobs: echo "%%" mkdir build cd build - cmake .. + cmake .. -DMBEDTLS_ROOT_DIR=/usr/local echo "%%" echo "%% Building AtomVM ..." diff --git a/.github/workflows/build-and-test-other.yaml b/.github/workflows/build-and-test-other.yaml index 5564ae47d..c927e9f07 100644 --- a/.github/workflows/build-and-test-other.yaml +++ b/.github/workflows/build-and-test-other.yaml @@ -98,7 +98,7 @@ jobs: apt update && apt install -y -t stretch-backports-sloppy libarchive13 && apt install -y -t stretch-backports cmake && - apt install -y file gcc g++ binutils make doxygen gperf zlib1g-dev libssl-dev + apt install -y file gcc g++ binutils make doxygen gperf zlib1g-dev libssl-dev libmbedtls-dev - arch: "arm32v7" platform: "arm/v7" diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 6e1dd0171..934ef4beb 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -174,7 +174,7 @@ jobs: cmake_opts_other: "-DOPENSSL_CRYPTO_LIBRARY=/usr/lib/i386-linux-gnu/libcrypto.so -DAVM_CREATE_STACKTRACES=off" arch: "i386" compiler_pkgs: "gcc-10 g++-10 gcc-10-multilib g++-10-multilib libc6-dev-i386 - libc6-dbg:i386 zlib1g-dev:i386 libssl-dev:i386" + libc6-dbg:i386 zlib1g-dev:i386 libssl-dev:i386 libmbedtls-dev:i386" env: CC: ${{ matrix.cc }} @@ -202,7 +202,7 @@ jobs: run: sudo apt update -y - name: "Install deps" - run: sudo apt install -y ${{ matrix.compiler_pkgs}} cmake gperf zlib1g-dev doxygen valgrind + run: sudo apt install -y ${{ matrix.compiler_pkgs}} cmake gperf zlib1g-dev doxygen valgrind libmbedtls-dev # Builder info - name: "System info" diff --git a/CHANGELOG.md b/CHANGELOG.md index 235462385..e830350c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for interrupts to STM32 GPIO port driver. - Added suppoprt for PicoW extra gpio pins (led) to the gpio driver. - Added support for `net:getaddrinfo/1,2` +- Added minimal support for the OTP `ssl` interface. ## [0.6.0-alpha.1] - 2023-10-09 diff --git a/CMakeModules/MbedTLS.cmake b/CMakeModules/MbedTLS.cmake new file mode 100644 index 000000000..52f0409d1 --- /dev/null +++ b/CMakeModules/MbedTLS.cmake @@ -0,0 +1,97 @@ +# +# This file is part of AtomVM. +# +# Copyright 2023 Paul Guyot +# +# 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 +# + +# Find MbedTLS +# Search for MbedTLS 2.x or 3.x and define libraries like MbedTLS 3.x does. + +# This script is not called FindMbedTLS.cmake because it would conflict with +# installed MbedTLS 3.x + +# If MBEDTLS_ROOT_DIR is set, no heuristic is applied. +# It must be set to the parent directory of include/mbedtls/version.h +# Libraries are at ${MBEDTLS_LIBRARIES_DIR} or, if unset, ${MBEDTLS_ROOT_DIR}/lib/ + +# If MBEDTLS_ROOT_DIR is not set, apply the following heuristic: +# Try to find mbedtls 3.x CMake package with find_package +# If it doesn't work, search for MBEDTLS_VERSION_NUMBER symbol as well as +# the three libraries we need with check_symbol_exists and find_library + +if (MBEDTLS_ROOT_DIR) + set(MbedTLS_FOUND TRUE) + if (NOT MBEDTLS_LIBRARIES_DIR) + set(MBEDTLS_LIBRARIES_DIR ${MBEDTLS_ROOT_DIR}/lib) + endif() + message(STATUS "Will use MbedTLS from ${MBEDTLS_ROOT_DIR} and ${MBEDTLS_LIBRARIES_DIR}") + + add_library(MbedTLS::mbedcrypto SHARED IMPORTED) + set_target_properties(MbedTLS::mbedcrypto PROPERTIES + IMPORTED_LOCATION "${MBEDTLS_LIBRARIES_DIR}/libmbedcrypto${CMAKE_SHARED_LIBRARY_SUFFIX}" + INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_ROOT_DIR}/include/" + ) + + add_library(MbedTLS::mbedx509 SHARED IMPORTED) + set_target_properties(MbedTLS::mbedx509 PROPERTIES + IMPORTED_LOCATION "${MBEDTLS_LIBRARIES_DIR}/libmbedx509${CMAKE_SHARED_LIBRARY_SUFFIX}" + INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_ROOT_DIR}/include/" + INTERFACE_LINK_LIBRARIES "MbedTLS::mbedcrypto" + ) + + add_library(MbedTLS::mbedtls SHARED IMPORTED) + set_target_properties(MbedTLS::mbedtls PROPERTIES + IMPORTED_LOCATION "${MBEDTLS_LIBRARIES_DIR}/libmbedtls${CMAKE_SHARED_LIBRARY_SUFFIX}" + INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_ROOT_DIR}/include/" + INTERFACE_LINK_LIBRARIES "MbedTLS::mbedx509" + ) +else() + # MbedTLS 3.x is installed as a CMake package + find_package(MbedTLS QUIET) + if (MbedTLS_FOUND) + message(STATUS "Found MbedTLS package ${MbedTLS_FOUND}") + else() + include(CheckSymbolExists) + check_symbol_exists(MBEDTLS_VERSION_NUMBER "mbedtls/version.h" HAVE_MBEDTLS_VERSION_NUMBER) + find_library(MBEDCRYPTO mbedcrypto) + find_library(MBEDX509 mbedx509) + find_library(MBEDTLS mbedtls) + if (HAVE_MBEDTLS_VERSION_NUMBER + AND NOT ${MBEDCRYPTO} STREQUAL "MBEDCRYPTO-NOTFOUND" + AND NOT ${MBEDX509} STREQUAL "MBEDX509-NOTFOUND" + AND NOT ${MBEDTLS} STREQUAL "MBEDTLS-NOTFOUND") + message(STATUS "Found MbedTLS with mbedcrypto ${MBEDCRYPTO}, mbedx509 ${MBEDX509} and mbedtls ${MBEDTLS}") + set(MbedTLS_FOUND TRUE) + add_library(MbedTLS::mbedcrypto SHARED IMPORTED) + set_target_properties(MbedTLS::mbedcrypto PROPERTIES + IMPORTED_LOCATION "${MBEDCRYPTO}" + ) + + add_library(MbedTLS::mbedx509 SHARED IMPORTED) + set_target_properties(MbedTLS::mbedx509 PROPERTIES + IMPORTED_LOCATION "${MBEDX509}" + INTERFACE_LINK_LIBRARIES "MbedTLS::mbedcrypto" + ) + + add_library(MbedTLS::mbedtls SHARED IMPORTED) + set_target_properties(MbedTLS::mbedtls PROPERTIES + IMPORTED_LOCATION "${MBEDTLS}" + INTERFACE_LINK_LIBRARIES "MbedTLS::mbedx509" + ) + endif() + endif() +endif() diff --git a/README.Md b/README.Md index 7a298e6fa..5c71a3e5f 100644 --- a/README.Md +++ b/README.Md @@ -38,6 +38,7 @@ Required for building: * gperf ([GNU Perfect Hash Function Generator](https://www.gnu.org/software/gperf/manual/gperf.html)) * erlc ([erlang compiler](https://www.erlang.org/)) * elixirc ([elixir compiler](https://elixir-lang.org)) +* Mbed TLS ([portable TLS library, optionally required to support SSL](https://www.trustedfirmware.org/projects/mbed-tls/)) * zlib ([zlib compression and decompression library](https://zlib.net/)) Documentation and Coverage: diff --git a/doc/src/build-instructions.md b/doc/src/build-instructions.md index bd3d415be..c6e9671db 100644 --- a/doc/src/build-instructions.md +++ b/doc/src/build-instructions.md @@ -58,6 +58,7 @@ The following software is required in order to build AtomVM in generic UNIX syst * `make` * `gperf` * `zlib` +* `Mbed TLS` * Erlang/OTP compiler (`erlc`) * Elixir compiler diff --git a/libs/estdlib/src/CMakeLists.txt b/libs/estdlib/src/CMakeLists.txt index 35c5db10b..68a4141ae 100644 --- a/libs/estdlib/src/CMakeLists.txt +++ b/libs/estdlib/src/CMakeLists.txt @@ -46,6 +46,7 @@ set(ERLANG_MODULES logger_std_h proplists socket + ssl string timer unicode diff --git a/libs/estdlib/src/gen_tcp.erl b/libs/estdlib/src/gen_tcp.erl index f245411b0..ca53230ab 100644 --- a/libs/estdlib/src/gen_tcp.erl +++ b/libs/estdlib/src/gen_tcp.erl @@ -86,7 +86,7 @@ %% @end %%----------------------------------------------------------------------------- -spec connect( - Address :: inet:address() | inet:hostname(), + Address :: inet:ip_address() | inet:hostname(), Port :: inet:port_number(), Options :: [connect_option()] ) -> diff --git a/libs/estdlib/src/gen_udp.erl b/libs/estdlib/src/gen_udp.erl index cf5d97460..23bd518a2 100644 --- a/libs/estdlib/src/gen_udp.erl +++ b/libs/estdlib/src/gen_udp.erl @@ -103,7 +103,7 @@ open(PortNum, Options) -> %%----------------------------------------------------------------------------- -spec send( Socket :: inet:socket(), - Address :: inet:address(), + Address :: inet:ip_address(), PortNum :: inet:port_number(), Packet :: packet() ) -> ok | {error, reason()}. @@ -121,7 +121,7 @@ send(Socket, Address, PortNum, Packet) -> %% @end %%----------------------------------------------------------------------------- -spec recv(Socket :: inet:socket(), Length :: non_neg_integer()) -> - {ok, {inet:address(), inet:port_number(), packet()}} | {error, reason()}. + {ok, {inet:ip_address(), inet:port_number(), packet()}} | {error, reason()}. recv(Socket, Length) -> recv(Socket, Length, infinity). @@ -143,7 +143,7 @@ recv(Socket, Length) -> %% @end %%----------------------------------------------------------------------------- -spec recv(Socket :: inet:socket(), Length :: non_neg_integer(), Timeout :: timeout()) -> - {ok, {inet:address(), inet:port_number(), packet()}} | {error, reason()}. + {ok, {inet:ip_address(), inet:port_number(), packet()}} | {error, reason()}. recv(Socket, Length, Timeout) -> call(Socket, {recvfrom, Length, Timeout}). diff --git a/libs/estdlib/src/inet.erl b/libs/estdlib/src/inet.erl index cea2a9f67..369a5d8d7 100644 --- a/libs/estdlib/src/inet.erl +++ b/libs/estdlib/src/inet.erl @@ -24,12 +24,11 @@ -type port_number() :: 0..65535. -type socket() :: pid(). --type address() :: ipv4_address(). --type ipv4_address() :: {octet(), octet(), octet(), octet()}. --type octet() :: 0..255. +-type ip_address() :: ip4_address(). +-type ip4_address() :: {0..255, 0..255, 0..255, 0..255}. -type hostname() :: iodata(). --export_type([socket/0, port_number/0, address/0, ipv4_address/0, octet/0, hostname/0]). +-export_type([socket/0, port_number/0, ip_address/0, ip4_address/0, hostname/0]). %%----------------------------------------------------------------------------- %% @param Socket the socket from which to obtain the port number @@ -61,7 +60,8 @@ close(Socket) -> %% This function should be called on a running socket instance. %% @end %%----------------------------------------------------------------------------- --spec sockname(Socket :: socket()) -> {ok, {address(), port_number()}} | {error, Reason :: term()}. +-spec sockname(Socket :: socket()) -> + {ok, {ip_address(), port_number()}} | {error, Reason :: term()}. sockname(Socket) -> call(Socket, {sockname}). @@ -72,7 +72,8 @@ sockname(Socket) -> %% This function should be called on a running socket instance. %% @end %%----------------------------------------------------------------------------- --spec peername(Socket :: socket()) -> {ok, {address(), port_number()}} | {error, Reason :: term()}. +-spec peername(Socket :: socket()) -> + {ok, {ip_address(), port_number()}} | {error, Reason :: term()}. peername(Socket) -> call(Socket, {peername}). diff --git a/libs/estdlib/src/ssl.erl b/libs/estdlib/src/ssl.erl new file mode 100644 index 000000000..ce74f6fe1 --- /dev/null +++ b/libs/estdlib/src/ssl.erl @@ -0,0 +1,397 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% 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(ssl). + +-export([ + start/0, + stop/0, + connect/3, + close/1, + send/2, + recv/2 +]). + +-behaviour(gen_server). +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2 +]). + +-export([ + nif_close_notify/1, + nif_conf_authmode/2, + nif_conf_rng/2, + nif_config_defaults/3, + nif_config_init/0, + nif_ctr_drbg_init/0, + nif_ctr_drbg_seed/3, + nif_entropy_init/0, + nif_handshake_step/1, + nif_init/0, + nif_read/2, + nif_set_bio/2, + nif_set_hostname/2, + nif_setup/2, + nif_write/2 +]). + +% Resources +-type entropy() :: binary(). +-type ctrdrbg() :: binary(). +-type sslcontext() :: binary(). +-type sslconfig() :: binary(). + +-opaque sslsocket() :: {sslcontext(), socket:socket()}. + +-export_type([ + sslsocket/0, + host/0, + hostname/0 +]). + +-type host() :: hostname() | ip_address(). +-type hostname() :: string(). +-type ip_address() :: inet:ip_address(). +-type tls_client_option() :: client_option(). +-type client_option() :: + {server_name_indication, sni()}. +-type sni() :: hostname() | disabled. +-type reason() :: any(). + +-spec start() -> ok. +start() -> + try + {ok, _Pid} = gen_server:start({local, ?MODULE}, ?MODULE, [], []) + catch + error:{badmatch, {error, {already_started, _}}} -> + ok + end, + ok. + +-spec stop() -> ok. +stop() -> + ok = gen_server:call(?MODULE, stop). + +-record(state, { + ctr_drbg :: ctrdrbg(), + entropy :: entropy() +}). + +init([]) -> + Entropy = ?MODULE:nif_entropy_init(), + CtrDrbg = ?MODULE:nif_ctr_drbg_init(), + ok = ?MODULE:nif_ctr_drbg_seed(CtrDrbg, Entropy, <<"AtomVM">>), + {ok, #state{entropy = Entropy, ctr_drbg = CtrDrbg}}. + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(get_ctr_drbg, _From, #state{ctr_drbg = CtrDrbg} = State) -> + {reply, CtrDrbg, State}; +handle_call(get_entropy, _From, #state{entropy = Entropy} = State) -> + {reply, Entropy, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Msg, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +-spec connect(Host :: host(), Port :: inet:port_number(), TLSOptions :: [tls_client_option()]) -> + {ok, sslsocket()} | {error, reason()}. +connect(Hostname, Port, TLSOptions) when + is_list(Hostname) andalso is_integer(Port) andalso is_list(TLSOptions) +-> + % Erlang OTP actually first checks some options + case net:getaddrinfo(Hostname) of + {ok, Results} -> + case + [ + Addr + || #{addr := #{addr := Addr}, type := stream, protocol := tcp, family := inet} <- + Results + ] + of + [TCPAddr | _] -> + NewTLSOptions = + case lists:keyfind(server_name_indication, 1, TLSOptions) of + false -> [{server_name_indication, Hostname} | TLSOptions]; + _ -> TLSOptions + end, + connect(TCPAddr, Port, NewTLSOptions); + [] -> + {error, nxdomain} + end; + {error, _} -> + {error, nxdomain} + end; +connect(Addr, Port, TLSOptions) when + is_tuple(Addr) andalso is_integer(Port) andalso is_list(TLSOptions) +-> + {ok, Socket} = socket:open(inet, stream, tcp), + case socket:connect(Socket, #{family => inet, addr => Addr, port => Port}) of + ok -> + connect(Socket, TLSOptions); + {error, _Reason} -> + {error, _Reason} + end. + +-spec connect(Socket :: socket:socket(), TLSOptions :: [tls_client_option()]) -> + {ok, sslsocket()} | {error, reason()}. +connect(Socket, TLSOptions) -> + SSLContext = ?MODULE:nif_init(), + ok = ?MODULE:nif_set_bio(SSLContext, Socket), + SSLConfig = ?MODULE:nif_config_init(), + ok = ?MODULE:nif_config_defaults(SSLConfig, client, stream), + process_options(SSLContext, SSLConfig, TLSOptions), + CtrDrbg = gen_server:call(?MODULE, get_ctr_drbg), + ok = ?MODULE:nif_conf_rng(SSLConfig, CtrDrbg), + ok = ?MODULE:nif_setup(SSLContext, SSLConfig), + handshake_loop(SSLContext, Socket). + +handshake_loop(SSLContext, Socket) -> + case ?MODULE:nif_handshake_step(SSLContext) of + ok -> + handshake_loop(SSLContext, Socket); + done -> + {ok, {SSLContext, Socket}}; + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + handshake_loop(SSLContext, Socket); + {closed, Ref} -> + ok = socket:close(Socket), + {error, closed} + end; + {error, _Reason} = Error -> + socket:close(Socket), + Error + end; + want_write -> + % We're currrently missing non-blocking writes + handshake_loop(SSLContext, Socket); + {error, _Reason} = Error -> + socket:close(Socket), + Error + end. + +-spec process_options( + SSLContext :: sslcontext(), SSLConfig :: sslconfig(), TLSOptions :: [tls_client_option()] +) -> ok. +process_options(_SSLContext, _SSLConfig, []) -> + ok; +process_options(SSLContext, SSLConfig, [{server_name_indication, disabled} | Tail]) -> + process_options(SSLContext, SSLConfig, Tail); +process_options(SSLContext, SSLConfig, [{server_name_indication, Hostname} | Tail]) -> + ok = ?MODULE:nif_set_hostname(SSLContext, Hostname), + process_options(SSLContext, SSLConfig, Tail); +process_options(SSLContext, SSLConfig, [{verify, verify_none} | Tail]) -> + ok = ?MODULE:nif_conf_authmode(SSLConfig, none), + process_options(SSLContext, SSLConfig, Tail); +process_options(SSLContext, SSLConfig, [{binary, true} | Tail]) -> + process_options(SSLContext, SSLConfig, Tail); +process_options(SSLContext, SSLConfig, [{active, false} | Tail]) -> + process_options(SSLContext, SSLConfig, Tail). + +-spec close(sslsocket()) -> ok. +close({SSLContext, Socket}) -> + _ = close_notify_loop(SSLContext, Socket), + ok = socket:close(Socket), + ok. + +close_notify_loop(SSLContext, Socket) -> + case ?MODULE:nif_close_notify(SSLContext) of + ok -> + ok; + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + close_notify_loop(SSLContext, Socket); + {closed, Ref} -> + ok = socket:close(Socket), + {error, closed} + end; + {error, _Reason} = Error -> + socket:close(Socket), + Error + end; + want_write -> + % We're currrently missing non-blocking writes + close_notify_loop(SSLContext, Socket); + {error, _Reason} = Error -> + socket:close(Socket), + Error + end. + +-spec send(Socket :: sslsocket(), Data :: iodata()) -> ok | {error, reason()}. +send(SSLSocket, IOList) when is_list(IOList) -> + send(SSLSocket, iolist_to_binary(IOList)); +send({SSLContext, Socket} = SSLSocket, Binary) -> + case ?MODULE:nif_write(SSLContext, Binary) of + ok -> + ok; + {ok, Rest} -> + send(SSLSocket, Rest); + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + send(SSLSocket, Binary); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + % We're currrently missing non-blocking writes + send(SSLSocket, Binary); + {error, _Reason} = Error -> + Error + end. + +-spec recv(Socket :: sslsocket(), Length :: non_neg_integer()) -> ok | {error, reason()}. +recv(SSLSocket, Length) -> + recv0(SSLSocket, Length, []). + +recv0(_SSLSocket, 0, Acc) -> + {ok, list_to_binary(lists:reverse(Acc))}; +recv0({SSLContext, Socket} = SSLSocket, Remaining, Acc) -> + case ?MODULE:nif_read(SSLContext, Remaining) of + {ok, Data} -> + Len = byte_size(Data), + recv0(SSLSocket, Remaining - Len, [Data | Acc]); + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + recv0(SSLSocket, Remaining, Acc); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + % We're currrently missing non-blocking writes + recv0(SSLSocket, Remaining, Acc); + {error, _Reason} = Error -> + Error + end. + +%%----------------------------------------------------------------------------- +%% NIF Functions +%%----------------------------------------------------------------------------- + +%% @private +-spec nif_entropy_init() -> entropy(). +nif_entropy_init() -> + erlang:nif_error(undefined). + +%% @private +-spec nif_ctr_drbg_init() -> ctrdrbg(). +nif_ctr_drbg_init() -> + erlang:nif_error(undefined). + +%% @private +-spec nif_ctr_drbg_seed(CtrDrbg :: ctrdrbg(), Entropy :: entropy(), Custom :: binary()) -> ok. +nif_ctr_drbg_seed(_CtrDrbg, _Entropy, _Custom) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_init() -> sslcontext(). +nif_init() -> + erlang:nif_error(undefined). + +%% @private +-spec nif_config_init() -> sslconfig(). +nif_config_init() -> + erlang:nif_error(undefined). + +%% @private +-spec nif_config_defaults( + Config :: sslconfig(), Endpoint :: client | server, Transport :: stream | dgram +) -> ok. +nif_config_defaults(_Config, _Endpoint, _Transport) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_conf_authmode(Config :: sslconfig(), none | optional | required) -> ok. +nif_conf_authmode(_Config, _AuthMode) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_conf_rng(Config :: sslconfig(), CtrDrbg :: ctrdrbg()) -> ok. +nif_conf_rng(_Config, _CtrDrbg) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_setup(Context :: sslcontext(), Config :: sslconfig()) -> ok. +nif_setup(_Context, _Config) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_set_bio(Context :: sslcontext(), Socket :: socket:socket()) -> ok. +nif_set_bio(_Context, _Socket) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_set_hostname(Context :: sslcontext(), Hostname :: hostname()) -> ok. +nif_set_hostname(_Context, _Hostname) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_handshake_step(Context :: sslcontext()) -> + ok | done | want_read | want_write | {error, reason()}. +nif_handshake_step(_Context) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_close_notify(Context :: sslcontext()) -> ok | want_read | want_write | {error, reason()}. +nif_close_notify(_Context) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_read(Context :: sslcontext(), Length :: non_neg_integer()) -> + {ok, binary()} | want_read | want_write | {error, reason()}. +nif_read(_Context, _Length) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_write(Context :: sslcontext(), Len :: non_neg_integer()) -> + {ok, binary()} | want_read | want_write | {error, reason()}. +nif_write(_Context, _Binary) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index 25dded5ec..62ff40533 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -216,9 +216,17 @@ COLD_FUNC void globalcontext_destroy(GlobalContext *glb) synclist_destroy(&glb->select_events); // Destroy refc binaries including resources + // (this list should be empty if resources were properly refcounted) struct ListHead *refc_binaries = synclist_nolock(&glb->refc_binaries); MUTABLE_LIST_FOR_EACH (item, tmp, refc_binaries) { struct RefcBinary *refc = GET_LIST_ENTRY(item, struct RefcBinary, head); +#ifndef NDEBUG + if (refc->resource_type) { + fprintf(stderr, "Warning, dangling resource of type %s, ref_count = %d\n", refc->resource_type->name, (int) refc->ref_count); + } else { + fprintf(stderr, "Warning, dangling refc binary, ref_count = %d\n", (int) refc->ref_count); + } +#endif refc_binary_destroy(refc, glb); } synclist_destroy(&glb->refc_binaries); diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index a950552fe..8fca3e3ee 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -557,12 +557,18 @@ static term nif_socket_open(Context *ctx, int argc, term argv[]) } } -static term get_socket(term socket_term) +bool term_to_otp_socket(term socket_term, struct SocketResource **rsrc_obj, Context *ctx) { - return term_get_tuple_element(socket_term, 0); + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), term_get_tuple_element(socket_term, 0), socket_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; + + return true; } -static bool term_is_socket(term socket_term) +bool term_is_otp_socket(term socket_term) { bool ret = term_is_tuple(socket_term) && term_get_tuple_arity(socket_term) == 2 @@ -609,13 +615,12 @@ static term nif_socket_close(Context *ctx, int argc, term argv[]) TRACE("nif_socket_close\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd) { if (rsrc_obj->selecting_process_id != INVALID_PROCESS_ID) { @@ -867,17 +872,16 @@ static term nif_socket_select_read(Context *ctx, int argc, term argv[]) UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); term select_ref_term = argv[1]; if (select_ref_term != UNDEFINED_ATOM) { VALIDATE_VALUE(select_ref_term, term_is_reference); } - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; ErlNifEnv *env = erl_nif_env_from_context(ctx); if (rsrc_obj->selecting_process_id != ctx->process_id && rsrc_obj->selecting_process_id != INVALID_PROCESS_ID) { @@ -961,13 +965,12 @@ static term nif_socket_select_stop(Context *ctx, int argc, term argv[]) TRACE("nif_socket_stop\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (UNLIKELY(enif_select(erl_nif_env_from_context(ctx), rsrc_obj->fd, ERL_NIF_SELECT_STOP, rsrc_obj, NULL, term_nil()) < 0)) { RAISE_ERROR(BADARG_ATOM); @@ -994,15 +997,14 @@ static term nif_socket_setopt(Context *ctx, int argc, term argv[]) TRACE("nif_socket_setopt\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { @@ -1082,15 +1084,14 @@ static term nif_socket_sockname(Context *ctx, int argc, term argv[]) TRACE("nif_socket_sockname\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { #elif OTP_SOCKET_LWIP @@ -1150,15 +1151,14 @@ static term nif_socket_peername(Context *ctx, int argc, term argv[]) TRACE("nif_socket_peername\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { @@ -1220,15 +1220,14 @@ static term nif_socket_bind(Context *ctx, int argc, term argv[]) TRACE("nif_socket_bind\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD TRACE("rsrc_obj->fd=%i\n", (int) rsrc_obj->fd); if (rsrc_obj->fd == 0) { @@ -1326,14 +1325,13 @@ static term nif_socket_listen(Context *ctx, int argc, term argv[]) GlobalContext *global = ctx->global; - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_integer); - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); @@ -1405,15 +1403,14 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) TRACE("nif_socket_accept\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); @@ -1506,6 +1503,146 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) // // recv/recvfrom // +#if OTP_SOCKET_LWIP +static size_t copy_pbuf_data(struct pbuf *src, size_t offset, size_t count, uint8_t *dst) +{ + size_t copied = 0; + while (count > 0 && src != NULL) { + if (offset > src->len) { + offset -= src->len; + src = src->next; + continue; + } + size_t chunk_count = MIN(count, src->len - offset); + memcpy(dst, ((const uint8_t *) src->payload) + offset, chunk_count); + count -= chunk_count; + copied += chunk_count; + dst += chunk_count; + src = src->next; + } + return copied; +} +#endif + +ssize_t socket_recv(struct SocketResource *rsrc_obj, uint8_t *buf, size_t len, int flags, term *from, Heap *heap) +{ +#if OTP_SOCKET_BSD + // + // receive data on the socket + // + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + ssize_t res; + if (from) { + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + GlobalContext *global = rsrc_refc->resource_type->global; + + res = recvfrom(rsrc_obj->fd, buf, len, flags, (struct sockaddr *) &addr, &addrlen); + + term address = inet_make_addr4(ntohl(addr.sin_addr.s_addr), heap); + term port_number = term_from_int(ntohs(addr.sin_port)); + + term map = term_alloc_map(2, heap); + term_set_map_assoc(map, 0, ADDR_ATOM, address); + term_set_map_assoc(map, 1, PORT_ATOM, port_number); + *from = map; + } else { + res = recv(rsrc_obj->fd, buf, len, flags); + } + if (res == 0) { + return SocketClosed; + } + if (res < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return SocketWouldBlock; + } + return SocketOtherError; + } + return res; +#elif OTP_SOCKET_LWIP + UNUSED(flags); + + uint32_t ip4_u32; + uint16_t port_u16; + + size_t remaining = len; + uint8_t *ptr = buf; + bool closed = false; + err_t err = ERR_OK; + // Use lwIP lock + LWIP_BEGIN(); + if (rsrc_obj->socket_state & SocketStateTCP) { + size_t pos = rsrc_obj->pos; + struct ListHead *item; + struct ListHead *tmp; + MUTABLE_LIST_FOR_EACH (item, tmp, &rsrc_obj->received_list) { + struct TCPReceivedItem *received_item = GET_LIST_ENTRY(item, struct TCPReceivedItem, list_head); + if (received_item->buf == NULL || received_item->err != ERR_OK) { + closed = received_item->buf == NULL; + err = received_item->err; + break; + } + if (pos < received_item->buf->tot_len) { + size_t copied = copy_pbuf_data(received_item->buf, pos, remaining, ptr); + ptr += copied; + remaining -= copied; + tcp_recved(rsrc_obj->tcp_pcb, copied); + if (copied + pos == received_item->buf->tot_len) { + // all data was copied. + list_remove(item); + pbuf_free(received_item->buf); + pos = 0; + } else { + pos = pos + copied; + } + if (remaining == 0) { + break; + } + } else { + pos -= received_item->buf->tot_len; + } + } + rsrc_obj->pos = pos; + if (from) { + ip4_u32 = ntohl(ip_addr_get_ip4_u32(&rsrc_obj->tcp_pcb->remote_ip)); + port_u16 = rsrc_obj->tcp_pcb->remote_port; + } + } else { + struct UDPReceivedItem *first_item = CONTAINER_OF(list_first(&rsrc_obj->received_list), struct UDPReceivedItem, list_head); + size_t copied = copy_pbuf_data(first_item->buf, 0, remaining, ptr); + remaining -= copied; + if (from) { + ip4_u32 = first_item->addr; + port_u16 = first_item->port; + } + list_remove(&first_item->list_head); + pbuf_free(first_item->buf); + free(first_item); + } + LWIP_END(); + if (remaining < len) { + if (from) { + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + GlobalContext *global = rsrc_refc->resource_type->global; + + term address = inet_make_addr4(ip4_u32, heap); + term port_number = term_from_int(port_u16); + + term map = term_alloc_map(2, heap); + term_set_map_assoc(map, 0, globalcontext_make_atom(global, addr_atom), address); + term_set_map_assoc(map, 1, PORT_ATOM, port_number); + + *from = map; + } + + return len - remaining; + } + if (closed) { + return SocketClosed; + } + return err == ERR_OK ? SocketWouldBlock : SocketOtherError; +#endif +} #if OTP_SOCKET_BSD static term nif_socket_recv_with_peek(Context *ctx, struct SocketResource *rsrc_obj, size_t len, bool is_recvfrom) @@ -1530,7 +1667,7 @@ static term nif_socket_recv_with_peek(Context *ctx, struct SocketResource *rsrc_ // {ok, Data :: binary()} // {ok, {Source :: #{addr => Address :: {0..255, 0..255, 0..255, 0..255}, port => Port :: non_neg_integer()}, Data :: binary()}} size_t ensure_packet_avail = term_binary_data_size_in_terms(buffer_size) + BINARY_HEADER_SIZE; - size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + TUPLE_SIZE(4) + term_map_size_in_terms(2)) : 0); + size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) : 0); if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); @@ -1538,25 +1675,18 @@ static term nif_socket_recv_with_peek(Context *ctx, struct SocketResource *rsrc_ } term data = term_create_uninitialized_binary(buffer_size, &ctx->heap, global); - const char *buffer = term_binary_data(data); + uint8_t *buffer = (uint8_t *) term_binary_data(data); // // receive data on the socket // - struct sockaddr_in addr; - socklen_t addrlen = sizeof(addr); - res = recvfrom(rsrc_obj->fd, (char *) buffer, buffer_size, flags, (struct sockaddr *) &addr, &addrlen); + term map = term_invalid_term(); + res = socket_recv(rsrc_obj, buffer, buffer_size, flags, is_recvfrom ? &map : NULL, &ctx->heap); TRACE("otp_socket.recv_handler: received data on fd: %i available=%lu, read=%lu\n", rsrc_obj->fd, (unsigned long) res, (unsigned long) buffer_size); - term payload = term_invalid_term(); + term payload; if (is_recvfrom) { - term address = inet_make_addr4(ntohl(addr.sin_addr.s_addr), &ctx->heap); - term port_number = term_from_int(ntohs(addr.sin_port)); - - term map = term_alloc_map(2, &ctx->heap); - term_set_map_assoc(map, 0, ADDR_ATOM, address); - term_set_map_assoc(map, 1, PORT_ATOM, port_number); term tuple = port_heap_create_tuple2(&ctx->heap, map, data); payload = port_heap_create_ok_tuple(&ctx->heap, tuple); } else { @@ -1575,7 +1705,7 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs // TODO plumb through buffer size size_t buffer_size = len == 0 ? DEFAULT_BUFFER_SIZE : len; - char *buffer = malloc(buffer_size); + uint8_t *buffer = malloc(buffer_size); term payload = term_invalid_term(); if (IS_NULL_PTR(buffer)) { @@ -1584,10 +1714,15 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs } else { - int flags = 0; - struct sockaddr_in addr; - socklen_t addrlen = sizeof(addr); - ssize_t res = recvfrom(rsrc_obj->fd, (char *) buffer, buffer_size, flags, (struct sockaddr *) &addr, &addrlen); + term map = term_invalid_term(); + if (is_recvfrom) { + if (UNLIKELY(memory_ensure_free(ctx, INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } + + ssize_t res = socket_recv(rsrc_obj, buffer, buffer_size, 0, is_recvfrom ? &map : NULL, &ctx->heap); if (res < 0) { @@ -1613,9 +1748,9 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs // {ok, Data :: binary()} // {ok, {Source :: #{addr => Address :: {0..255, 0..255, 0..255, 0..255}, port => Port :: non_neg_integer()}, Data :: binary()}} size_t ensure_packet_avail = term_binary_data_size_in_terms(len) + BINARY_HEADER_SIZE; - size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + TUPLE_SIZE(4) + term_map_size_in_terms(2)) : 0); + size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? TUPLE_SIZE(2) : 0); - if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, is_recvfrom ? 1 : 0, &map, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -1623,12 +1758,6 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs term data = term_from_literal_binary(buffer, len, &ctx->heap, global); if (is_recvfrom) { - term address = inet_make_addr4(ntohl(addr.sin_addr.s_addr), &ctx->heap); - term port_number = term_from_int(ntohs(addr.sin_port)); - - term map = term_alloc_map(2, &ctx->heap); - term_set_map_assoc(map, 0, ADDR_ATOM, address); - term_set_map_assoc(map, 1, PORT_ATOM, port_number); term tuple = port_heap_create_tuple2(&ctx->heap, map, data); payload = port_heap_create_ok_tuple(&ctx->heap, tuple); } else { @@ -1642,24 +1771,6 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs } #elif OTP_SOCKET_LWIP -static size_t copy_pbuf_data(struct pbuf *src, size_t offset, size_t count, uint8_t *dst) -{ - size_t copied = 0; - while (count > 0 && src != NULL) { - if (offset > src->len) { - offset -= src->len; - src = src->next; - continue; - } - size_t chunk_count = MIN(count, src->len - offset); - memcpy(dst, src->payload, chunk_count); - count -= chunk_count; - copied += chunk_count; - dst += chunk_count; - src = src->next; - } - return copied; -} static term nif_socket_recv_lwip(Context *ctx, struct SocketResource *rsrc_obj, size_t len, bool is_recvfrom) { @@ -1713,7 +1824,7 @@ static term nif_socket_recv_lwip(Context *ctx, struct SocketResource *rsrc_obj, } size_t ensure_packet_avail = term_binary_data_size_in_terms(len) + BINARY_HEADER_SIZE; - size_t requested_size = REF_SIZE + 2 * TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + term_map_size_in_terms(2)) : 0); + size_t requested_size = REF_SIZE + 2 * TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) : 0); if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -1723,66 +1834,14 @@ static term nif_socket_recv_lwip(Context *ctx, struct SocketResource *rsrc_obj, uint8_t *ptr = (uint8_t *) term_binary_data(data); size_t remaining = buffer_size; - uint32_t ip4_u32; - uint16_t port_u16; + term map = term_invalid_term(); - // Use lwIP lock - LWIP_BEGIN(); - if (rsrc_obj->socket_state & SocketStateTCP) { - size_t pos = rsrc_obj->pos; - struct ListHead *item; - struct ListHead *tmp; - MUTABLE_LIST_FOR_EACH (item, tmp, &rsrc_obj->received_list) { - struct TCPReceivedItem *received_item = GET_LIST_ENTRY(item, struct TCPReceivedItem, list_head); - if (received_item->buf == NULL || received_item->err != ERR_OK) { - break; - } - if (pos < received_item->buf->tot_len) { - size_t copied = copy_pbuf_data(received_item->buf, pos, remaining, ptr); - ptr += copied; - tcp_recved(rsrc_obj->tcp_pcb, copied); - if (copied + pos == received_item->buf->tot_len) { - // all data was copied. - list_remove(item); - pbuf_free(received_item->buf); - pos = 0; - } else { - pos = pos + copied; - } - if (remaining == 0) { - break; - } - } else { - pos -= received_item->buf->tot_len; - } - } - rsrc_obj->pos = pos; - if (is_recvfrom) { - ip4_u32 = ntohl(ip_addr_get_ip4_u32(&rsrc_obj->tcp_pcb->remote_ip)); - port_u16 = rsrc_obj->tcp_pcb->remote_port; - } - } else { - struct UDPReceivedItem *first_item = CONTAINER_OF(list_first(&rsrc_obj->received_list), struct UDPReceivedItem, list_head); - copy_pbuf_data(first_item->buf, 0, remaining, ptr); - if (is_recvfrom) { - ip4_u32 = first_item->addr; - port_u16 = first_item->port; - } - list_remove(&first_item->list_head); - pbuf_free(first_item->buf); - free(first_item); - } - LWIP_END(); + ssize_t result = socket_recv(rsrc_obj, ptr, remaining, 0, is_recvfrom ? &map : NULL, &ctx->heap); + UNUSED(result); term payload; if (is_recvfrom) { - term address = inet_make_addr4(ip4_u32, &ctx->heap); - term port_number = term_from_int(port_u16); - - term map = term_alloc_map(2, &ctx->heap); - term_set_map_assoc(map, 0, ADDR_ATOM, address); - term_set_map_assoc(map, 1, PORT_ATOM, port_number); term tuple = port_heap_create_tuple2(&ctx->heap, map, data); payload = port_heap_create_ok_tuple(&ctx->heap, tuple); } else { @@ -1795,7 +1854,7 @@ static term nif_socket_recv_lwip(Context *ctx, struct SocketResource *rsrc_obj, static term nif_socket_recv_internal(Context *ctx, term argv[], bool is_recvfrom) { - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_integer); avm_int_t len = term_to_int(argv[1]); // We raise badarg but return error tuples for POSIX errors @@ -1803,11 +1862,10 @@ static term nif_socket_recv_internal(Context *ctx, term argv[], bool is_recvfrom RAISE_ERROR(BADARG_ATOM); } - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { - return make_error_tuple(posix_errno_to_term(EINVAL, ctx->global), ctx); + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { + RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { return make_error_tuple(posix_errno_to_term(EBADF, ctx->global), ctx); @@ -1851,50 +1909,13 @@ static term nif_socket_recvfrom(Context *ctx, int argc, term argv[]) // // send/sendto // -static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool is_sendto) +ssize_t socket_send(struct SocketResource *rsrc_obj, const uint8_t *buf, size_t len, term dest) { - TRACE("nif_socket_send_internal\n"); - UNUSED(argc); - - VALIDATE_VALUE(argv[0], term_is_socket); - VALIDATE_VALUE(argv[1], term_is_binary); - - GlobalContext *global = ctx->global; - - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { - RAISE_ERROR(BADARG_ATOM); - } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; - -#if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); - } -#elif OTP_SOCKET_LWIP - if (rsrc_obj->socket_state == SocketStateClosed) { - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); - } - if (rsrc_obj->socket_state & SocketStateListening) { - return make_error_tuple(posix_errno_to_term(EOPNOTSUPP, global), ctx); - } -#endif - - term data = argv[1]; - term dest = term_invalid_term(); - if (is_sendto) { - dest = argv[2]; - } - - const char *buf = term_binary_data(data); - size_t len = term_binary_size(data); - ssize_t sent_data = -1; - #if OTP_SOCKET_BSD - // TODO make non-blocking - - if (is_sendto) { + if (!term_is_invalid_term(dest)) { + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + GlobalContext *global = rsrc_refc->resource_type->global; struct sockaddr_in destaddr; memset(&destaddr, 0, sizeof(destaddr)); @@ -1902,7 +1923,7 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i term port = interop_kv_get_value_default(dest, port_atom, term_from_int(0), global); destaddr.sin_port = htons(term_to_int(port)); - term addr = interop_kv_get_value(dest, addr_atom, ctx->global); + term addr = interop_kv_get_value(dest, addr_atom, global); if (globalcontext_is_term_equal_to_atom_string(global, addr, loopback_atom)) { destaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); } else { @@ -1914,20 +1935,31 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i } else { sent_data = send(rsrc_obj->fd, buf, len, 0); } - + if (sent_data == 0) { + return SocketClosed; + } + if (sent_data < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return SocketWouldBlock; + } + return SocketOtherError; + } + return sent_data; #elif OTP_SOCKET_LWIP err_t err; ip_addr_t ip_addr; uint16_t port_u16; - if (is_sendto) { + if (!term_is_invalid_term(dest)) { + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + GlobalContext *global = rsrc_refc->resource_type->global; term port_term = interop_kv_get_value_default(dest, port_atom, term_from_int(0), global); avm_int_t port_number = term_to_int(port_term); if (port_number < 0 || port_number > 65535) { - RAISE_ERROR(BADARG_ATOM); + return SocketOtherError; } port_u16 = (uint16_t) port_number; - term addr = interop_kv_get_value(dest, addr_atom, ctx->global); + term addr = interop_kv_get_value(dest, addr_atom, global); if (globalcontext_is_term_equal_to_atom_string(global, addr, loopback_atom)) { ip_addr_set_loopback(false, &ip_addr); } else { @@ -1938,9 +1970,9 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i LWIP_BEGIN(); if (rsrc_obj->socket_state & SocketStateUDP) { struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); - char *bytes = (char *) p->payload; + uint8_t *bytes = (uint8_t *) p->payload; memcpy(bytes, buf, len); - if (is_sendto) { + if (!term_is_invalid_term(dest)) { err = udp_sendto(rsrc_obj->udp_pcb, p, &ip_addr, port_u16); } else { err = udp_send(rsrc_obj->udp_pcb, p); @@ -1970,8 +2002,58 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i } } LWIP_END(); + if (err == ERR_CLSD) { + return SocketClosed; + } + if (sent_data == 0) { + return SocketWouldBlock; + } + if (err != ERR_OK) { + return SocketOtherError; + } + return sent_data; +#endif +} + +static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool is_sendto) +{ + TRACE("nif_socket_send_internal\n"); + UNUSED(argc); + VALIDATE_VALUE(argv[0], term_is_otp_socket); + VALIDATE_VALUE(argv[1], term_is_binary); + + GlobalContext *global = ctx->global; + + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { + RAISE_ERROR(BADARG_ATOM); + } + +#if OTP_SOCKET_BSD + if (rsrc_obj->fd == 0) { + return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + } +#elif OTP_SOCKET_LWIP + if (rsrc_obj->socket_state == SocketStateClosed) { + return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + } + if (rsrc_obj->socket_state & SocketStateListening) { + return make_error_tuple(posix_errno_to_term(EOPNOTSUPP, global), ctx); + } #endif + + term data = argv[1]; + term dest = term_invalid_term(); + if (is_sendto) { + dest = argv[2]; + } + + const uint8_t *buf = (const uint8_t *) term_binary_data(data); + size_t len = term_binary_size(data); + + ssize_t sent_data = socket_send(rsrc_obj, buf, len, dest); + // {ok, RestData} | {error, Reason} size_t rest_len = len - sent_data; @@ -2078,16 +2160,15 @@ static term nif_socket_connect(Context *ctx, int argc, term argv[]) TRACE("nif_socket_connect\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_map); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; term sockaddr = argv[1]; term port = interop_kv_get_value_default(sockaddr, port_atom, term_from_int(0), ctx->global); @@ -2181,16 +2262,15 @@ static term nif_socket_shutdown(Context *ctx, int argc, term argv[]) TRACE("nif_socket_shutdown\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_atom); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; int how; int val = interop_atom_term_select_int(otp_socket_shutdown_direction_table, argv[1], global); diff --git a/src/libAtomVM/otp_socket.h b/src/libAtomVM/otp_socket.h index 04d12e0c3..e42e3abd7 100644 --- a/src/libAtomVM/otp_socket.h +++ b/src/libAtomVM/otp_socket.h @@ -39,9 +39,59 @@ extern "C" { #endif #endif +enum SocketErrors +{ + SocketClosed = 0, + SocketWouldBlock = -1, + SocketOtherError = -2 +}; + +struct SocketResource; + const struct Nif *otp_socket_nif_get_nif(const char *nifname); void otp_socket_init(GlobalContext *global); +/** + * @brief Get the resource object associated with a socket term. + * + * @param socket_term the term with the socket + * @param otp_socket on output, the socket resource + * @return true in case of success + */ +bool term_to_otp_socket(term socket_term, struct SocketResource **otp_socket, Context *ctx); + +/** + * @brief Determine if a term is a socket term. + * + * @param socket_term the term to test + * @return true if it is a term + */ +bool term_is_otp_socket(term socket_term); + +/** + * @brief Send data to a socket (without blocking) + * + * @param otp_socket the socket resource + * @param buf buffer to send + * @param len number of bytes + * @param dest destination address or invalid term for sendto/send + * @return the number of written bytes or a value from SocketErrors + */ +ssize_t socket_send(struct SocketResource *socket, const uint8_t *buf, size_t len, term dest); + +/** + * @brief Read data from a socket. + * + * @param otp_socket the socket resource + * @param buf buffer to store data + * @param len number of bytes + * @param flags flags passed to recvfrom + * @param from filled with origin address using recvfrom (can be NULL) + * @param heap heap to build the origin address term (can be NULL if from is NULL) + * @return the number of read bytes or a value from SocketErrors + */ +ssize_t socket_recv(struct SocketResource *socket, uint8_t *buf, size_t len, int flags, term *from, Heap *heap); + #if OTP_SOCKET_LWIP struct LWIPEvent { diff --git a/src/libAtomVM/otp_ssl.c b/src/libAtomVM/otp_ssl.c new file mode 100644 index 000000000..b2a485386 --- /dev/null +++ b/src/libAtomVM/otp_ssl.c @@ -0,0 +1,795 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Paul Guyot + * + * 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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +// #define ENABLE_TRACE +#include + +#ifndef MBEDTLS_PRIVATE +#define MBEDTLS_PRIVATE(member) member +#endif + +// Default read buffer if mbedtls_ssl_get_max_in_record_payload fails +#define DEFAULT_READ_BUFFER_FALLBACK 512 + +#if defined(MBEDTLS_DEBUG_C) && defined(ENABLE_TRACE) + +#include + +static void mbedtls_debug_cb(void *ctx, int level, const char *filename, int line, const char *msg) +{ + UNUSED(ctx); + UNUSED(level); + + TRACE("%s:%d: %s", filename, line, msg); +} + +#endif + +// +// Resources +// + +struct EntropyContextResource +{ + mbedtls_entropy_context context; +}; + +struct CtrDrbgResource +{ + mbedtls_ctr_drbg_context context; +}; + +struct SSLContextResource +{ + mbedtls_ssl_context context; +}; + +struct SSLConfigResource +{ + mbedtls_ssl_config config; +}; + +static void entropycontext_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct EntropyContextResource *rsrc_obj = (struct EntropyContextResource *) obj; + mbedtls_entropy_free(&rsrc_obj->context); +} + +static void ctrdrbg_dtor(ErlNifEnv *caller_env, void *obj) +{ + TRACE("%s\n", __func__); + UNUSED(caller_env); + + struct CtrDrbgResource *rsrc_obj = (struct CtrDrbgResource *) obj; + // Release the entropy + mbedtls_entropy_context *entropy_context = rsrc_obj->context.MBEDTLS_PRIVATE(p_entropy); + if (entropy_context) { + struct EntropyContextResource *entropy_obj = CONTAINER_OF(entropy_context, struct EntropyContextResource, context); + struct RefcBinary *entropy_refc = refc_binary_from_data(entropy_obj); + refc_binary_decrement_refcount(entropy_refc, caller_env->global); + } + mbedtls_ctr_drbg_free(&rsrc_obj->context); +} + +static void sslcontext_dtor(ErlNifEnv *caller_env, void *obj) +{ + TRACE("%s\n", __func__); + UNUSED(caller_env); + + struct SSLContextResource *rsrc_obj = (struct SSLContextResource *) obj; + // Release the config + const mbedtls_ssl_config *config = rsrc_obj->context.MBEDTLS_PRIVATE(conf); + if (config) { + struct SSLConfigResource *config_obj = CONTAINER_OF(config, struct SSLConfigResource, config); + struct RefcBinary *config_refc = refc_binary_from_data(config_obj); + refc_binary_decrement_refcount(config_refc, caller_env->global); + } + mbedtls_ssl_free(&rsrc_obj->context); +} + +static void sslconfig_dtor(ErlNifEnv *caller_env, void *obj) +{ + TRACE("%s\n", __func__); + UNUSED(caller_env); + + struct SSLConfigResource *rsrc_obj = (struct SSLConfigResource *) obj; + mbedtls_ssl_config_free(&rsrc_obj->config); +} + +static const ErlNifResourceTypeInit EntropyContextResourceTypeInit = { + .members = 1, + .dtor = entropycontext_dtor, +}; +static const ErlNifResourceTypeInit CtrDrbgResourceTypeInit = { + .members = 1, + .dtor = ctrdrbg_dtor, +}; +static const ErlNifResourceTypeInit SSLContextResourceTypeInit = { + .members = 1, + .dtor = sslcontext_dtor, +}; +static const ErlNifResourceTypeInit SSLConfigResourceTypeInit = { + .members = 1, + .dtor = sslconfig_dtor, +}; + +static ErlNifResourceType *entropycontext_resource_type; +static ErlNifResourceType *ctrdrbg_resource_type; +static ErlNifResourceType *sslcontext_resource_type; +static ErlNifResourceType *sslconfig_resource_type; + +// +// Interface with sockets +// +int mbedtls_ssl_send_cb(void *ctx, const unsigned char *buf, size_t len) +{ + TRACE("%s\n", __func__); + ssize_t res = socket_send((struct SocketResource *) ctx, buf, len, term_invalid_term()); + if (res == SocketWouldBlock) { + return MBEDTLS_ERR_SSL_WANT_WRITE; + } + return res; +} + +int mbedtls_ssl_recv_cb(void *ctx, unsigned char *buf, size_t len) +{ + TRACE("%s\n", __func__); + ssize_t res = socket_recv((struct SocketResource *) ctx, buf, len, 0, NULL, NULL); + if (res == SocketWouldBlock) { + return MBEDTLS_ERR_SSL_WANT_READ; + } + return res; +} + +// +// Interop +// + +#define UNKNOWN_TABLE_VALUE -1 + +static const AtomStringIntPair endpoint_table[] = { + { ATOM_STR("\x6", "client"), MBEDTLS_SSL_IS_CLIENT }, + { ATOM_STR("\x6", "server"), MBEDTLS_SSL_IS_SERVER }, + SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +}; + +static const AtomStringIntPair authmode_table[] = { + { ATOM_STR("\x4", "none"), MBEDTLS_SSL_VERIFY_NONE }, + { ATOM_STR("\x8", "optional"), MBEDTLS_SSL_VERIFY_OPTIONAL }, + { ATOM_STR("\x8", "required"), MBEDTLS_SSL_VERIFY_REQUIRED }, + SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +}; + +// +// Nifs +// + +static term nif_ssl_entropy_init(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + struct EntropyContextResource *rsrc_obj = enif_alloc_resource(entropycontext_resource_type, sizeof(struct EntropyContextResource)); + if (IS_NULL_PTR(rsrc_obj)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + mbedtls_entropy_init(&rsrc_obj->context); + + return obj; +} + +static term nif_ssl_ctr_drbg_init(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + struct CtrDrbgResource *rsrc_obj = enif_alloc_resource(ctrdrbg_resource_type, sizeof(struct CtrDrbgResource)); + if (IS_NULL_PTR(rsrc_obj)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + mbedtls_ctr_drbg_init(&rsrc_obj->context); + + return obj; +} + +static term nif_ssl_ctr_drbg_seed(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + VALIDATE_VALUE(argv[2], term_is_binary); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], ctrdrbg_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct CtrDrbgResource *ctrdrbg_obj = (struct CtrDrbgResource *) rsrc_obj_ptr; + + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[1], entropycontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct EntropyContextResource *entropy_obj = (struct EntropyContextResource *) rsrc_obj_ptr; + + int err = mbedtls_ctr_drbg_seed(&ctrdrbg_obj->context, mbedtls_entropy_func, &entropy_obj->context, (const unsigned char *) term_binary_data(argv[2]), term_binary_size(argv[2])); + if (UNLIKELY(err)) { + RAISE_ERROR(BADARG_ATOM); + } + + struct RefcBinary *entropy_refc = refc_binary_from_data(entropy_obj); + refc_binary_increment_refcount(entropy_refc); + + return OK_ATOM; +} + +static term nif_ssl_init(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + struct SSLContextResource *rsrc_obj = enif_alloc_resource(sslcontext_resource_type, sizeof(struct SSLContextResource)); + if (IS_NULL_PTR(rsrc_obj)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + mbedtls_ssl_init(&rsrc_obj->context); + + return obj; +} + +static term nif_ssl_set_bio(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + VALIDATE_VALUE(argv[1], term_is_otp_socket); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *rsrc_obj = (struct SSLContextResource *) rsrc_obj_ptr; + + struct SocketResource *socket_resource; + if (UNLIKELY(!term_to_otp_socket(argv[1], &socket_resource, ctx))) { + RAISE_ERROR(BADARG_ATOM); + } + + mbedtls_ssl_set_bio(&rsrc_obj->context, socket_resource, mbedtls_ssl_send_cb, mbedtls_ssl_recv_cb, NULL); + + return OK_ATOM; +} + +static term nif_ssl_config_init(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + struct SSLConfigResource *rsrc_obj = enif_alloc_resource(sslconfig_resource_type, sizeof(struct SSLConfigResource)); + if (IS_NULL_PTR(rsrc_obj)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + mbedtls_ssl_config_init(&rsrc_obj->config); + +#if defined(MBEDTLS_DEBUG_C) && defined(ENABLE_TRACE) + mbedtls_ssl_conf_dbg(&rsrc_obj->config, mbedtls_debug_cb, NULL); +#endif + + return obj; +} + +static term nif_ssl_config_defaults(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + VALIDATE_VALUE(argv[1], term_is_atom); + VALIDATE_VALUE(argv[2], term_is_atom); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslconfig_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLConfigResource *rsrc_obj = (struct SSLConfigResource *) rsrc_obj_ptr; + + int endpoint = interop_atom_term_select_int(endpoint_table, argv[1], ctx->global); + if (UNLIKELY(endpoint == UNKNOWN_TABLE_VALUE)) { + RAISE_ERROR(BADARG_ATOM); + } + + enum inet_type transport_type = inet_atom_to_type(argv[2], ctx->global); + if (UNLIKELY(transport_type != InetStreamType && transport_type != InetDgramType)) { + RAISE_ERROR(BADARG_ATOM); + } + int transport = transport_type == InetStreamType ? MBEDTLS_SSL_TRANSPORT_STREAM : MBEDTLS_SSL_TRANSPORT_DATAGRAM; + + int err = mbedtls_ssl_config_defaults(&rsrc_obj->config, endpoint, transport, MBEDTLS_SSL_PRESET_DEFAULT); + if (UNLIKELY(err != 0)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + return OK_ATOM; +} + +static term nif_ssl_set_hostname(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *rsrc_obj = (struct SSLContextResource *) rsrc_obj_ptr; + + int ok; + char *host_str = interop_term_to_string(argv[1], &ok); + if (!ok) { + RAISE_ERROR(BADARG_ATOM); + } + + int err = mbedtls_ssl_set_hostname(&rsrc_obj->context, host_str); + free(host_str); + + if (UNLIKELY(err == MBEDTLS_ERR_SSL_ALLOC_FAILED)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + if (UNLIKELY(err)) { // MBEDTLS_ERR_SSL_BAD_INPUT_DATA or any undocumented error + RAISE_ERROR(BADARG_ATOM); + } + + return OK_ATOM; +} + +static term nif_ssl_conf_authmode(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslconfig_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLConfigResource *rsrc_obj = (struct SSLConfigResource *) rsrc_obj_ptr; + + int authmode = interop_atom_term_select_int(authmode_table, argv[1], ctx->global); + if (UNLIKELY(authmode == UNKNOWN_TABLE_VALUE)) { + RAISE_ERROR(BADARG_ATOM); + } + + mbedtls_ssl_conf_authmode(&rsrc_obj->config, authmode); + + return OK_ATOM; +} + +static term nif_ssl_conf_rng(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslconfig_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLConfigResource *conf_obj = (struct SSLConfigResource *) rsrc_obj_ptr; + + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[1], ctrdrbg_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct CtrDrbgResource *ctr_drbg_obj = (struct CtrDrbgResource *) rsrc_obj_ptr; + + mbedtls_ssl_conf_rng(&conf_obj->config, mbedtls_ctr_drbg_random, &ctr_drbg_obj->context); + + return OK_ATOM; +} + +static term nif_ssl_setup(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[1], sslconfig_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLConfigResource *config_rsrc = (struct SSLConfigResource *) rsrc_obj_ptr; + + int err = mbedtls_ssl_setup(&context_rsrc->context, &config_rsrc->config); + if (UNLIKELY(err == MBEDTLS_ERR_SSL_ALLOC_FAILED)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + if (UNLIKELY(err)) { // Any undocumented error + RAISE_ERROR(BADARG_ATOM); + } + + struct RefcBinary *config_refc = refc_binary_from_data(config_rsrc); + refc_binary_increment_refcount(config_refc); + + return OK_ATOM; +} + +static term make_err_result(int err, Context *ctx) +{ + switch (err) { + case 0: + return OK_ATOM; + case MBEDTLS_ERR_SSL_WANT_READ: + return globalcontext_make_atom(ctx->global, ATOM_STR("\x9", "want_read")); + case MBEDTLS_ERR_SSL_WANT_WRITE: + return globalcontext_make_atom(ctx->global, ATOM_STR("\xA", "want_write")); +#if MBEDTLS_VERSION_NUMBER >= 0x020B0000 + case MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS: + return globalcontext_make_atom(ctx->global, ATOM_STR("\xA", "async_in_progress")); +#if MBEDTLS_VERSION_NUMBER >= 0x020E0000 + case MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS: + return globalcontext_make_atom(ctx->global, ATOM_STR("\xA", "crypto_in_progress")); +#endif +#endif + default: { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + } +} + +static term nif_ssl_handshake_step(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + int err = mbedtls_ssl_handshake_step(&context_rsrc->context); + +#if MBEDTLS_VERSION_NUMBER >= 0x03020000 + if (err == 0 && mbedtls_ssl_is_handshake_over(&context_rsrc->context)) { + return globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "done")); + } +#else + if (err == 0 && context_rsrc->context.state >= MBEDTLS_SSL_HANDSHAKE_OVER) { + return globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "done")); + } +#endif + return make_err_result(err, ctx); +} + +static term nif_ssl_close_notify(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + int err = mbedtls_ssl_close_notify(&context_rsrc->context); + return make_err_result(err, ctx); +} + +static term nif_ssl_write(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + VALIDATE_VALUE(argv[1], term_is_binary); + + term data = argv[1]; + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + const uint8_t *buffer = (const uint8_t *) term_binary_data(data); + size_t len = term_binary_size(data); + + int res = mbedtls_ssl_write(&context_rsrc->context, buffer, len); + + if (res == (int) len) { + return OK_ATOM; + } + if (LIKELY(res >= 0)) { // ensure we don't return OK if res is 0 + size_t rest_len = len - res; + size_t requested_size = term_sub_binary_heap_size(data, rest_len); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) + requested_size, 1, &data, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term rest = term_maybe_create_sub_binary(data, res, rest_len, &ctx->heap, ctx->global); + return port_create_tuple2(ctx, OK_ATOM, rest); + } + + return make_err_result(res, ctx); +} + +static term nif_ssl_read(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + VALIDATE_VALUE(argv[1], term_is_integer); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + avm_int_t len = term_to_int(argv[1]); + if (len < 0) { + RAISE_ERROR(BADARG_ATOM); + } +#if MBEDTLS_VERSION_NUMBER >= 0x03000000 + if (len == 0) { + len = mbedtls_ssl_get_max_in_record_payload(&context_rsrc->context); + } +#endif + if (len <= 0) { + len = DEFAULT_READ_BUFFER_FALLBACK; + } + size_t ensure_packet_avail = term_binary_data_size_in_terms(len) + BINARY_HEADER_SIZE; + size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail; + + if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term data = term_create_uninitialized_binary(len, &ctx->heap, ctx->global); + uint8_t *buffer = (uint8_t *) term_binary_data(data); + + int res = mbedtls_ssl_read(&context_rsrc->context, buffer, len); + + if (res == len) { + return port_create_tuple2(ctx, OK_ATOM, data); + } + + if (res >= 0 && res < len) { + size_t requested_size = term_sub_binary_heap_size(data, res); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) + requested_size, 1, &data, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term rest = term_maybe_create_sub_binary(data, 0, res, &ctx->heap, ctx->global); + return port_create_tuple2(ctx, OK_ATOM, rest); + } + + return make_err_result(res, ctx); +} + +static const struct Nif ssl_entropy_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_entropy_init +}; +static const struct Nif ssl_ctr_drbg_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_ctr_drbg_init +}; +static const struct Nif ssl_ctr_drbg_seed_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_ctr_drbg_seed +}; +static const struct Nif ssl_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_init +}; +static const struct Nif ssl_set_bio_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_set_bio +}; +static const struct Nif ssl_config_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_config_init +}; +static const struct Nif ssl_config_defaults_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_config_defaults +}; +static const struct Nif ssl_conf_authmode_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_conf_authmode +}; +static const struct Nif ssl_conf_rng_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_conf_rng +}; +static const struct Nif ssl_set_hostname_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_set_hostname +}; +static const struct Nif ssl_setup_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_setup +}; +static const struct Nif ssl_handshake_step_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_handshake_step +}; +static const struct Nif ssl_close_notify_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_close_notify +}; +static const struct Nif ssl_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_write +}; +static const struct Nif ssl_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_read +}; + +// +// Entrypoints +// + +const struct Nif *otp_ssl_nif_get_nif(const char *nifname) +{ + if (strncmp("ssl:", nifname, 4) == 0) { + const char *rest = nifname + 4; + if (strcmp("nif_entropy_init/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_entropy_init_nif; + } + if (strcmp("nif_ctr_drbg_init/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_ctr_drbg_init_nif; + } + if (strcmp("nif_ctr_drbg_seed/3", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_ctr_drbg_seed_nif; + } + if (strcmp("nif_init/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_init_nif; + } + if (strcmp("nif_set_bio/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_set_bio_nif; + } + if (strcmp("nif_config_init/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_config_init_nif; + } + if (strcmp("nif_config_defaults/3", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_config_defaults_nif; + } + if (strcmp("nif_conf_authmode/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_conf_authmode_nif; + } + if (strcmp("nif_conf_rng/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_conf_rng_nif; + } + if (strcmp("nif_set_hostname/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_set_hostname_nif; + } + if (strcmp("nif_setup/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_setup_nif; + } + if (strcmp("nif_handshake_step/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_handshake_step_nif; + } + if (strcmp("nif_close_notify/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_close_notify_nif; + } + if (strcmp("nif_write/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_write_nif; + } + if (strcmp("nif_read/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_read_nif; + } + } + return NULL; +} + +void otp_ssl_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + entropycontext_resource_type = enif_init_resource_type(&env, "entropycontext", &EntropyContextResourceTypeInit, ERL_NIF_RT_CREATE, NULL); + ctrdrbg_resource_type = enif_init_resource_type(&env, "ctr_drbg", &CtrDrbgResourceTypeInit, ERL_NIF_RT_CREATE, NULL); + sslcontext_resource_type = enif_init_resource_type(&env, "sslcontext", &SSLContextResourceTypeInit, ERL_NIF_RT_CREATE, NULL); + sslconfig_resource_type = enif_init_resource_type(&env, "sslconfig", &SSLConfigResourceTypeInit, ERL_NIF_RT_CREATE, NULL); + +#if defined(MBEDTLS_DEBUG_C) && defined(ENABLE_TRACE) + mbedtls_debug_set_threshold(5); +#endif +} diff --git a/src/libAtomVM/otp_ssl.h b/src/libAtomVM/otp_ssl.h new file mode 100644 index 000000000..25ff7617a --- /dev/null +++ b/src/libAtomVM/otp_ssl.h @@ -0,0 +1,38 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Paul Guyot + * + * 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 + */ + +#ifndef _OTP_SSL_H_ +#define _OTP_SSL_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +const struct Nif *otp_ssl_nif_get_nif(const char *nifname); +void otp_ssl_init(GlobalContext *global); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt index d5063dc3a..72b5dd7fa 100644 --- a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt @@ -30,6 +30,7 @@ set(AVM_BUILTIN_COMPONENT_SRCS "uart_driver.c" "otp_net_platform.c" "otp_socket_platform.c" + "otp_ssl_platform.c" ) if (IDF_VERSION_MAJOR GREATER_EQUAL 5) @@ -38,6 +39,10 @@ else() set(ADDITIONAL_PRIV_REQUIRES "") endif() +if(CONFIG_AVM_ENABLE_OTP_SSL_NIFS) + set(ADDITIONAL_PRIV_REQUIRES ${ADDITIONAL_PRIV_REQUIRES} "mbedtls") +endif() + # WHOLE_ARCHIVE option is supported only with esp-idf 5.x # A link option will be used with esp-idf 4.x if (IDF_VERSION_MAJOR EQUAL 5) diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 94c4c7f00..61c164090 100644 --- a/src/platforms/esp32/components/avm_builtins/Kconfig +++ b/src/platforms/esp32/components/avm_builtins/Kconfig @@ -74,4 +74,8 @@ config AVM_ENABLE_OTP_NET_NIFS bool "Enable OTP Net NIFs" default y +config AVM_ENABLE_OTP_SSL_NIFS + bool "Enable OTP SSL NIFs" + default y + endmenu diff --git a/src/platforms/esp32/components/avm_builtins/otp_ssl_platform.c b/src/platforms/esp32/components/avm_builtins/otp_ssl_platform.c new file mode 100644 index 000000000..a9bc7f8ab --- /dev/null +++ b/src/platforms/esp32/components/avm_builtins/otp_ssl_platform.c @@ -0,0 +1,31 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Paul Guyot + * + * 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 + */ + +#include +#include +#include +#include + + +#ifdef CONFIG_AVM_ENABLE_OTP_SSL_NIFS + +REGISTER_NIF_COLLECTION(otp_ssl, otp_ssl_init, NULL, otp_ssl_nif_get_nif) + +#endif diff --git a/src/platforms/esp32/components/avm_sys/CMakeLists.txt b/src/platforms/esp32/components/avm_sys/CMakeLists.txt index 240fa466c..b9030be34 100644 --- a/src/platforms/esp32/components/avm_sys/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_sys/CMakeLists.txt @@ -28,6 +28,7 @@ set(AVM_SYS_COMPONENT_SRCS "../../../../libAtomVM/inet.c" "../../../../libAtomVM/otp_net.c" "../../../../libAtomVM/otp_socket.c" + "../../../../libAtomVM/otp_ssl.c" ) if (IDF_VERSION_MAJOR GREATER_EQUAL 5) diff --git a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt index d82c488ce..fb907ded1 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -47,6 +47,7 @@ compile_erlang(test_net) compile_erlang(test_rtc_slow) compile_erlang(test_select) compile_erlang(test_socket) +compile_erlang(test_ssl) compile_erlang(test_time_and_processes) compile_erlang(test_tz) @@ -64,6 +65,7 @@ add_custom_command( test_rtc_slow.beam test_select.beam test_socket.beam + test_ssl.beam test_time_and_processes.beam test_tz.beam DEPENDS @@ -78,6 +80,7 @@ add_custom_command( "${CMAKE_CURRENT_BINARY_DIR}/test_rtc_slow.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_select.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_socket.beam" + "${CMAKE_CURRENT_BINARY_DIR}/test_ssl.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_time_and_processes.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_tz.beam" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} diff --git a/src/platforms/esp32/test/main/test_erl_sources/test_ssl.erl b/src/platforms/esp32/test/main/test_erl_sources/test_ssl.erl new file mode 100644 index 000000000..15f7a835e --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_ssl.erl @@ -0,0 +1,162 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% 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_ssl). +-export([start/0]). + +start() -> + % start SSL + Entropy = ssl:nif_entropy_init(), + CtrDrbg = ssl:nif_ctr_drbg_init(), + ok = ssl:nif_ctr_drbg_seed(CtrDrbg, Entropy, <<"AtomVM">>), + % Get address of github.com + {ok, Results} = net:getaddrinfo_nif("github.com", undefined), + [TCPAddr | _] = [ + Addr + || #{addr := #{addr := Addr}, type := stream, protocol := tcp, family := inet} <- Results + ], + % Connect to github.com:443 + {ok, Socket} = socket:open(inet, stream, tcp), + ok = socket:connect(Socket, #{family => inet, addr => TCPAddr, port => 443}), + % Initialize SSL Socket and config + SSLContext = ssl:nif_init(), + ok = ssl:nif_set_bio(SSLContext, Socket), + SSLConfig = ssl:nif_config_init(), + ok = ssl:nif_config_defaults(SSLConfig, client, stream), + ok = ssl:nif_set_hostname(SSLContext, "github.com"), + ok = ssl:nif_conf_authmode(SSLConfig, none), + ok = ssl:nif_conf_rng(SSLConfig, CtrDrbg), + ok = ssl:nif_setup(SSLContext, SSLConfig), + % Handshake + ok = handshake_loop(SSLContext, Socket), + % Write + ok = send_loop( + SSLContext, + Socket, + <<"GET / HTTP/1.1\r\nHost: atomvm.net\r\nUser-Agent: AtomVM within qemu\r\n\r\n">> + ), + % Read + {ok, <<"HTTP/1.1">>} = recv_loop(SSLContext, Socket, 8, []), + % Close + ok = close_notify_loop(SSLContext, Socket), + ok = socket:close(Socket), + ok. + +handshake_loop(SSLContext, Socket) -> + case ssl:nif_handshake_step(SSLContext) of + ok -> + handshake_loop(SSLContext, Socket); + done -> + ok; + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + handshake_loop(SSLContext, Socket); + {closed, Ref} -> + ok = socket:close(Socket), + {error, closed} + end; + {error, _Reason} = Error -> + socket:close(Socket), + Error + end; + want_write -> + handshake_loop(SSLContext, Socket); + {error, _Reason} = Error -> + socket:close(Socket), + Error + end. + +send_loop(SSLContext, Socket, Binary) -> + case ssl:nif_write(SSLContext, Binary) of + ok -> + ok; + {ok, Rest} -> + send_loop(SSLContext, Socket, Rest); + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + send_loop(SSLContext, Socket, Binary); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + send_loop(SSLContext, Socket, Binary); + {error, _Reason} = Error -> + Error + end. + +recv_loop(_SSLContext, _Socket, 0, Acc) -> + {ok, list_to_binary(lists:reverse(Acc))}; +recv_loop(SSLContext, Socket, Remaining, Acc) -> + case ssl:nif_read(SSLContext, Remaining) of + {ok, Data} -> + Len = byte_size(Data), + recv_loop(SSLContext, Socket, Remaining - Len, [Data | Acc]); + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + recv_loop(SSLContext, Socket, Remaining, Acc); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + recv_loop(SSLContext, Socket, Remaining, Acc); + {error, _Reason} = Error -> + Error + end. + +close_notify_loop(SSLContext, Socket) -> + case ssl:nif_close_notify(SSLContext) of + ok -> + ok; + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + close_notify_loop(SSLContext, Socket); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + close_notify_loop(SSLContext, Socket); + {error, _Reason} = Error -> + Error + end. diff --git a/src/platforms/esp32/test/main/test_main.c b/src/platforms/esp32/test/main/test_main.c index 4455ee729..7bedbcded 100644 --- a/src/platforms/esp32/test/main/test_main.c +++ b/src/platforms/esp32/test/main/test_main.c @@ -161,6 +161,8 @@ term avm_test_case(const char *test_module) term_display(stdout, ret_value, ctx); fprintf(stdout, "\n"); + context_destroy(ctx); + nif_collection_destroy_all(glb); port_driver_destroy_all(glb); @@ -462,6 +464,26 @@ TEST_CASE("test_socket", "[test_run]") eth_stop(eth_netif); } +TEST_CASE("test_ssl", "[test_run]") +{ + // esp_netif_init() was called by network_driver_init + ESP_LOGI(TAG, "Registering handler\n"); + network_got_ip = false; + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL)); + ESP_LOGI(TAG, "Starting network\n"); + esp_netif_t *eth_netif = eth_start(); + + while (!network_got_ip) { + vTaskDelay(1); + } + + term ret_value = avm_test_case("test_ssl.beam"); + TEST_ASSERT(ret_value == OK_ATOM); + + ESP_LOGI(TAG, "Stopping network\n"); + eth_stop(eth_netif); +} + TEST_CASE("test_rtc_slow", "[test_run]") { term ret_value = avm_test_case("test_rtc_slow.beam"); diff --git a/src/platforms/generic_unix/lib/CMakeLists.txt b/src/platforms/generic_unix/lib/CMakeLists.txt index 8fba09b36..72525665d 100644 --- a/src/platforms/generic_unix/lib/CMakeLists.txt +++ b/src/platforms/generic_unix/lib/CMakeLists.txt @@ -61,6 +61,20 @@ define_if_function_exists(libAtomVM${PLATFORM_LIB_SUFFIX} getservbyname "netdb.h target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC libAtomVM) include_directories(${CMAKE_SOURCE_DIR}/src/platforms/generic_unix/lib) +include(MbedTLS) +if (MbedTLS_FOUND) + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC MbedTLS::mbedtls) + target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ATOMVM_HAS_MBEDTLS) + target_sources(libAtomVM${PLATFORM_LIB_SUFFIX} + PRIVATE + ../../../libAtomVM/otp_ssl.c + ../../../libAtomVM/otp_ssl.h + ) +else() + message("WARNING: Could NOT find MbedTLS, SSL will not be supported. Install MbedTLS 3.x or try to set MBEDTLS_ROOT_DIR to installation prefix of MbedTLS 2.x") +endif() + +# For now we still use OpenSSL for random and crypto find_package(OpenSSL) if (${OPENSSL_FOUND} STREQUAL TRUE) target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${OPENSSL_INCLUDE_DIR}) diff --git a/src/platforms/generic_unix/lib/platform_nifs.c b/src/platforms/generic_unix/lib/platform_nifs.c index 3f2307254..ab9279435 100644 --- a/src/platforms/generic_unix/lib/platform_nifs.c +++ b/src/platforms/generic_unix/lib/platform_nifs.c @@ -27,6 +27,7 @@ #include "nifs.h" #include "otp_net.h" #include "otp_socket.h" +#include "otp_ssl.h" #include "platform_defaultatoms.h" #include "term.h" #include @@ -263,9 +264,15 @@ const struct Nif *platform_nifs_get_nif(const char *nifname) return &atomvm_platform_nif; } const struct Nif *nif = otp_net_nif_get_nif(nifname); - if (nif == NULL) { - return otp_socket_nif_get_nif(nifname); - } else { + if (nif) { return nif; } + nif = otp_socket_nif_get_nif(nifname); +#if defined ATOMVM_HAS_MBEDTLS + if (nif) { + return nif; + } + nif = otp_ssl_nif_get_nif(nifname); +#endif + return nif; } diff --git a/src/platforms/generic_unix/lib/sys.c b/src/platforms/generic_unix/lib/sys.c index 85f230d55..3a817c249 100644 --- a/src/platforms/generic_unix/lib/sys.c +++ b/src/platforms/generic_unix/lib/sys.c @@ -31,6 +31,10 @@ #include "smp.h" #include "utils.h" +#if ATOMVM_HAS_MBEDTLS +#include "otp_ssl.h" +#endif + #include #include #include @@ -562,6 +566,9 @@ void sys_init_platform(GlobalContext *global) otp_net_init(global); otp_socket_init(global); +#if ATOMVM_HAS_MBEDTLS + otp_ssl_init(global); +#endif global->platform_data = platform; } diff --git a/src/platforms/rp2040/src/lib/CMakeLists.txt b/src/platforms/rp2040/src/lib/CMakeLists.txt index 2fa764dc9..2ddc275d1 100644 --- a/src/platforms/rp2040/src/lib/CMakeLists.txt +++ b/src/platforms/rp2040/src/lib/CMakeLists.txt @@ -83,13 +83,16 @@ if (PICO_CYW43_SUPPORTED) ../../../../libAtomVM/inet.c otp_socket_platform.c ../../../../libAtomVM/otp_socket.c + otp_ssl_platform.c + ../../../../libAtomVM/otp_ssl.c ../../../../libAtomVM/inet.h otp_socket_platform.h ../../../../libAtomVM/otp_socket.h + ../../../../libAtomVM/otp_ssl.h otp_net_lwip_raw.c otp_net_lwip_raw.h) - target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC pico_cyw43_arch_lwip_threadsafe_background pico_lwip_sntp INTERFACE pan_lwip_dhserver) - target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,networkregister_port_driver -Wl,-u -Wl,otp_socket_nif -Wl,-u -Wl,otp_net_nif") + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC pico_cyw43_arch_lwip_threadsafe_background pico_lwip_sntp pico_mbedtls INTERFACE pan_lwip_dhserver) + target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,networkregister_port_driver -Wl,-u -Wl,otp_socket_nif -Wl,-u -Wl,otp_net_nif -Wl,-u -Wl,otp_ssl_nif") endif() target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif") diff --git a/src/platforms/rp2040/src/lib/mbedtls_config.h b/src/platforms/rp2040/src/lib/mbedtls_config.h new file mode 100644 index 000000000..413dbbd2a --- /dev/null +++ b/src/platforms/rp2040/src/lib/mbedtls_config.h @@ -0,0 +1,93 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 Paul Guyot + * + * 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 + */ + +/* Workaround for some mbedtls source files using INT_MAX without including limits.h */ +#include + +// Reasonable config copied from pico samples + +// Protocols +#define MBEDTLS_SSL_PROTO_TLS1_2 + +// Options that enable ciphersuites +#define MBEDTLS_CIPHER_MODE_CBC +#define MBEDTLS_ECP_DP_SECP192R1_ENABLED +#define MBEDTLS_ECP_DP_SECP224R1_ENABLED +#define MBEDTLS_ECP_DP_SECP256R1_ENABLED +#define MBEDTLS_ECP_DP_SECP384R1_ENABLED +#define MBEDTLS_ECP_DP_SECP521R1_ENABLED +#define MBEDTLS_ECP_DP_SECP192K1_ENABLED +#define MBEDTLS_ECP_DP_SECP224K1_ENABLED +#define MBEDTLS_ECP_DP_SECP256K1_ENABLED +#define MBEDTLS_ECP_DP_BP256R1_ENABLED +#define MBEDTLS_ECP_DP_BP384R1_ENABLED +#define MBEDTLS_ECP_DP_BP512R1_ENABLED +#define MBEDTLS_ECP_DP_CURVE25519_ENABLED +// Following is unused until pico's mbedtls is upgraded +#define MBEDTLS_ECP_DP_CURVE448_ENABLED + +#define MBEDTLS_KEY_EXCHANGE_RSA_ENABLED +#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED +#define MBEDTLS_AES_C + +// Requirements +#define MBEDTLS_PKCS1_V15 +#define MBEDTLS_ASN1_PARSE_C +#define MBEDTLS_BIGNUM_C +#define MBEDTLS_CIPHER_C +#define MBEDTLS_CTR_DRBG_C +#define MBEDTLS_ENTROPY_C +#define MBEDTLS_MD_C +#define MBEDTLS_MD5_C +#define MBEDTLS_OID_C +#define MBEDTLS_PKCS5_C +#define MBEDTLS_PK_C +#define MBEDTLS_PK_PARSE_C +#define MBEDTLS_RSA_C +#define MBEDTLS_SHA1_C +#define MBEDTLS_SHA224_C +#define MBEDTLS_SHA256_C +#define MBEDTLS_SHA512_C +#define MBEDTLS_SSL_CLI_C +#define MBEDTLS_SSL_SRV_C +#define MBEDTLS_SSL_TLS_C +#define MBEDTLS_X509_CRT_PARSE_C +#define MBEDTLS_X509_USE_C +#define MBEDTLS_SSL_SERVER_NAME_INDICATION +#define MBEDTLS_GCM_C +#define MBEDTLS_ECDH_C +#define MBEDTLS_ECP_C +#define MBEDTLS_ECDSA_C +#define MBEDTLS_ASN1_WRITE_C + +// Pico port +#define MBEDTLS_PLATFORM_C +#define MBEDTLS_NO_PLATFORM_ENTROPY +#define MBEDTLS_HAVE_TIME +#define MBEDTLS_ENTROPY_HARDWARE_ALT + +// Options that reduce ROM or RAM usage at the expense of performance +// These default values match esp-idf's default configuration +#define MBEDTLS_SSL_OUT_CONTENT_LEN 4096 +// #define MBEDTLS_AES_FEWER_TABLES +// #define MBEDTLS_SHA256_SMALLER + +// Uncomment for debugging SSL otp_ssl.c +// #define MBEDTLS_DEBUG_C diff --git a/src/platforms/rp2040/src/lib/otp_ssl_platform.c b/src/platforms/rp2040/src/lib/otp_ssl_platform.c new file mode 100644 index 000000000..f17a0a5c5 --- /dev/null +++ b/src/platforms/rp2040/src/lib/otp_ssl_platform.c @@ -0,0 +1,25 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Paul Guyot + * + * 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 + */ + +#include +#include +#include + +REGISTER_NIF_COLLECTION(otp_ssl, otp_ssl_init, NULL, otp_ssl_nif_get_nif) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0aa15bb18..20e8e8a8b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -54,6 +54,14 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") endif() endif() +include(MbedTLS) +if (MbedTLS_FOUND) + target_link_libraries(test-erlang PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-enif PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-mailbox PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-structs PRIVATE MbedTLS::mbedtls) +endif() + set( PLATFORM_LIB_SUFFIX ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR} diff --git a/tests/libs/estdlib/CMakeLists.txt b/tests/libs/estdlib/CMakeLists.txt index b543f4ba5..5501a842b 100644 --- a/tests/libs/estdlib/CMakeLists.txt +++ b/tests/libs/estdlib/CMakeLists.txt @@ -35,6 +35,7 @@ set(ERLANG_MODULES test_maps test_net test_spawn + test_ssl test_string test_proplists test_timer diff --git a/tests/libs/estdlib/test_ssl.erl b/tests/libs/estdlib/test_ssl.erl new file mode 100644 index 000000000..fde3ec6ba --- /dev/null +++ b/tests/libs/estdlib/test_ssl.erl @@ -0,0 +1,80 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% 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_ssl). + +-export([test/0]). + +-include("etest.hrl"). + +test() -> + case is_ssl_available() of + true -> + test_ssl(); + false -> + io:format("Warning: skipping test_ssl as ssl is not available\n"), + ok + end. + +is_ssl_available() -> + case erlang:system_info(machine) of + "BEAM" -> + true; + _ -> + try + ssl:nif_init(), + true + catch + error:undef -> + false + end + end. + +test_ssl() -> + ok = ssl:start(), + ok = test_start_twice(), + ok = test_connect_close(), + ok = test_connect_error(), + ok = test_send_recv(), + ok = ssl:stop(), + ok. + +test_start_twice() -> + ok = ssl:start(). + +test_connect_close() -> + {ok, SSLSocket} = ssl:connect("atomvm.net", 443, [{verify, verify_none}, {active, false}]), + ok = ssl:close(SSLSocket). + +test_connect_error() -> + {error, _Error} = ssl:connect("atomvm.net", 80, [{verify, verify_none}, {active, false}]), + ok. + +test_send_recv() -> + {ok, SSLSocket} = ssl:connect("atomvm.net", 443, [ + {verify, verify_none}, {active, false}, {binary, true} + ]), + UserAgent = erlang:system_info(machine), + ok = ssl:send(SSLSocket, [ + <<"GET / HTTP/1.1\r\nHost: atomvm.net\r\nUser-Agent: ">>, UserAgent, <<"\r\n\r\n">> + ]), + {ok, <<"HTTP/1.1">>} = ssl:recv(SSLSocket, 8), + ok = ssl:close(SSLSocket), + ok. diff --git a/tests/libs/estdlib/tests.erl b/tests/libs/estdlib/tests.erl index 801fed6b6..5c4daab28 100644 --- a/tests/libs/estdlib/tests.erl +++ b/tests/libs/estdlib/tests.erl @@ -36,7 +36,7 @@ get_otp_version() -> get_tests(OTPVersion) when (is_integer(OTPVersion) andalso OTPVersion >= 24) orelse OTPVersion == atomvm -> - [test_tcp_socket, test_udp_socket, test_net | get_tests(undefined)]; + [test_tcp_socket, test_udp_socket, test_net, test_ssl | get_tests(undefined)]; get_tests(_OTPVersion) -> [ test_lists,