From d81f973bcaa38ffdbf25e6826fbe9205eeb4a316 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Wed, 14 Mar 2018 10:37:48 +0100 Subject: [PATCH] More documentation and a more intuitive step_handshake --- README.md | 27 +++++++++++ src/enoise.erl | 49 +++++++++++++------- test/enoise_tests.erl | 101 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 143 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e74fb5c..ca2781b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,33 @@ enoise An Erlang implementation of the [Noise protocol](https://noiseprotocol.org/) +`enoise` provides a generic handshake mechanism, that can be used in a couple +of different ways. There is also a plain `gen_tcp`-wrapper, where you can +"upgrade" a TCP socket to a Noise socket and use it in much the same way as you +would use `gen_tcp`. + +Interactive handshake +--------------------- + +When using `enoise` to do an interactive handshake, `enoise` will only take +care of message composition/decompositiona and encryption/decryption - i.e. the +user has to do the actual sending and receiving. + +An example of the interactive handshake can be seen in the `noise_interactive` +test in `test/enoise_tests.erl`. + +Generic handshake +----------------- + +There is also the option to use an automated handshake procedure. If provided +with a generic _Communication state_ that describe how data is sent and +received, the handshake procedure is done automatically. The result of a +successful handshake is two Cipher states that can be used to encrypt/decrypt a +RX channel and a TX channel respectively. + +The provided `gen_tcp`-wrapper is implemented using the generic handshake, see +`src/enoise.erl`. + Build ----- diff --git a/src/enoise.erl b/src/enoise.erl index a720a11..5b5cf0f 100644 --- a/src/enoise.erl +++ b/src/enoise.erl @@ -66,6 +66,12 @@ binary(). %% Noise communication state - used to parameterize a handshake. Consists of a %% send function one receive function and an internal state. +-type noise_split_state() :: #{ rx := enoise_cipher_state:state(), + tx := enoise_cipher_state:state(), + hs_hash := binary() }. +%% Return value from the final `split' operation. Provides a CipherState for +%% receiving and a CipherState transmission. Also includes the final handshake +%% hash for channel binding. -opaque noise_socket() :: #enoise{}. %% An abstract Noise socket - holds a reference to a socket that has completed @@ -81,14 +87,20 @@ binary(). %% @end -spec handshake(Options :: noise_options(), Role :: enoise_hs_state:noise_role()) -> - {in, enoise_hs_state:state()} - | {out, binary(), enoise_hs_state:state()} - | {done, enoise_hs_state:state()} - | {error, term()}. + {ok, enoise_hs_state:state()} | {error, term()}. handshake(Options, Role) -> HState = create_hstate(Options, Role), - step_handshake(HState, <<>>). + {ok, HState}. +%% @doc Do a step (either `{send, Payload}', `{rcvd, EncryptedData}', +%% or `done') +%% @end +-spec step_handshake(HState :: enoise_hs_state:state(), + Data :: {rcvd, binary()} | {send, binary()}) -> + {ok, send, binary(), enoise_hs_state:state()} + | {ok, rcvd, binary(), enoise_hs_state:state()} + | {ok, done, map()} + | {error, term()}. step_handshake(HState, Data) -> do_step_handshake(HState, Data). @@ -196,17 +208,22 @@ hs_send_msg(CS = #{ send_msg := Send, state := S }, Data) -> end. do_step_handshake(HState, Data) -> - case enoise_hs_state:next_message(HState) of - in when Data == <<>> -> - {in, HState}; - in -> - {ok, HState1, _Msg} = enoise_hs_state:read_message(HState, Data), %% TODO: error handling - do_step_handshake(HState1, <<>>); - out -> - {ok, HState1, Msg} = enoise_hs_state:write_message(HState, <<>>), - {out, Msg, HState1}; - done -> - {done, enoise_hs_state:finalize(HState)} + case {enoise_hs_state:next_message(HState), Data} of + {in, {rcvd, Encrypted}} -> + case enoise_hs_state:read_message(HState, Encrypted) of + {ok, HState1, Msg} -> + {ok, rcvd, Msg, HState1}; + Err = {error, _} -> + Err + end; + {out, {send, Payload}} -> + {ok, HState1, Msg} = enoise_hs_state:write_message(HState, Payload), + {ok, send, Msg, HState1}; + {done, done} -> + {ok, Res} = enoise_hs_state:finalize(HState), + {ok, done, Res}; + {Next, _} -> + {error, {invalid_step, expected, Next, got, Data}} end. %% -- gen_tcp specific functions --------------------------------------------- diff --git a/test/enoise_tests.erl b/test/enoise_tests.erl index 9f67424..6dc9d7b 100644 --- a/test/enoise_tests.erl +++ b/test/enoise_tests.erl @@ -6,6 +6,71 @@ -include_lib("eunit/include/eunit.hrl"). +noise_interactive_test_() -> + %% Test vectors from https://raw.githubusercontent.com/rweather/noise-c/master/tests/vector/noise-c-basic.txt + {setup, + fun() -> test_utils:noise_test_vectors() end, + fun(_X) -> ok end, + fun(Tests) -> + [ {maps:get(name, T), fun() -> noise_interactive(T) end} + || T <- test_utils:noise_test_filter(Tests) ] + end + }. + +noise_interactive(V = #{ name := Name }) -> + Protocol = enoise_protocol:from_name(Name), + + FixK = fun(undefined) -> undefined; + (Bin) -> test_utils:hex_str_to_bin("0x" ++ binary_to_list(Bin)) end, + + Init = #{ prologue => FixK(maps:get(init_prologue, V, <<>>)) + , e => FixK(maps:get(init_ephemeral, V, undefined)) + , s => FixK(maps:get(init_static, V, undefined)) + , rs => FixK(maps:get(init_remote_static, V, undefined)) }, + Resp = #{ prologue => FixK(maps:get(resp_prologue, V, <<>>)) + , e => FixK(maps:get(resp_ephemeral, V, undefined)) + , s => FixK(maps:get(resp_static, V, undefined)) + , rs => FixK(maps:get(resp_remote_static, V, undefined)) }, + Messages = maps:get(messages, V), + HandshakeHash = maps:get(handshake_hash, V), + + noise_interactive(Name, Protocol, Init, Resp, Messages, FixK(HandshakeHash)), + + ok. + +noise_interactive(_Name, Protocol, Init, Resp, Messages, HSHash) -> + DH = enoise_protocol:dh(Protocol), + SecK = fun(undefined) -> undefined; (Sec) -> enoise_keypair:new(DH, Sec, undefined) end, + PubK = fun(undefined) -> undefined; (Pub) -> enoise_keypair:new(DH, Pub) end, + + HSInit = fun(#{ e := E, s := S, rs := RS, prologue := PL }, R) -> + Opts = [{noise, Protocol}, {s, SecK(S)}, {e, SecK(E)}, {rs, PubK(RS)}, {prologue, PL}], + enoise:handshake(Opts, R) + end, + {ok, InitHS} = HSInit(Init, initiator), + {ok, RespHS} = HSInit(Resp, responder), + + noise_interactive(Messages, InitHS, RespHS, HSHash). + +noise_interactive([#{ payload := PL0, ciphertext := CT0 } | Msgs], SendHS, RecvHS, HSHash) -> + PL = test_utils:hex_str_to_bin("0x" ++ binary_to_list(PL0)), + CT = test_utils:hex_str_to_bin("0x" ++ binary_to_list(CT0)), + case enoise_hs_state:next_message(SendHS) of + out -> + {ok, send, Message, SendHS1} = enoise:step_handshake(SendHS, {send, PL}), + ?assertEqual(CT, Message), + {ok, rcvd, PL1, RecvHS1} = enoise:step_handshake(RecvHS, {rcvd, Message}), + ?assertEqual(PL, PL1), + noise_interactive(Msgs, RecvHS1, SendHS1, HSHash); + done -> + {ok, done, #{ rx := RX1, tx := TX1, hs_hash := HSHash1 }} = enoise:step_handshake(SendHS, done), + {ok, done, #{ rx := RX2, tx := TX2, hs_hash := HSHash2 }} = enoise:step_handshake(RecvHS, done), + ?assertEqual(RX1, TX2), ?assertEqual(RX2, TX1), + ?assertEqual(HSHash, HSHash1), ?assertEqual(HSHash, HSHash2) + end. + + + noise_dh25519_test_() -> %% Test vectors from https://raw.githubusercontent.com/rweather/noise-c/master/tests/vector/noise-c-basic.txt {setup, @@ -91,27 +156,27 @@ need_rs(Role, Protocol) -> lists:member({in, [s]}, PreMsgs). %% Talks to local echo-server (noise-c) -client_test() -> - TestProtocol = enoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_BLAKE2b"), - ClientPrivKey = <<64,168,119,119,151,194,94,141,86,245,144,220,78,53,243,231,168,216,66,199,49,148,202,117,98,40,61,109,170,37,133,122>>, - ClientPubKey = <<115,39,86,77,44,85,192,176,202,11,4,6,194,144,127,123, 34,67,62,180,190,232,251,5,216,168,192,190,134,65,13,64>>, - ServerPubKey = <<112,91,141,253,183,66,217,102,211,40,13,249,238,51,77,114,163,159,32,1,162,219,76,106,89,164,34,71,149,2,103,59>>, +%% client_test() -> +%% TestProtocol = enoise_protocol:from_name("Noise_XK_25519_ChaChaPoly_BLAKE2b"), +%% ClientPrivKey = <<64,168,119,119,151,194,94,141,86,245,144,220,78,53,243,231,168,216,66,199,49,148,202,117,98,40,61,109,170,37,133,122>>, +%% ClientPubKey = <<115,39,86,77,44,85,192,176,202,11,4,6,194,144,127,123, 34,67,62,180,190,232,251,5,216,168,192,190,134,65,13,64>>, +%% ServerPubKey = <<112,91,141,253,183,66,217,102,211,40,13,249,238,51,77,114,163,159,32,1,162,219,76,106,89,164,34,71,149,2,103,59>>, - {ok, TcpSock} = gen_tcp:connect("localhost", 7890, [{active, once}, binary, {reuseaddr, true}], 1000), - gen_tcp:send(TcpSock, <<0,8,0,0,3>>), %% "Noise_XK_25519_ChaChaPoly_Blake2b" +%% {ok, TcpSock} = gen_tcp:connect("localhost", 7890, [{active, once}, binary, {reuseaddr, true}], 1000), +%% gen_tcp:send(TcpSock, <<0,8,0,0,3>>), %% "Noise_XK_25519_ChaChaPoly_Blake2b" - Opts = [ {noise, TestProtocol} - , {s, enoise_keypair:new(dh25519, ClientPrivKey, ClientPubKey)} - , {rs, enoise_keypair:new(dh25519, ServerPubKey)} - , {prologue, <<0,8,0,0,3>>}], +%% Opts = [ {noise, TestProtocol} +%% , {s, enoise_keypair:new(dh25519, ClientPrivKey, ClientPubKey)} +%% , {rs, enoise_keypair:new(dh25519, ServerPubKey)} +%% , {prologue, <<0,8,0,0,3>>}], - {ok, EConn} = enoise:connect(TcpSock, Opts), - ok = enoise:send(EConn, <<"ok\n">>), - receive - {noise, EConn, <<"ok\n">>} -> ok - after 1000 -> error(timeout) end, - %% {ok, <<"ok\n">>} = enoise:recv(EConn, 3, 1000), - enoise:close(EConn). +%% {ok, EConn} = enoise:connect(TcpSock, Opts), +%% ok = enoise:send(EConn, <<"ok\n">>), +%% receive +%% {noise, EConn, <<"ok\n">>} -> ok +%% after 1000 -> error(timeout) end, +%% %% {ok, <<"ok\n">>} = enoise:recv(EConn, 3, 1000), +%% enoise:close(EConn). %% Expects a call-in from a local echo-client (noise-c)