Skip to content

Commit

Permalink
Verify peer cert fingerprint
Browse files Browse the repository at this point in the history
  • Loading branch information
mickel8 committed Nov 27, 2023
1 parent 6154a3b commit 770cf3b
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 57 deletions.
48 changes: 32 additions & 16 deletions lib/ex_webrtc/dtls_transport.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule ExWebRTC.DTLSTransport do
require Logger

alias ExICE.ICEAgent
alias ExWebRTC.Utils

@type dtls_transport() :: GenServer.server()

Expand Down Expand Up @@ -48,9 +49,10 @@ defmodule ExWebRTC.DTLSTransport do
end

@doc false
@spec start_dtls(dtls_transport(), :active | :passive) :: :ok | {:error, :already_started}
def start_dtls(dtls_transport, mode) do
GenServer.call(dtls_transport, {:start_dtls, mode})
@spec start_dtls(dtls_transport(), :active | :passive, binary()) ::
:ok | {:error, :already_started}
def start_dtls(dtls_transport, mode, peer_fingerprint) do
GenServer.call(dtls_transport, {:start_dtls, mode, peer_fingerprint})
end

@doc false
Expand All @@ -61,11 +63,8 @@ defmodule ExWebRTC.DTLSTransport do

@impl true
def init([ice_config, ice_module, owner]) do
# temporary hack to generate certs
dtls = ExDTLS.init(client_mode: true, dtls_srtp: true)
cert = ExDTLS.get_cert(dtls)
pkey = ExDTLS.get_pkey(dtls)
fingerprint = ExDTLS.get_cert_fingerprint(dtls)
{pkey, cert} = ExDTLS.generate_key_cert()
fingerprint = ExDTLS.get_cert_fingerprint(cert)

{:ok, ice_agent} = ice_module.start_link(:controlled, ice_config)
srtp = ExLibSRTP.new()
Expand All @@ -78,6 +77,8 @@ defmodule ExWebRTC.DTLSTransport do
cert: cert,
pkey: pkey,
fingerprint: fingerprint,
# sha256 hex dump
peer_fingerprint: nil,
srtp: srtp,
dtls_state: :new,
dtls: nil,
Expand All @@ -100,22 +101,25 @@ defmodule ExWebRTC.DTLSTransport do
end

@impl true
def handle_call({:start_dtls, mode}, _from, %{dtls: nil} = state)
def handle_call({:start_dtls, mode, peer_fingerprint}, _from, %{dtls: nil} = state)
when mode in [:active, :passive] do
ex_dtls_mode = if mode == :active, do: :client, else: :server

dtls =
ExDTLS.init(
client_mode: mode == :active,
mode: ex_dtls_mode,
dtls_srtp: true,
pkey: state.pkey,
cert: state.cert
cert: state.cert,
verify_peer: true
)

state = %{state | dtls: dtls, mode: mode}
state = %{state | dtls: dtls, mode: mode, peer_fingerprint: peer_fingerprint}
{:reply, :ok, state}
end

@impl true
def handle_call({:start_dtls, _mode}, _from, state) do
def handle_call({:start_dtls, _mode, _peer_fingerprint}, _from, state) do
# is there a case when mode will change and new handshake will be needed?
{:reply, {:error, :already_started}, state}
end
Expand Down Expand Up @@ -198,9 +202,20 @@ defmodule ExWebRTC.DTLSTransport do
{:handshake_finished, _, remote_keying_material, profile, packets} ->
Logger.debug("DTLS handshake finished")
ICEAgent.send_data(state.ice_agent, packets)
# TODO: validate fingerprint
state = setup_srtp(state, remote_keying_material, profile)
update_dtls_state(state, :connected)

peer_fingerprint =
state.dtls
|> ExDTLS.get_peer_cert()
|> ExDTLS.get_cert_fingerprint()
|> Utils.hex_dump()

if peer_fingerprint == state.peer_fingerprint do
state = setup_srtp(state, remote_keying_material, profile)
update_dtls_state(state, :connected)
else
Logger.debug("Non-matching peer cert fingerprint.")
update_dtls_state(state, :failed)
end

{:handshake_finished, _, remote_keying_material, profile} ->
Logger.debug("DTLS handshake finished")
Expand Down Expand Up @@ -283,6 +298,7 @@ defmodule ExWebRTC.DTLSTransport do
defp update_dtls_state(%{dtls_state: dtls_state} = state, dtls_state), do: state

defp update_dtls_state(state, new_dtls_state) do
Logger.debug("Changing DTLS state: #{state.dtls_state} -> #{new_dtls_state}")
notify(state.owner, {:state_change, new_dtls_state})
%{state | dtls_state: new_dtls_state}
end
Expand Down
35 changes: 28 additions & 7 deletions lib/ex_webrtc/peer_connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ defmodule ExWebRTC.PeerConnection do
signaling_state: :stable,
conn_state: :new,
last_offer: nil,
last_answer: nil
last_answer: nil,
peer_fingerprint: nil
]

