diff --git a/README.md b/README.md index 519b132..6c3b9af 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,20 @@ Focus has first and foremost been on the correct use of dirty schedulers, withou Also, while the standard `crypto` bindings in Erlang does a great job at providing cryptographic primitives, these are based on OpenSSL, which is known to be highly problematic in many ways. It is not as easy to use the OpenSSL library correctly as it is with these bindings. Rather than providing a low-level cipher suite, NaCl provides intermediate level primitives constructed as to protect the user against typical low-level cryptographic gotchas and problems. +## Scheduler handling + +The major problem which a NIF library has to address is the problem of blocking Erlang schedulers. A long-running NIF messes with the scheduler in many ways, the worst of which is breaking it. To avoid this, we have to address long-running work on the NIF. The current method used is to care about the *progress* of the system rather than the *precision*. That is, we guarantee the system will always quickly progress toward a new process, even when running many cryptographic NIFs are run in a given process. However, we don't care about the precision of the progress. A cryptographic NIF may get either a free ride on the reduction budget, or be penalized more than it should be. + +The current approach is to switch between blocking NIF calls and dirty scheduler use at a breakoff threshold. Currently, we use the meaurements obtained by assuming a schedule of 100μs is 1/10th of a 1ms budget. And then we set a reduction budget based on these values. 100μs is roughly set at 200 reductions. And to be on the safe side, we multiply these values by two to handle older CPUs as well too. Measurements are obtained by running: + + enacl_timing:all(). + +The current "typical modern machine" is: + + Intel Core i7-4900QM + +I'm interested in machines for which the schedules end up being far off. That is, machines for which the current CPU schedule takes more than 250μs. This is especially interesting for virtual machines. + # Testing Every primitive has been stress-tested through the use of Erlang QuickCheck with both *positive* and *negative* testing. This has been used to check against memory leaks as well as correct invocation. Please report any error so we can extend the test cases to include a randomized test which captures the problem so we generically catch every problem in a given class of errors. diff --git a/src/enacl.erl b/src/enacl.erl index a57ca6c..73de835 100644 --- a/src/enacl.erl +++ b/src/enacl.erl @@ -67,6 +67,12 @@ reds/1 ]). +%% Definitions of system budgets +-define(HASH_SIZE, 32 * 1024). +-define(HASH_REDUCTIONS, 2 * 200). +-define(BOX_SIZE, 32 * 1024). +-define(BOX_REDUCTIONS, 2 * 250). + %% Count reductions and number of scheduler yields for Fun. Fun is assumed %% to be one of the above exor variants. reds(Fun) -> @@ -97,7 +103,12 @@ reds(Fun) -> when Data :: binary(), Checksum :: binary(). -hash(Bin) -> enacl_nif:crypto_hash(Bin). +hash(Bin) when byte_size(Bin) =< ?HASH_SIZE -> + R = enacl_nif:crypto_hash_b(Bin), + bump(?HASH_REDUCTIONS, ?HASH_SIZE, byte_size(Bin)), + R; +hash(Bin) -> + enacl_nif:crypto_hash(Bin). %% @doc verify_16/2 implements constant time 16-byte string verification %%
A subtle problem in cryptographic software are timing attacks where an attacker exploits @@ -138,6 +149,10 @@ box_keypair() -> PK :: binary(), SK :: binary(), CipherText :: binary(). +box(Msg, Nonce, PK, SK) when byte_size(Msg) =< ?BOX_SIZE -> + R = enacl_nif:crypto_box_b([p_zerobytes(), Msg], Nonce, PK, SK), + bump(?BOX_REDUCTIONS, ?BOX_SIZE, byte_size(Msg)), + R; box(Msg, Nonce, PK, SK) -> enacl_nif:crypto_box([p_zerobytes(), Msg], Nonce, PK, SK). @@ -151,6 +166,13 @@ box(Msg, Nonce, PK, SK) -> PK :: binary(), SK :: binary(), Msg :: binary(). +box_open(CipherText, Nonce, PK, SK) when byte_size(CipherText) =< ?BOX_SIZE -> + R = case enacl_nif:crypto_box_open_b([p_box_zerobytes(), CipherText], Nonce, PK, SK) of + {error, Err} -> {error, Err}; + Bin when is_binary(Bin) -> {ok, Bin} + end, + bump(?BOX_REDUCTIONS, ?BOX_SIZE, byte_size(CipherText)), + R; box_open(CipherText, Nonce, PK, SK) -> case enacl_nif:crypto_box_open([p_box_zerobytes(), CipherText], Nonce, PK, SK) of {error, Err} -> {error, Err}; @@ -350,3 +372,8 @@ s_zerobytes() -> s_box_zerobytes() -> binary:copy(<<0>>, enacl_nif:crypto_secretbox_BOXZEROBYTES()). + +bump(Budget, Max, Sz) -> + Reds = (Budget * Sz) div Max, + erlang:bump_reductions(max(1, Reds)), + ok. diff --git a/src/enacl_nif.erl b/src/enacl_nif.erl index c8d7933..c492d2a 100644 --- a/src/enacl_nif.erl +++ b/src/enacl_nif.erl @@ -143,6 +143,3 @@ crypto_hash(Input) when is_binary(Input) -> not_loaded(). crypto_hash_b(Input) when is_binary(Input) -> not_loaded(). crypto_verify_16(_X, _Y) -> not_loaded(). crypto_verify_32(_X, _Y) -> not_loaded(). - - - diff --git a/src/enacl_timing.erl b/src/enacl_timing.erl new file mode 100644 index 0000000..1e00c26 --- /dev/null +++ b/src/enacl_timing.erl @@ -0,0 +1,61 @@ +%%% @doc module enacl_timing provides helpers for timing enacl toward your installation +%%% @end +-module(enacl_timing). + +-export([all/0]). + +all() -> + [time_hashing(), + time_box()]. + +-define(ROUNDS, 300). + +time_box() -> + Sz = 1024 * 32, + ZB = binary:copy(<<0>>, enacl_nif:crypto_box_ZEROBYTES()), + BZB = binary:copy(<<0>>, enacl_nif:crypto_box_BOXZEROBYTES()), + Bin = binary:copy(<<0>>, Sz), + Nonce = binary:copy(<<0>>, enacl_nif:crypto_box_NONCEBYTES()), + #{ public := PK1, secret := SK1 } = enacl:box_keypair(), + #{ public := PK2, secret := SK2 } = enacl:box_keypair(), + box([ZB, Bin], Nonce, PK1, SK2, ?ROUNDS), + T = timed(fun() -> box([ZB, Bin], Nonce, PK1, SK2, ?ROUNDS) end) / ?ROUNDS, + Boxed = enacl:box([ZB, Bin], Nonce, PK1, SK2), + box_open([BZB, Boxed], Nonce, PK2, SK1, ?ROUNDS), + T2 = timed(fun() -> box_open([BZB, Boxed], Nonce, PK2, SK1, ?ROUNDS) end) / ?ROUNDS, + [ + #{ size => Sz, time => T, operation => box}, + #{ size => Sz, time => T2, operation => box_open} + ]. + +%% BOX +%% -------- +box_open(_Bin, _Nonce, _PK, _SK, 0) -> ok; +box_open(Bin, Nonce, PK, SK, N) -> + enacl_nif:crypto_box_open_b(Bin, Nonce, PK, SK), + box_open(Bin, Nonce, PK, SK, N-1). + +box(_Bin, _Nonce, _PK, _SK, 0) -> ok; +box(Bin, Nonce, PK, SK, N) -> + enacl_nif:crypto_box_b(Bin, Nonce, PK, SK), + box(Bin, Nonce, PK, SK, N-1). + +%% HASHING +%% ---------------- +time_hashing() -> + Sz = 1024 * 32, + Bin = binary:copy(<<0>>, Sz), + hash(Bin, ?ROUNDS), + T = timed(fun() -> hash(Bin, ?ROUNDS) end) / ?ROUNDS, + #{ size => Sz, time => T, operation => hash}. + +hash(_Bin, 0) -> ok; +hash(Bin, N) -> + enacl_nif:crypto_hash_b(Bin), + hash(Bin, N-1). + +%% Helpers +timed(Fun) -> + {T, _} = timer:tc(Fun), + T. +