diff --git a/c_src/ex_dtls/native.c b/c_src/ex_dtls/native.c index 4f22934..68287b3 100644 --- a/c_src/ex_dtls/native.c +++ b/c_src/ex_dtls/native.c @@ -266,6 +266,41 @@ UNIFEX_TERM do_handshake(UnifexEnv *env, State *state) { return unifex_raise(state->env, "Handshake failed: no packets generated"); } +UNIFEX_TERM write_data(UnifexEnv *env, State *state, UnifexPayload *payload) { + if (state->hsk_finished != 1) { + DEBUG("Cannot write, handshake not finished"); + return write_data_result_error_handshake_not_finished(env); + } + + int ret = SSL_write(state->ssl, payload->data, payload->size); + if (ret <= 0) { + DEBUG("Unable to write data"); + return unifex_raise(env, "Unable to write data"); + } + + BIO *wbio = SSL_get_wbio(state->ssl); + size_t pending_data_len = BIO_ctrl_pending(wbio); + if (pending_data_len == 0) { + DEBUG("No data to read from BIO after writing"); + return unifex_raise(env, "No data to read from BIO after writing"); + } + + UnifexPayload res_payload; + unifex_payload_alloc(env, UNIFEX_PAYLOAD_BINARY, pending_data_len, &res_payload); + + int read_bytes = BIO_read(wbio, res_payload.data, pending_data_len); + if (read_bytes <= 0 || (size_t) read_bytes != pending_data_len) { + DEBUG("Unable to read data from BIO after writing"); + return unifex_raise(env, "Unable to read data from BIO after writing"); + } + + DEBUG("Wrote %d bytes of data", read_bytes); + res_payload.size = (unsigned int) pending_data_len; + UNIFEX_TERM res_term = write_data_result_ok(env, &res_payload); + unifex_payload_release(&res_payload); + return res_term; +} + UNIFEX_TERM handle_data(UnifexEnv *env, State *state, UnifexPayload *payload) { (void)env; diff --git a/c_src/ex_dtls/native.spec.exs b/c_src/ex_dtls/native.spec.exs index c5f890c..2609834 100644 --- a/c_src/ex_dtls/native.spec.exs +++ b/c_src/ex_dtls/native.spec.exs @@ -25,6 +25,8 @@ spec do_handshake(state) :: {packets :: payload, timeout :: int} spec handle_timeout(state) :: (:ok :: label) | {:retransmit :: label, packets :: payload, timeout :: int} +spec write_data(state, packets :: payload) :: {:ok :: label, packets :: payload} | {:error :: label, :handshake_not_finished :: label} + spec handle_data(state, packets :: payload) :: {:ok :: label, packets :: payload} | (:handshake_want_read :: label) diff --git a/lib/ex_dtls.ex b/lib/ex_dtls.ex index d5d16a1..11e1fa7 100644 --- a/lib/ex_dtls.ex +++ b/lib/ex_dtls.ex @@ -3,7 +3,7 @@ defmodule ExDTLS do Module that allows performing DTLS handshake including a DTLS-SRTP one. `ExDTLS` executes native OpenSSL functions to perform DTLS handshake. - It doesn't create or require any socket. + It doesn't create or require any socket. Instead, it returns generated DTLS packets, which then have to be transported to the peer. """ @@ -82,7 +82,7 @@ defmodule ExDTLS do This is always 2048-bit RSA key. - `not_before` and `not_after` can be used to + `not_before` and `not_after` can be used to specify certificate duration in seconds. They have to fit into architecture-dependent integer size. Defaults to (-1 year) - (+ 1 year). @@ -111,7 +111,7 @@ defmodule ExDTLS do @doc """ Gets peer certificate. - Returns DER representation in binary format or `nil` + Returns DER representation in binary format or `nil` when no certificate was presented by the peer or no connection was established. """ @@ -142,6 +142,15 @@ defmodule ExDTLS do @spec do_handshake(dtls()) :: {packets :: binary(), timeout :: integer()} defdelegate do_handshake(dtls), to: Native + @doc """ + Writes data to the DTLS connection. + + Generates encrypted packets that need to be passed to the second host. + """ + @spec write_data(dtls(), packets :: binary()) :: + {:ok, packets :: binary()} | {:error, :handshake_not_finished} + defdelegate write_data(dtls, packets), to: Native + @doc """ Handles peer's packets. @@ -149,7 +158,7 @@ defmodule ExDTLS do which is decoded data. `:handshake_packets` contains handshake data that has to be sent to the peer. - `:handshake_want_read` means some additional data is needed for continuing handshake. + `:handshake_want_read` means some additional data is needed for continuing handshake. It can be returned when retransmitted packet was passed but timer didn't expired yet. `timeout` is a time in ms after which `handle_timeout/1` should be called. diff --git a/mix.exs b/mix.exs index eb599d4..b7bffc6 100644 --- a/mix.exs +++ b/mix.exs @@ -49,6 +49,7 @@ defmodule ExDTLS.Mixfile do defp deps do [ {:unifex, "~> 1.0"}, + {:bundlex, "~> 1.5.3", override: true}, {:ex_doc, "~> 0.29", only: :dev, runtime: false}, {:dialyxir, "~> 1.0", only: :dev, runtime: false}, {:credo, "~> 1.7", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 699b92f..f1c93de 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, - "bundlex": {:hex, :bundlex, "1.5.1", "a85890a9d0a70366afa538c8589a4ba75e1319d32a771e1f5f3b7566beea9c26", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, "~> 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "aae447d63230fe1f3b788c429ac02bc696f30163d0f23f52fcfe6ed38372c7ea"}, + "bundlex": {:hex, :bundlex, "1.5.3", "35d01e5bc0679510dd9a327936ffb518f63f47175c26a35e708cc29eaec0890b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "debd0eac151b404f6216fc60222761dff049bf26f7d24d066c365317650cd118"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, @@ -14,18 +14,18 @@ "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, - "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "0.3.1", "99d5244672fafdfac89bfad3d3ab8f0d367603ce1dc4855f86a1c75008bce56f", [:mix], [], "hexpm", "4bf510adedff0449a1d6e200e43e57a814794c8b5b6439071274d248d272a549"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, - "req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"}, + "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, "shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, diff --git a/test/integration_test.exs b/test/integration_test.exs index d372a59..fc34799 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -23,10 +23,31 @@ defmodule ExDTLS.IntegrationTest do assert ExDTLS.get_peer_cert(tx_dtls) == ExDTLS.get_cert(rx_dtls) # Client only sends its certificate when requested to do so by the server. - # Because `verify_peer` is set to `false`, server won't ask for the client's certificate. + # Because `verify_peer` is set to `false`, server won't ask for the client's certificate. assert ExDTLS.get_peer_cert(rx_dtls) == nil end + test "sending over DTLS" do + sr_dtls = ExDTLS.init(mode: :server, dtls_srtp: true, verify_peer: true) + cl_dtls = ExDTLS.init(mode: :client, dtls_srtp: true, verify_peer: true) + + assert {:error, :handshake_not_finished} = ExDTLS.write_data(sr_dtls, <<1, 2, 3>>) + assert {:error, :handshake_not_finished} = ExDTLS.write_data(cl_dtls, <<1, 2, 3>>) + + {packets, _timeout} = ExDTLS.do_handshake(cl_dtls) + assert :ok == loop({sr_dtls, false}, {cl_dtls, false}, packets) + + msg = <<1, 3, 2, 5>> + assert {:ok, data} = ExDTLS.write_data(cl_dtls, msg) + assert data != msg + assert {:ok, ^msg} = ExDTLS.handle_data(sr_dtls, data) + + msg = <<1, 3, 8, 9>> + assert {:ok, data} = ExDTLS.write_data(sr_dtls, msg) + assert data != msg + assert {:ok, ^msg} = ExDTLS.handle_data(cl_dtls, data) + end + test "expired cert" do # generate expired cert {key, cert} = ExDTLS.generate_key_cert(-1, 0)