More documentation and a more intuitive step_handshake

This commit is contained in:
Hans Svensson 2018-03-14 10:37:48 +01:00
parent 8f3aff4d8b
commit d81f973bca
3 changed files with 143 additions and 34 deletions

View File

@ -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
-----

View File

@ -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 ---------------------------------------------

View File

@ -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)