#### API ####
Expand Down Expand Up @@ -280,7 +281,7 @@ defmodule ExWebRTC.PeerConnection do
{:ok, state} <- apply_local_description(other_type, sdp, state) do
{:reply, :ok, %{state | signaling_state: next_state}}
else
error -> {:reply, error, state}
{:error, _reason} = error -> {:reply, error, state}
end
end
end
Expand All @@ -299,7 +300,7 @@ defmodule ExWebRTC.PeerConnection do
{:ok, state} <- apply_remote_description(other_type, sdp, state) do
{:reply, :ok, %{state | signaling_state: next_state}}
else
error -> {:reply, error, state}
{:error, _reason} = error -> {:reply, error, state}
end
end
end
Expand Down Expand Up @@ -351,7 +352,13 @@ defmodule ExWebRTC.PeerConnection do
state = %__MODULE__{state | ice_state: new_ice_state}
next_conn_state = next_conn_state(new_ice_state, state.dtls_state)
state = update_conn_state(state, next_conn_state)
{:noreply, state}

if next_conn_state == :failed do
Logger.debug("Stopping PeerConnection")
{:stop, {:shutdown, :conn_state_failed}, state}
else
{:noreply, state}
end
end

@impl true
Expand Down Expand Up @@ -408,7 +415,7 @@ defmodule ExWebRTC.PeerConnection do
dtls =
if type == :answer do
{:setup, setup} = ExSDP.Media.get_attribute(hd(sdp.media), :setup)
:ok = DTLSTransport.start_dtls(state.dtls_transport, setup)
:ok = DTLSTransport.start_dtls(state.dtls_transport, setup, state.peer_fingerprint)
else
state.dtls_transport
end
Expand All @@ -430,6 +437,7 @@ defmodule ExWebRTC.PeerConnection do
with :ok <- SDPUtils.ensure_mid(sdp),
:ok <- SDPUtils.ensure_bundle(sdp),
{:ok, {ice_ufrag, ice_pwd}} <- SDPUtils.get_ice_credentials(sdp),
{:ok, {:fingerprint, {:sha256, peer_fingerprint}}} <- SDPUtils.get_cert_fingerprint(sdp),
{:ok, new_transceivers} <-
update_remote_transceivers(state.transceivers, sdp, state.config) do
:ok = ICEAgent.set_remote_credentials(state.ice_agent, ice_ufrag, ice_pwd)
Expand Down Expand Up @@ -460,12 +468,24 @@ defmodule ExWebRTC.PeerConnection do
:passive -> :active
end

:ok = DTLSTransport.start_dtls(state.dtls_transport, setup)
:ok = DTLSTransport.start_dtls(state.dtls_transport, setup, peer_fingerprint)
else
state.dtls_transport
end

{:ok, %{state | transceivers: new_transceivers, dtls_transport: dtls}}
{:ok,
%{
state
| transceivers: new_transceivers,
dtls_transport: dtls,
peer_fingerprint: peer_fingerprint
}}
else
{:ok, {:fingerprint, {_hash_function, _fingerprint}}} ->
{:error, :unsupported_cert_fingerprint_hash_function}

{:error, _reason} = error ->
error
end
end

Expand Down Expand Up @@ -569,6 +589,7 @@ defmodule ExWebRTC.PeerConnection do
defp update_conn_state(%{conn_state: conn_state} = state, conn_state), do: state

defp update_conn_state(state, new_conn_state) do
Logger.debug("Changing PeerConnection state: #{state.conn_state} -> #{new_conn_state}")
notify(state.owner, {:connection_state_change, new_conn_state})
%{state | conn_state: new_conn_state}
end
Expand Down
73 changes: 73 additions & 0 deletions lib/ex_webrtc/sdp_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ defmodule ExWebRTC.SDPUtils do
end)
end

@spec delete_attribute(ExSDP.t() | ExSDP.Media.t(), module() | atom() | binary()) ::
ExSDP.t() | ExSDP.Media.t()
def delete_attribute(sdp_or_mline, key) do
delete_attributes(sdp_or_mline, [key])
end

@spec delete_attributes(ExSDP.t() | ExSDP.Media.t(), [module() | atom() | binary()]) ::
ExSDP.t() | ExSDP.Media.t()
def delete_attributes(sdp_or_mline, attributes) when is_list(attributes) do
new_attrs =
Enum.reject(sdp_or_mline.attributes, fn
%module{} -> module in attributes
{k, _v} -> k in attributes
# flag attributes
k -> k in attributes
end)

Map.put(sdp_or_mline, :attributes, new_attrs)
end

@spec ensure_mid(ExSDP.t()) :: :ok | {:error, :missing_mid | :duplicated_mid}
def ensure_mid(sdp) do
sdp.media
Expand Down Expand Up @@ -101,6 +121,34 @@ defmodule ExWebRTC.SDPUtils do
end
end

@spec get_cert_fingerprint(ExSDP.t()) ::
{:ok, {:fingerprint, {:sha256, binary()}}}
| {:error, :missing_cert_fingerprint | :conflicting_cert_fingerprints}
def get_cert_fingerprint(sdp) do
session_fingerprint = do_get_cert_fingerprint(sdp)
mline_fingerprints = Enum.map(sdp.media, fn mline -> do_get_cert_fingerprint(mline) end)

case {session_fingerprint, mline_fingerprints} do
{nil, []} ->
{:error, :missing_cert_fingerprint}

{session_fingerprint, []} ->
{:ok, session_fingerprint}

{nil, mline_fingerprints} ->
with :ok <- ensure_fingerprints_present(mline_fingerprints),
:ok <- ensure_fingerprints_unique(mline_fingerprints) do
{:ok, List.first(mline_fingerprints)}
end

{session_fingerprint, mline_fingerprints} ->
with :ok <- ensure_fingerprints_present(mline_fingerprints),
:ok <- ensure_fingerprints_unique([session_fingerprint | mline_fingerprints]) do
{:ok, session_fingerprint}
end
end
end

@spec get_extensions(ExSDP.t()) ::
%{(id :: non_neg_integer()) => extension :: module() | {SourceDescription, atom()}}
def get_extensions(sdp) do
Expand Down Expand Up @@ -156,6 +204,16 @@ defmodule ExWebRTC.SDPUtils do
{ice_ufrag, ice_pwd}
end

defp do_get_cert_fingerprint(sdp_or_mline) do
get_attr =
case sdp_or_mline do
%ExSDP{} -> &ExSDP.get_attribute/2
%ExSDP.Media{} -> &ExSDP.Media.get_attribute/2
end

get_attr.(sdp_or_mline, :fingerprint)
end

defp ensure_ice_credentials_present(creds) do
creds
|> Enum.find(fn {ice_ufrag, ice_pwd} -> ice_ufrag == nil or ice_pwd == nil end)
Expand All @@ -180,4 +238,19 @@ defmodule ExWebRTC.SDPUtils do
_ -> {:error, :conflicting_ice_credentials}
end
end

defp ensure_fingerprints_present(fingerprints) do
if Enum.all?(fingerprints, &(&1 != nil)) do
:ok
else
{:error, :missing_cert_fingerprint}
end
end

defp ensure_fingerprints_unique(fingerprints) do
case Enum.uniq(fingerprints) do
[_] -> :ok
_ -> {:error, :conflicting_cert_fingerprints}
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ defmodule ExWebRTC.MixProject do
[
{:ex_sdp, "~> 0.13.0"},
{:ex_ice, "~> 0.2.0"},
{:ex_dtls, "~> 0.14.0"},
{:ex_dtls, github: "elixir-webrtc/ex_dtls"},
{:ex_libsrtp, "~> 0.6.0"},
{:ex_rtp, "~> 0.2.0"},
{:ex_rtcp, "~> 0.1.0"},
Expand Down
14 changes: 7 additions & 7 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
"dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"},
"earmark_parser": {:hex, :earmark_parser, "1.4.34", "b0fbb4fd333ee7e9babc07e9573796850759cd12796fcf2fec59cf0031cbaad9", [:mix], [], "hexpm", "cc0d7a6f2367e4504867b4ec38ceee24e89ee6bca9c7b94a6d940f54aba2e8d5"},
"credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"},
"dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"},
"earmark_parser": {:hex, :earmark_parser, "1.4.38", "b42252eddf63bda05554ba8be93a1262dc0920c721f1aaf989f5de0f73a2e367", [:mix], [], "hexpm", "2cd0907795aaef0c7e8442e376633c5b3bd6edc8dbbdc539b22f095501c1cdb6"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
"ex_dtls": {:hex, :ex_dtls, "0.14.0", "f2e589a24396599551c6b142c3a6a7a55f7fa772c90716888ed42bf3c994cb2d", [:mix], [{:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "7fa79815d8dbcee3b1464c3590527fb85fae9592b6e092f2554c5974e2c5847a"},
"ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"},
"ex_dtls": {:git, "https://github.com/elixir-webrtc/ex_dtls.git", "876e2119f97eee053671b6f6fd069575f0da8d77", []},
"ex_ice": {:hex, :ex_ice, "0.2.0", "6002de5b1c0561f8ff50238a6d04cb99eabf9b7db91031b6f0580d67888f0365", [:mix], [{:ex_stun, "~> 0.1.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "1def9449009241918b31ff269f344e5c4bd15257ba5cf95d1edc1250c28af39b"},
"ex_libsrtp": {:hex, :ex_libsrtp, "0.6.0", "d96cd7fc1780157614f0bf47d31587e5eab953b43067f4885849f8177ec452a9", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "e9ce8a507a658f7e2df72fae82a4b3ba0a056c175f0bc490e79ab03058e094d5"},
"ex_rctp": {:git, "https://github.com/elixir-webrtc/ex_rtcp.git", "c0cf2b7f995e34d13cee4cbb228376a55700fb6a", []},
Expand All @@ -25,15 +25,15 @@
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"},
Expand Down
Loading

0 comments on commit 770cf3b

Please sign in to comment.