summaryrefslogtreecommitdiff
path: root/lib/ssl/test
diff options
context:
space:
mode:
authorRaimo Niskanen <raimo@erlang.org>2023-01-18 22:33:06 +0100
committerRaimo Niskanen <raimo@erlang.org>2023-02-09 15:33:37 +0100
commit8a6a620b14d9d34b10682eeee6478c2e3b250f4d (patch)
tree984693fa8a5cd9c3ae2e99b429410e6cfbf7d8a2 /lib/ssl/test
parent764c7aff16c6a55963500b5494075165a2332fbd (diff)
downloaderlang-8a6a620b14d9d34b10682eeee6478c2e3b250f4d.tar.gz
Break out cryptcookie as a utility module
Diffstat (limited to 'lib/ssl/test')
-rw-r--r--lib/ssl/test/Makefile1
-rw-r--r--lib/ssl/test/cryptcookie.erl665
-rw-r--r--lib/ssl/test/dist_cryptcookie.erl586
3 files changed, 1250 insertions, 2 deletions
diff --git a/lib/ssl/test/Makefile b/lib/ssl/test/Makefile
index 93a62bba15..e27e3870c7 100644
--- a/lib/ssl/test/Makefile
+++ b/lib/ssl/test/Makefile
@@ -98,6 +98,7 @@ MODULES = \
inet_cryptcookie_dist \
inet_crypto_dist \
dist_cryptcookie \
+ cryptcookie \
inet_epmd_inet_cryptcookie \
inet_epmd_socket_cryptcookie \
openssl_ocsp_SUITE \
diff --git a/lib/ssl/test/cryptcookie.erl b/lib/ssl/test/cryptcookie.erl
new file mode 100644
index 0000000000..b713bc15a8
--- /dev/null
+++ b/lib/ssl/test/cryptcookie.erl
@@ -0,0 +1,665 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2022-2023. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%% -------------------------------------------------------------------------
+%%
+%% Module for an encrypted Erlang stream based only on
+%% the cookie as a shared secret
+%%
+-module(cryptcookie).
+-feature(maybe_expr, enable).
+
+-export([supported/0, start_keypair_server/0, init/1, init/2]).
+-export([encrypt_and_send_chunk/4, recv_and_decrypt_chunk/2]).
+
+%% -------------------------------------------------------------------------
+%% The curve choice greatly affects setup time,
+%% we really want an Edwards curve but that would
+%% require a very new openssl version.
+%%
+%% Twisted brainpool curves (*t1) are faster than
+%% non-twisted (*r1), 256 is much faster than 384,
+%% and so on...
+%%
+%%% -define(CURVE, brainpoolP384t1).
+%%% -define(CURVE, brainpoolP256t1).
+-define(CURVE, secp256r1). % Portability
+-define(CIPHER, aes_gcm). % kTLS
+-define(HMAC, sha256). % kTLS
+
+supported() ->
+ maybe
+ true ?= crypto_supports(curves, ?CURVE),
+ true ?= crypto_supports(ciphers, ?CIPHER),
+ true ?= crypto_supports(macs, hmac),
+ true ?= crypto_supports(hashs, ?HMAC),
+ ok
+ end.
+
+crypto_supports(Tag, Item) ->
+ lists:member(Item, crypto:supports(Tag))
+ orelse "Crypto does not support "
+ ++ atom_to_list(Tag) ++ ": " ++ atom_to_list(Item).
+
+%% -------------------------------------------------------------------------
+
+-define(PACKET_SIZE, (1 bsl 16)). % 2 byte size header
+%% Plenty of room for AEAD tag and chunk type
+-define(CHUNK_SIZE, (?PACKET_SIZE - 256)).
+
+-record(params,
+ {hmac_algorithm = ?HMAC,
+ aead_cipher = ?CIPHER,
+ iv,
+ key,
+ tag_len = 16,
+ rekey_count = 256 * 1024,
+ rekey_time = 2 * 3600, % (seconds): 2 hours
+ rekey_timestamp,
+ rekey_key
+ }).
+
+params() ->
+ #{ iv_length := IvLen, key_length := KeyLen } =
+ crypto:cipher_info(?CIPHER),
+ #params{ iv = IvLen, key = KeyLen, rekey_timestamp = timestamp() }.
+
+
+-record(keypair,
+ {type = ecdh,
+ params = ?CURVE,
+ public,
+ private,
+ life_count = 256, % Number of connection setups
+ life_time = 3600, % 1 hour
+ life_timestamp
+ }).
+
+%% -------------------------------------------------------------------------
+%% Keep the node's public/private key pair in the process state
+%% of a key pair server linked to the net_kernel process.
+%% Create the key pair the first time it is needed
+%% so crypto gets time to start first.
+%%
+
+start_keypair_server() ->
+ Parent = self(),
+ Ref = make_ref(),
+ _ =
+ spawn_link(
+ fun () ->
+ try register(?MODULE, self()) of
+ true ->
+ Parent ! Ref,
+ keypair_server()
+ catch error : badarg ->
+ %% Already started - simply exit
+ %% and let the other run
+ Parent ! Ref,
+ ok
+ end
+ end),
+ receive Ref -> ok end.
+
+keypair_server() ->
+ keypair_server(undefined, 1).
+%%
+keypair_server(KeyPair) ->
+ keypair_server(KeyPair, KeyPair#keypair.life_count).
+%%
+keypair_server(_KeyPair, 0) ->
+ keypair_server();
+keypair_server(KeyPair, Count) ->
+ receive
+ {RefAlias, get_keypair} when is_reference(RefAlias) ->
+ case KeyPair of
+ undefined ->
+ KeyPair_1 = generate_keypair(),
+ RefAlias ! {RefAlias, KeyPair_1},
+ keypair_server(KeyPair_1);
+ #keypair{} ->
+ RefAlias ! {RefAlias, KeyPair},
+ keypair_server(KeyPair, Count - 1)
+ end;
+ {RefAlias, get_new_keypair} ->
+ KeyPair_1 = generate_keypair(),
+ RefAlias ! {RefAlias, KeyPair_1},
+ keypair_server(KeyPair_1)
+ end.
+
+call_keypair_server(Request) ->
+ Pid = whereis(?MODULE),
+ RefAlias = erlang:monitor(process, Pid, [{alias, reply_demonitor}]),
+ Pid ! {RefAlias, Request},
+ receive
+ {RefAlias, Reply} ->
+ Reply;
+ {'DOWN', RefAlias, process, Pid, Reason} ->
+ error({keypair_server, Reason})
+ end.
+
+generate_keypair() ->
+ #keypair{ type = Type, params = Params } = #keypair{},
+ {Public, Private} = crypto:generate_key(Type, Params),
+ #keypair{
+ public = Public, private = Private,
+ life_timestamp = timestamp() }.
+
+
+get_keypair() ->
+ call_keypair_server(?FUNCTION_NAME).
+
+get_new_keypair() ->
+ call_keypair_server(?FUNCTION_NAME).
+
+compute_shared_secret(
+ #keypair{
+ type = PublicKeyType,
+ params = PublicKeyParams,
+ private = PrivKey }, PubKey) ->
+ %%
+ crypto:compute_key(PublicKeyType, PubKey, PrivKey, PublicKeyParams).
+
+%% -------------------------------------------------------------------------
+
+-define(DATA_CHUNK, 2).
+-define(TICK_CHUNK, 3).
+-define(REKEY_CHUNK, 4).
+
+%% -------------------------------------------------------------------------
+%% Crypto strategy
+%% -------
+%% The crypto strategy is as simple as possible to get an encrypted
+%% connection as benchmark reference. It is geared around AEAD
+%% ciphers in particular AES-GCM.
+%%
+%% The init message and the start message must fit in the TCP buffers
+%% since both sides start with sending the init message, waits
+%% for the other end's init message, sends the start message
+%% and waits for the other end's start message. So if the send
+%% blocks we have a deadlock.
+%%
+%% The init + start sequence tries to implement Password Encrypted
+%% Key Exchange using a node public/private key pair and the
+%% shared secret (the Cookie) to create session encryption keys
+%% that can not be re-created if the shared secret is compromized,
+%% which should create forward secrecy. You need both nodes'
+%% key pairs and the shared secret to decrypt the traffic
+%% between the nodes.
+%%
+%% All exchanged messages uses {packet, 2} i.e 16 bit size header.
+%%
+%% The init message contains a random number and encrypted: the public key
+%% and two random numbers. The encryption is done with Key and IV hashed
+%% from the unencrypted random number and the shared secret.
+%%
+%% The other node's public key is used with the own node's private
+%% key to create a shared key that is hashed with one of the encrypted
+%% random numbers from each side to create Key and IV for the session.
+%%
+%% The start message contains the two encrypted random numbers
+%% this time encrypted with the session keys for verification
+%% by the other side, plus the rekey count. The rekey count
+%% is just there to get an early check for if the other side's
+%% maximum rekey count is acceptable, it is just an embryo
+%% of some better check. Any side may rekey earlier but if the
+%% rekey count is exceeded the connection fails. Rekey is also
+%% triggered by a timer.
+%%
+%% Subsequent encrypted messages has the sequence number and the length
+%% of the message as AAD data, and an incrementing IV. These messages
+%% has got a message type that differentiates data from ticks and rekeys.
+%% Ticks have a random size in an attempt to make them less obvious to spot.
+%%
+%% Rekeying is done by the sender that creates a new key pair and
+%% a new shared secret from the other end's public key and with
+%% this and the current key and iv hashes a new key and iv.
+%% The new public key is sent to the other end that uses it
+%% and its old private key to create the same new shared
+%% secret and from that a new key and iv.
+%% So the receiver keeps its private key, and the sender keeps
+%% the receivers public key for the connection's life time.
+%% While the sender generates a new key pair at every rekey,
+%% which changes the shared secret at every rekey.
+%%
+%% The only reaction to errors is to crash noisily (?) which will bring
+%% down the connection and hopefully produce something useful
+%% in the local log, but all the other end sees is a closed connection.
+%% -------------------------------------------------------------------------
+
+
+%% -------------------------------------------------------------------------
+%% Initialize encryption on Stream; initial handshake
+%%
+
+init(Stream) ->
+ Secret = atom_to_binary(auth:get_cookie(), latin1),
+ init(Stream, Secret).
+
+init(Stream = {_, OutStream, _}, Secret) ->
+ #keypair{ public = PubKey } = KeyPair = get_keypair(),
+ Params = params(),
+ {R2, R3, Msg} = init_msg(Params, Secret, PubKey),
+ OutStream_1 = init_send_block(OutStream, Msg, iolist_size(Msg)),
+ Stream_1 = setelement(2, Stream, OutStream_1),
+ init_recv(Stream_1, Params, Secret, KeyPair, R2, R3).
+
+init_recv(
+ {InStream, OutStream, ControllingProcessFun},
+ Params = #params{ iv = IVLen },
+ Secret, KeyPair, R2, R3) ->
+ %%
+ [InitMsg | InStream_1] = init_recv_block(InStream),
+ IVSaltLen = IVLen - 6,
+ try
+ case init_msg(Params, Secret, KeyPair, R2, R3, InitMsg) of
+ {SendParams =
+ #params{iv = <<IV2ASalt:IVSaltLen/binary, IV2ANo:48>> },
+ RecvParams,
+ SendStartMsg} ->
+ OutStream_1 =
+ init_send_block(
+ OutStream, SendStartMsg, iolist_size(SendStartMsg)),
+ [RecvStartMsg | InStream_2] = init_recv_block(InStream_1),
+ RecvParams_1 =
+ #params{
+ iv = <<IV2BSalt:IVSaltLen/binary, IV2BNo:48>> } =
+ start_msg(RecvParams, R2, R3, RecvStartMsg),
+ {{InStream_2, OutStream_1, ControllingProcessFun},
+ ?CHUNK_SIZE,
+ [0 | RecvParams_1#params{ iv = {IV2BSalt, IV2BNo} }],
+ [0 | SendParams#params{ iv = {IV2ASalt, IV2ANo} }]}
+ end
+ catch
+ Class : Reason : Stacktrace when Class =:= error ->
+ error_report(
+ [init_recv_exception,
+ {class, Class},
+ {reason, Reason},
+ {stacktrace, Stacktrace}]),
+ _ = trace({Reason, Stacktrace}),
+ exit(connection_closed)
+ end.
+
+init_msg(
+ #params{
+ hmac_algorithm = HmacAlgo,
+ aead_cipher = AeadCipher,
+ key = KeyLen,
+ iv = IVLen,
+ tag_len = TagLen }, Secret, PubKeyA) ->
+ %%
+ RLen = KeyLen + IVLen,
+ <<R1A:RLen/binary, R2A:RLen/binary, R3A:RLen/binary>> =
+ crypto:strong_rand_bytes(3 * RLen),
+ {Key1A, IV1A} = hmac_key_iv(HmacAlgo, R1A, Secret, KeyLen, IVLen),
+ Plaintext = [R2A, R3A, PubKeyA],
+ MsgLen = byte_size(R1A) + TagLen + iolist_size(Plaintext),
+ AAD = [<<MsgLen:32>>, R1A],
+ {Ciphertext, Tag} =
+ crypto:crypto_one_time_aead(
+ AeadCipher, Key1A, IV1A, Plaintext, AAD, TagLen, true),
+ Msg = [R1A, Tag, Ciphertext],
+ {R2A, R3A, Msg}.
+%%
+init_msg(
+ #params{
+ hmac_algorithm = HmacAlgo,
+ aead_cipher = AeadCipher,
+ key = KeyLen,
+ iv = IVLen,
+ tag_len = TagLen,
+ rekey_count = RekeyCount } = Params,
+ Secret, KeyPair, R2A, R3A, Msg) ->
+ %%
+ RLen = KeyLen + IVLen,
+ case Msg of
+ <<R1B:RLen/binary, Tag:TagLen/binary, Ciphertext/binary>> ->
+ {Key1B, IV1B} = hmac_key_iv(HmacAlgo, R1B, Secret, KeyLen, IVLen),
+ MsgLen = byte_size(Msg),
+ AAD = [<<MsgLen:32>>, R1B],
+ case
+ crypto:crypto_one_time_aead(
+ AeadCipher, Key1B, IV1B, Ciphertext, AAD, Tag, false)
+ of
+ <<R2B:RLen/binary, R3B:RLen/binary, PubKeyB/binary>> ->
+ SharedSecret = compute_shared_secret(KeyPair, PubKeyB),
+ %%
+ {Key2A, IV2A} =
+ hmac_key_iv(
+ HmacAlgo, SharedSecret, [R2A, R3B], KeyLen, IVLen),
+ SendParams =
+ Params#params{
+ rekey_key = PubKeyB,
+ key = Key2A, iv = IV2A },
+ %%
+ StartCleartext = [R2B, R3B, <<RekeyCount:32>>],
+ StartMsgLen = TagLen + iolist_size(StartCleartext),
+ StartAAD = <<StartMsgLen:32>>,
+ {StartCiphertext, StartTag} =
+ crypto:crypto_one_time_aead(
+ AeadCipher, Key2A, IV2A,
+ StartCleartext, StartAAD, TagLen, true),
+ StartMsg = [StartTag, StartCiphertext],
+ %%
+ {Key2B, IV2B} =
+ hmac_key_iv(
+ HmacAlgo, SharedSecret, [R2B, R3A], KeyLen, IVLen),
+ RecvParams =
+ Params#params{
+ rekey_key = KeyPair,
+ key = Key2B, iv = IV2B },
+ %%
+ {SendParams, RecvParams, StartMsg}
+ end
+ end.
+
+start_msg(
+ #params{
+ aead_cipher = AeadCipher,
+ key = Key2B,
+ iv = IV2B,
+ tag_len = TagLen,
+ rekey_count = RekeyCountA } = RecvParams, R2A, R3A, Msg) ->
+ %%
+ case Msg of
+ <<Tag:TagLen/binary, Ciphertext/binary>> ->
+ KeyLen = byte_size(Key2B),
+ IVLen = byte_size(IV2B),
+ RLen = KeyLen + IVLen,
+ MsgLen = byte_size(Msg),
+ AAD = <<MsgLen:32>>,
+ case
+ crypto:crypto_one_time_aead(
+ AeadCipher, Key2B, IV2B, Ciphertext, AAD, Tag, false)
+ of
+ <<R2A:RLen/binary, R3A:RLen/binary, RekeyCountB:32>>
+ when RekeyCountA =< (RekeyCountB bsl 2),
+ RekeyCountB =< (RekeyCountA bsl 2) ->
+ RecvParams#params{ rekey_count = RekeyCountB }
+ end
+ end.
+
+hmac_key_iv(HmacAlgo, MacKey, Data, KeyLen, IVLen) ->
+ <<Key:KeyLen/binary, IV:IVLen/binary>> =
+ crypto:macN(hmac, HmacAlgo, MacKey, Data, KeyLen + IVLen),
+ {Key, IV}.
+
+
+init_send_block(OutStream, Chunk, Size) ->
+ OutStream_1 = send_block(OutStream, Chunk, Size),
+ if
+ hd(OutStream_1) =:= closed ->
+ error_report(
+ [?FUNCTION_NAME,
+ {reason, closed}]),
+ _ = trace({?FUNCTION_NAME, closed}),
+ exit(connection_closed);
+ true ->
+ OutStream_1
+ end.
+
+init_recv_block(InStream) ->
+ Result = [Data | _] = recv_block(InStream),
+ if
+ Data =:= closed ->
+ error_report(
+ [?FUNCTION_NAME,
+ {reason, closed}]),
+ _ = trace({?FUNCTION_NAME, closed}),
+ exit(connection_closed);
+ true ->
+ Result
+ end.
+
+
+%% -------------------------------------------------------------------------
+encrypt_and_send_chunk(
+ OutStream,
+ [Seq |
+ Params =
+ #params{ rekey_count = RekeyCount,
+ rekey_time = RekeyTime,
+ rekey_timestamp = RekeyTimestamp }],
+ Chunk, Size) ->
+ %%
+ Timestamp = timestamp(),
+ if
+ RekeyCount =< Seq;
+ RekeyTimestamp + RekeyTime =< Timestamp ->
+ {OutStream_1, Params_1} =
+ encrypt_and_send_rekey_chunk(
+ OutStream, Seq, Params, Timestamp),
+ if
+ hd(OutStream_1) =:= closed ->
+ {OutStream_1, [0 | Params_1]};
+ true ->
+ {encrypt_and_send_chunk(
+ OutStream_1, 0, Params_1, Chunk, Size),
+ [1 | Params_1]}
+ end;
+ true ->
+ {encrypt_and_send_chunk(OutStream, Seq, Params, Chunk, Size),
+ [Seq + 1 | Params]}
+ end.
+
+encrypt_and_send_chunk(OutStream, Seq, Params, Chunk, 0) -> % Tick
+ <<>> = Chunk, % ASSERT
+ %% Ticks are sent as a somewhat random size block to make
+ %% it less obvious to spot
+ TickSize = 7 + rand:uniform(56),
+ TickData = binary:copy(<<0>>, TickSize),
+ encrypt_and_send_block(
+ OutStream, Seq, Params, [?TICK_CHUNK, TickData], 1 + TickSize);
+encrypt_and_send_chunk(OutStream, Seq, Params, Chunk, Size) ->
+ encrypt_and_send_block(
+ OutStream, Seq, Params, [?DATA_CHUNK, Chunk], 1 + Size).
+
+encrypt_and_send_rekey_chunk(
+ OutStream, Seq,
+ Params =
+ #params{
+ rekey_key = PubKeyB,
+ key = Key,
+ iv = {IVSalt, IVNo},
+ hmac_algorithm = HmacAlgo },
+ Timestamp) ->
+ %%
+ KeyLen = byte_size(Key),
+ IVSaltLen = byte_size(IVSalt),
+ #keypair{ public = PubKeyA } = KeyPair = get_new_keypair(),
+ SharedSecret = compute_shared_secret(KeyPair, PubKeyB),
+ IV = <<(IVNo + Seq):48>>,
+ {Key_1, <<IVSalt_1:IVSaltLen/binary, IVNo_1:48>>} =
+ hmac_key_iv(
+ HmacAlgo, SharedSecret, [Key, IVSalt, IV],
+ KeyLen, IVSaltLen + 6),
+ %%
+ {encrypt_and_send_block(
+ OutStream, Seq, Params,
+ [?REKEY_CHUNK, PubKeyA], 1 + byte_size(PubKeyA)),
+ Params#params{
+ key = Key_1, iv = {IVSalt_1, IVNo_1},
+ rekey_timestamp = Timestamp }}.
+
+encrypt_and_send_block(OutStream, Seq, Params, Block, Size) ->
+ {EncryptedBlock, EncryptedSize} =
+ encrypt_block(Seq, Params, Block, Size),
+ send_block(OutStream, EncryptedBlock, EncryptedSize).
+
+encrypt_block(
+ Seq,
+ #params{
+ aead_cipher = AeadCipher,
+ iv = {IVSalt, IVNo}, key = Key, tag_len = TagLen },
+ Block, Size) ->
+ %%
+ EncryptedSize = Size + TagLen,
+ AAD = <<Seq:32, EncryptedSize:32>>,
+ IVBin = <<IVSalt/binary, (IVNo + Seq):48>>,
+ {Ciphertext, CipherTag} =
+ crypto:crypto_one_time_aead(
+ AeadCipher, Key, IVBin, Block, AAD, TagLen, true),
+ EncryptedBlock = [Ciphertext, CipherTag],
+ {EncryptedBlock, EncryptedSize}.
+
+%% Send packet=2
+%%
+send_block(OutStream, Block, Size) ->
+ Msg =
+ if
+ is_binary(Block) -> [<<Size:16>>, Block];
+ is_list(Block) -> [<<Size:16>> | Block]
+ end,
+ (hd(OutStream))(OutStream, Msg).
+
+
+%% -------------------------------------------------------------------------
+
+recv_and_decrypt_chunk(InStream, SeqParams = [Seq | Params]) ->
+ case recv_block(InStream) of
+ Result = [closed | _] ->
+ {Result, SeqParams};
+ [Ciphertext | InStream_1] ->
+ case decrypt_block(Seq, Params, Ciphertext) of
+ <<?DATA_CHUNK, DataChunk/binary>> ->
+ {[DataChunk | InStream_1], [Seq + 1 | Params]};
+ <<?TICK_CHUNK, _/binary>> ->
+ {[<<>> | InStream_1], [Seq + 1 | Params]};
+ <<?REKEY_CHUNK, RekeyChunk>> ->
+ case decrypt_rekey(Params, RekeyChunk) of
+ Params_1 = #params{} ->
+ recv_and_decrypt_chunk(
+ InStream_1, [0 | Params_1]);
+ Problem when is_atom(Problem) ->
+ error_report(
+ [?FUNCTION_NAME, {reason, Problem}]),
+ {[closed | InStream_1], [Seq + 1 | Params]}
+ end;
+ <<_UnknownChunk/binary>> ->
+ error_report([?FUNCTION_NAME, {reason, unknown_chunk}]),
+ {[closed | InStream_1], [Seq + 1 | Params]};
+ Problem when is_atom(Problem) ->
+ error_report([?FUNCTION_NAME, {reason, Problem}]),
+ {[closed | InStream_1], [Seq + 1 | Params]}
+ end
+ end.
+
+%% Non-optimized receive packet=2
+%%
+recv_block(InStream) ->
+ Result_1 = [Data | InStream_1] = (hd(InStream))(InStream, 2),
+ if
+ Data =:= closed ->
+ Result_1;
+ is_binary(Data) ->
+ recv_block(InStream_1, Data);
+ is_list(Data) ->
+ recv_block(InStream_1, iolist_to_binary(Data))
+ end.
+%%
+recv_block(InStream, <<0:16>>) ->
+ [<<>> | InStream];
+recv_block(InStream, <<Size:16>>) ->
+ case (hd(InStream))(InStream, Size) of
+ [Data | InStream_1] when is_list(Data) ->
+ [iolist_to_binary(Data) | InStream_1];
+ Result = [Data | _]
+ when is_binary(Data);
+ Data =:= closed ->
+ Result
+ end.
+
+decrypt_block(
+ Seq, #params{ rekey_count = RekeyCount }, _Ciphertext)
+ when RekeyCount =:= Seq ->
+ %% This was one chunk too many without rekeying
+ rekey_overdue;
+decrypt_block(
+ Seq,
+ #params{
+ aead_cipher = AeadCipher,
+ iv = {IVSalt, IVNo}, key = Key, tag_len = TagLen },
+ Ciphertext) ->
+ %%
+ CiphertextSize = byte_size(Ciphertext),
+ if
+ CiphertextSize < TagLen ->
+ decrypt_short_block;
+ true ->
+ AAD = <<Seq:32, CiphertextSize:32>>,
+ IV = <<IVSalt/binary, (IVNo + Seq):48>>,
+ Size = CiphertextSize - TagLen,
+ <<EncryptedBlock:Size/binary, CipherTag:TagLen/binary>> =
+ Ciphertext,
+ case
+ crypto:crypto_one_time_aead(
+ AeadCipher, Key, IV, EncryptedBlock, AAD, CipherTag,
+ false)
+ of
+ Block when is_binary(Block) ->
+ Block;
+ error ->
+ decrypt_error
+ end
+ end.
+
+decrypt_rekey(
+ Params =
+ #params{
+ iv = IV,
+ key = Key,
+ rekey_key = #keypair{public = PubKeyA} = KeyPair,
+ hmac_algorithm = HmacAlgorithm},
+ RekeyChunk) ->
+ %%
+ PubKeyLen = byte_size(PubKeyA),
+ case RekeyChunk of
+ <<PubKeyB:PubKeyLen/binary>> ->
+ SharedSecret = compute_shared_secret(KeyPair, PubKeyB),
+ KeyLen = byte_size(Key),
+ IVLen = byte_size(IV),
+ IVSaltLen = IVLen - 6,
+ {Key_1, <<IVSalt_1:IVSaltLen/binary, IVNo_1:48>>} =
+ hmac_key_iv(
+ HmacAlgorithm, SharedSecret, [Key, IV], KeyLen, IVLen),
+ Params#params{
+ iv = {IVSalt_1, IVNo_1},
+ key = Key_1 };
+ _ ->
+ decrypt_bad_rekey_chunk
+ end.
+
+
+%% -------------------------------------------------------------------------
+
+timestamp() ->
+ erlang:monotonic_time(second).
+
+
+error_report(Report) ->
+ error_logger:error_report(Report).
+
+-ifdef(undefined).
+info_report(Report) ->
+ error_logger:info_report(Report).
+-endif.
+
+%% Trace point
+trace(Term) -> Term.
diff --git a/lib/ssl/test/dist_cryptcookie.erl b/lib/ssl/test/dist_cryptcookie.erl
index a1afc6ae9d..4850813d99 100644
--- a/lib/ssl/test/dist_cryptcookie.erl
+++ b/lib/ssl/test/dist_cryptcookie.erl
@@ -38,6 +38,586 @@
-define(PROTOCOL, cryptcookie).
+supported() ->
+ cryptcookie:supported().
+
+%% ------------------------------------------------------------
+protocol() ->
+ cryptcookie:start_keypair_server(),
+ ?PROTOCOL.
+
+%% ------------------------------------------------------------
+start_dist_ctrl(Stream) ->
+ %%
+ ControllingProcess = self(),
+ Tag = make_ref(),
+ Pid =
+ spawn_opt(
+ fun () ->
+ receive
+ {Tag, Alias, {start, Stream_2}} ->
+ _ = crypto:rand_seed_alg(crypto_cache),
+ CCInit = cryptcookie:init(Stream_2),
+ reply(Alias, trace(started)),
+ handshake(CCInit, Tag, ControllingProcess)
+ end
+ end,
+ [link,
+ {priority, max},
+ {message_queue_data, off_heap},
+ {fullsweep_after, 0}]),
+ Stream_1 = (element(3, Stream))(Stream, Pid),
+ DistCtrlHandle = {Pid, Tag},
+ started = call(DistCtrlHandle, {start, Stream_1}),
+ DistCtrlHandle.
+
+
+call({Pid, Tag}, Request) ->
+ Ref = Alias = monitor(process, Pid, [{alias, reply_demonitor}]),
+ Pid ! {Tag, Alias, Request},
+ receive
+ {Alias, Response} ->
+ Response;
+ {'DOWN', Ref, process, Pid, Reason} ->
+ error({dist_ctrl, Reason})
+ end.
+
+reply(Alias, Response) when is_reference(Alias) ->
+ Alias ! {Alias, Response},
+ ok.
+
+%% ------------------------------------------------------------
+controlling_process(DistCtrlHandle, Pid) ->
+ call(DistCtrlHandle, {controlling_process, Pid}),
+ DistCtrlHandle.
+
+%% ------------------------------------------------------------
+hs_data(NetAddress, {DistCtrl, _} = DistCtrlHandle) ->
+ #hs_data{
+ socket = DistCtrlHandle,
+ f_send =
+ fun (S, Packet) when S =:= DistCtrlHandle ->
+ call(S, {send, Packet})
+ end,
+ f_recv =
+ fun (S, 0, infinity) when S =:= DistCtrlHandle ->
+ call(S, recv)
+ end,
+ f_setopts_pre_nodeup = f_ok(DistCtrlHandle),
+ f_setopts_post_nodeup = f_ok(DistCtrlHandle),
+ f_address =
+ fun (S, Node) when S =:= DistCtrlHandle ->
+ inet_epmd_dist:f_address(NetAddress, Node)
+ end,
+ f_getll =
+ fun (S) when S =:= DistCtrlHandle ->
+ {ok, DistCtrl}
+ end,
+ f_handshake_complete =
+ fun (S, _Node, DistHandle) when S =:= DistCtrlHandle ->
+ call(S, {handshake_complete, DistHandle})
+ end,
+ %%
+ %%
+ mf_tick =
+ fun (S) when S =:= DistCtrlHandle ->
+ DistCtrl ! dist_tick
+ end }.
+
+f_ok(DistCtrlHandle) ->
+ fun (S) when S =:= DistCtrlHandle -> ok end.
+
+
+%% -------------------------------------------------------------------------
+%% net_kernel distribution handshake in progress
+%%
+
+handshake(
+ {Stream, ChunkSize, DecryptState, EncryptState},
+ Tag, ControllingProcess) ->
+ handshake(
+ Stream, Tag, ControllingProcess,
+ ChunkSize, DecryptState, EncryptState).
+%%
+handshake(
+ Stream = {InStream, OutStream, ControllingProcessFun},
+ Tag, ControllingProcess, ChunkSize, DecryptState, EncryptState) ->
+ receive
+ {Tag, From, {controlling_process, NewControllingProcess}} ->
+ link(NewControllingProcess),
+ unlink(ControllingProcess),
+ reply(From, ok),
+ handshake(
+ Stream, Tag, NewControllingProcess,
+ ChunkSize, DecryptState, EncryptState);
+ {Tag, From, {handshake_complete, DistHandle}} ->
+ InputHandler =
+ spawn_opt(
+ fun () ->
+ link(ControllingProcess),
+ receive
+ {Tag, dist_handle, DistHandle, InStream_2} ->
+ input_handler_start(
+ InStream_2, DecryptState, DistHandle)
+ end
+ end,
+ [link,
+ {priority, normal},
+ {message_queue_data, off_heap},
+ {fullsweep_after, 0}]),
+ _ = monitor(process, InputHandler), % For the benchmark test
+ {InStream_1, OutStream_1, _} =
+ ControllingProcessFun(Stream, InputHandler),
+ false = erlang:dist_ctrl_set_opt(DistHandle, get_size, true),
+ ok = erlang:dist_ctrl_input_handler(DistHandle, InputHandler),
+ InputHandler ! {Tag, dist_handle, DistHandle, InStream_1},
+ reply(From, ok),
+ process_flag(priority, normal),
+ output_handler_start(
+ OutStream_1, EncryptState, ChunkSize, DistHandle);
+ %%
+ {Tag, From, {send, Data}} ->
+ {OutStream_1, EncryptState_1} =
+ cryptcookie:encrypt_and_send_chunk(
+ OutStream, EncryptState, Data, iolist_size(Data)),
+ if
+ hd(OutStream_1) =:= closed ->
+ reply(From, {error, closed}),
+ death_row({send, trace(closed)});
+ true ->
+ reply(From, ok),
+ handshake(
+ setelement(2, Stream, OutStream_1),
+ Tag, ControllingProcess,
+ ChunkSize, DecryptState, EncryptState_1)
+ end;
+ {Tag, From, recv} ->
+ {[Data | InStream_1], DecryptState_1} =
+ cryptcookie:recv_and_decrypt_chunk(InStream, DecryptState),
+ if
+ Data =:= closed ->
+ reply(From, {error, Data}),
+ death_row({recv, trace(Data)});
+ true ->
+ reply(From, {ok, binary_to_list(Data)}),
+ handshake(
+ setelement(1, Stream, InStream_1),
+ Tag, ControllingProcess,
+ ChunkSize, DecryptState_1, EncryptState)
+ end;
+ %%
+ Alien ->
+ _ = trace(Alien),
+ handshake(
+ Stream, Tag, ControllingProcess,
+ ChunkSize, DecryptState, EncryptState)
+ end.
+
+
+%% -------------------------------------------------------------------------
+%% Output handler process
+%%
+%% Await an event about what to do; fetch dist data from the VM,
+%% or send a dist tick.
+%%
+%% In case we are overloaded we could get many accumulated
+%% dist_tick messages; make sure to flush all of them
+%% before proceeding with what to do. But, do not use selective
+%% receive since that does not perform well when there are
+%% many messages in the process mailbox.
+
+%% Entry function
+output_handler_start(OutStream, EncryptState, ChunkSize, DistHandle) ->
+ try
+ erlang:dist_ctrl_get_data_notification(DistHandle),
+ output_handler(OutStream, EncryptState, [ChunkSize|DistHandle])
+ catch
+ Class : Reason : Stacktrace when Class =:= error ->
+ error_report(
+ [output_handler_exception,
+ {class, Class},
+ {reason, Reason},
+ {stacktrace, Stacktrace}]),
+ erlang:raise(Class, Reason, Stacktrace)
+ end.
+
+%% Loop top
+%%
+%% Awaiting outbound data or tick
+%%
+output_handler(OutStream, EncryptState, CS_DH) ->
+ receive
+ Msg ->
+ case Msg of
+ dist_data ->
+ output_handler_data(
+ OutStream, EncryptState, CS_DH);
+ dist_tick ->
+ output_handler_tick(
+ OutStream, EncryptState, CS_DH);
+ _ ->
+ %% Ignore
+ _ = trace(Msg),
+ output_handler(OutStream, EncryptState, CS_DH)
+ end
+ end.
+
+%% We have received at least one dist_tick but no dist_data message
+%%
+output_handler_tick(OutStream, EncryptState, CS_DH) ->
+ receive
+ Msg ->
+ case Msg of
+ dist_data ->
+ output_handler_data(
+ OutStream, EncryptState, CS_DH);
+ dist_tick ->
+ %% Consume all dist_tick messages
+ %%
+ output_handler_tick(OutStream, EncryptState, CS_DH);
+ _ ->
+ %% Ignore
+ _ = trace(Msg),
+ output_handler_tick(OutStream, EncryptState, CS_DH)
+ end
+ after 0 ->
+ {OutStream_1, EncryptState_1} =
+ cryptcookie:encrypt_and_send_chunk(
+ OutStream, EncryptState, <<>>, 0),
+ if
+ hd(OutStream_1) =:= closed ->
+ death_row({send_tick, trace(closed)});
+ true ->
+ output_handler(OutStream_1, EncryptState_1, CS_DH)
+ end
+ end.
+
+%% We have received a dist_data notification
+%%
+output_handler_data(OutStream, EncryptState, CS_DH) ->
+ {OutStream_1, EncryptState_1} =
+ output_handler_xfer(OutStream, EncryptState, CS_DH, [], 0, []),
+ erlang:dist_ctrl_get_data_notification(tl(CS_DH)),
+ output_handler(OutStream_1, EncryptState_1, CS_DH).
+
+%% Get outbound data from VM; encrypt and send,
+%% until the VM has no more
+%%
+%% Front,Size,Rear is an Okasaki queue of binaries with total byte Size
+%%
+output_handler_xfer(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear)
+ when hd(CS_DH) =< Size ->
+ %%
+ %% We have a full chunk or more
+ %% -> collect one chunk or less and send
+ output_handler_collect(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear);
+output_handler_xfer(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear) ->
+ %% when Size < hd(CS_DH) ->
+ %%
+ %% We do not have a full chunk -> try to fetch more from VM
+ case erlang:dist_ctrl_get_data(tl(CS_DH)) of
+ none ->
+ if
+ Size =:= 0 ->
+ %% No more data from VM, nothing buffered
+ %% -> done, for now
+ {OutStream, EncryptState};
+ true ->
+ %% The VM had no more -> send what we have
+ output_handler_collect(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear)
+ end;
+ {Len,Iov} ->
+ output_handler_enq(
+ OutStream, EncryptState, CS_DH,
+ Front, Size + 4 + Len, [<<Len:32>>|Rear],
+ Iov)
+ end.
+
+%% Enqueue VM data while splitting large binaries into
+%% chunk size; hd(CS_DH)
+%%
+output_handler_enq(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear, []) ->
+ output_handler_xfer(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear);
+output_handler_enq(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear, [Bin|Iov]) ->
+ output_handler_enq(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear, Iov, Bin).
+%%
+output_handler_enq(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear, Iov, Bin) ->
+ BinSize = byte_size(Bin),
+ ChunkSize = hd(CS_DH),
+ if
+ BinSize =< ChunkSize ->
+ output_handler_enq(
+ OutStream, EncryptState, CS_DH, Front, Size, [Bin|Rear],
+ Iov);
+ true ->
+ <<Bin1:ChunkSize/binary, Bin2/binary>> = Bin,
+ output_handler_enq(
+ OutStream, EncryptState, CS_DH, Front, Size, [Bin1|Rear],
+ Iov, Bin2)
+ end.
+
+%% Collect small binaries into chunks of at most
+%% chunk size; hd(CS_DH)
+%%
+output_handler_collect(OutStream, EncryptState, CS_DH, [], Zero, []) ->
+ 0 = Zero, % ASSERT
+ %% No more enqueued -> try to get more form VM
+ output_handler_xfer(OutStream, EncryptState, CS_DH, [], Zero, []);
+output_handler_collect(OutStream, EncryptState, CS_DH, Front, Size, Rear) ->
+ output_handler_collect(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear, [], 0).
+%%
+output_handler_collect(
+ OutStream, EncryptState, CS_DH, [], Zero, [], Acc, DataSize) ->
+ 0 = Zero, % ASSERT
+ output_handler_chunk(
+ OutStream, EncryptState, CS_DH, [], Zero, [], Acc, DataSize);
+output_handler_collect(
+ OutStream, EncryptState, CS_DH, [], Size, Rear, Acc, DataSize) ->
+ %% Okasaki queue transfer Rear -> Front
+ output_handler_collect(
+ OutStream, EncryptState, CS_DH, lists:reverse(Rear), Size, [],
+ Acc, DataSize);
+output_handler_collect(
+ OutStream, EncryptState, CS_DH, [Bin|Iov] = Front, Size, Rear,
+ Acc, DataSize) ->
+ ChunkSize = hd(CS_DH),
+ BinSize = byte_size(Bin),
+ DataSize_1 = DataSize + BinSize,
+ if
+ ChunkSize < DataSize_1 ->
+ %% Bin does not fit in chunk -> send Acc
+ output_handler_chunk(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear,
+ Acc, DataSize);
+ DataSize_1 < ChunkSize ->
+ %% Chunk not full yet -> try to accumulate more
+ output_handler_collect(
+ OutStream, EncryptState, CS_DH, Iov, Size - BinSize, Rear,
+ [Bin|Acc], DataSize_1);
+ true -> % DataSize_1 == ChunkSize ->
+ %% Optimize one iteration; Bin fits exactly
+ %% -> accumulate and send
+ output_handler_chunk(
+ OutStream, EncryptState, CS_DH, Iov, Size - BinSize, Rear,
+ [Bin|Acc], DataSize_1)
+ end.
+
+%% Encrypt and send a chunk
+%%
+output_handler_chunk(
+ OutStream, EncryptState, CS_DH, Front, Size, Rear, Acc, DataSize) ->
+ Data = lists:reverse(Acc),
+ {OutStream_1, EncryptState_1} =
+ cryptcookie:encrypt_and_send_chunk(
+ OutStream, EncryptState, Data, DataSize),
+ if
+ hd(OutStream_1) =:= closed ->
+ death_row({send_chunk, trace(closed)});
+ true ->
+ output_handler_collect(
+ OutStream_1, EncryptState_1, CS_DH, Front, Size, Rear)
+ end.
+
+
+%% -------------------------------------------------------------------------
+%% Input handler process
+%%
+
+%% Entry function
+input_handler_start(InStream, DecryptState, DistHandle) ->
+ try
+ input_handler(InStream, DecryptState, DistHandle)
+ catch
+ Class : Reason : Stacktrace when Class =:= error ->
+ error_report(
+ [input_handler_exception,
+ {class, Class},
+ {reason, Reason},
+ {stacktrace, Stacktrace}]),
+ erlang:raise(Class, Reason, Stacktrace)
+ end.
+
+%% Loop top
+input_handler(InStream, DecryptState, DistHandle) ->
+ %% Shortcut into the loop
+ {InStream_1, DecryptState_1, Chunk} =
+ input_chunk(InStream, DecryptState),
+ input_handler(
+ InStream_1, DecryptState_1, DistHandle, Chunk, [], byte_size(Chunk)).
+%%
+input_handler(InStream, DecryptState, DistHandle, First, Buffer, Size) ->
+ %% Size is size of First + Buffer
+ case First of
+ <<Packet1Size:32, Packet1:Packet1Size/binary,
+ Packet2Size:32, Packet2:Packet2Size/binary, Rest/binary>> ->
+ erlang:dist_ctrl_put_data(DistHandle, Packet1),
+ erlang:dist_ctrl_put_data(DistHandle, Packet2),
+ input_handler(
+ InStream, DecryptState, DistHandle,
+ Rest, Buffer, Size - (8 + Packet1Size + Packet2Size));
+ <<PacketSize:32, Packet:PacketSize/binary, Rest/binary>> ->
+ erlang:dist_ctrl_put_data(DistHandle, Packet),
+ input_handler(
+ InStream, DecryptState, DistHandle,
+ Rest, Buffer, Size - (4 + PacketSize));
+ <<PacketSize:32, PacketStart/binary>> ->
+ %% Partial packet in First
+ input_handler(
+ InStream, DecryptState, DistHandle,
+ PacketStart, Buffer, Size - 4, PacketSize);
+ Tick = <<>> ->
+ erlang:dist_ctrl_put_data(DistHandle, Tick),
+ if
+ Buffer =:= [] ->
+ Size = 0, % ASSERT
+ input_handler(InStream, DecryptState, DistHandle);
+ true ->
+ [First_1 | Buffer_1] = lists:reverse(Buffer),
+ input_handler(
+ InStream, DecryptState, DistHandle,
+ First_1, Buffer_1, Size)
+ end;
+ <<Bin/binary>> ->
+ %% Partial header in First
+ if
+ 4 =< Size ->
+ %% Complete header in First + Buffer
+ {First_1, Buffer_1, PacketSize} =
+ input_get_packet_size(Bin, lists:reverse(Buffer)),
+ input_handler(
+ InStream, DecryptState, DistHandle,
+ First_1, Buffer_1, Size - 4, PacketSize);
+ true ->
+ %% Incomplete header received so far
+ {InStream_1, DecryptState_1, Chunk} =
+ input_chunk(InStream, DecryptState),
+ input_handler(
+ InStream_1, DecryptState_1, DistHandle,
+ Bin, [Chunk|Buffer], Size + byte_size(Chunk))
+ end
+ end.
+%%
+input_handler(
+ InStream, DecryptState, DistHandle,
+ PacketStart, Buffer, Size, PacketSize) ->
+ %%
+ %% Size is size of PacketStart + Buffer
+ RestSize = Size - PacketSize,
+ if
+ RestSize < 0 ->
+ %% Incomplete packet received so far
+ {InStream_1, DecryptState_1, Chunk} =
+ input_chunk(InStream, DecryptState),
+ input_handler(
+ InStream_1, DecryptState_1, DistHandle,
+ PacketStart, [Chunk|Buffer], Size + byte_size(Chunk),
+ PacketSize);
+ 0 < RestSize, Buffer =:= [] ->
+ %% Rest data in PacketStart
+ <<Packet:PacketSize/binary, Rest/binary>> = PacketStart,
+ erlang:dist_ctrl_put_data(DistHandle, Packet),
+ input_handler(
+ InStream, DecryptState, DistHandle, Rest, [], RestSize);
+ Buffer =:= [] -> % RestSize == 0, Size == 0
+ %% No rest data
+ erlang:dist_ctrl_put_data(DistHandle, PacketStart),
+ input_handler(InStream, DecryptState, DistHandle);
+ true ->
+ %% Split packet from rest data
+ LastBin = hd(Buffer),
+ <<PacketLast:(byte_size(LastBin) - RestSize)/binary,
+ Rest/binary>> = LastBin,
+ Packet = [PacketStart|lists:reverse(tl(Buffer), PacketLast)],
+ erlang:dist_ctrl_put_data(DistHandle, Packet),
+ input_handler(
+ InStream, DecryptState, DistHandle, Rest, [], RestSize)
+ end.
+
+input_get_packet_size(First, [Bin|Buffer]) ->
+ MissingSize = 4 - byte_size(First),
+ if
+ MissingSize =< byte_size(Bin) ->
+ <<Last:MissingSize/binary, Rest/binary>> = Bin,
+ <<PacketSize:32>> = <<First/binary, Last/binary>>,
+ {Rest, lists:reverse(Buffer), PacketSize};
+ true ->
+ input_get_packet_size(<<First/binary, Bin/binary>>, Buffer)
+ end.
+
+input_chunk(InStream, DecryptState) ->
+ {[Chunk | InStream_1], DecryptState_1} =
+ cryptcookie:recv_and_decrypt_chunk(InStream, DecryptState),
+ if
+ is_atom(Chunk) ->
+ _ = Chunk =:= closed
+ orelse
+ error_report(
+ [?FUNCTION_NAME,
+ {reason, Chunk}]),
+ _ = trace({?FUNCTION_NAME, Chunk}),
+ exit(connection_closed);
+ is_binary(Chunk) ->
+ {InStream_1, DecryptState_1, Chunk}
+ end.
+
+
+%% -------------------------------------------------------------------------
+
+%% Wait for getting killed by process link,
+%% and if that does not happen - drop dead
+
+death_row(Reason) ->
+ receive
+ after 5000 ->
+ death_row_timeout(Reason)
+ end.
+
+death_row_timeout(Reason) ->
+ error_report(
+ [?FUNCTION_NAME,
+ {reason, Reason},
+ {pid, self()}]),
+ exit(Reason).
+
+%% -------------------------------------------------------------------------
+
+error_report(Report) ->
+ error_logger:error_report(Report).
+
+-ifdef(undefined).
+info_report(Report) ->
+ error_logger:info_report(Report).
+-endif.
+
+%% Trace point
+trace(Term) -> Term.
+
+
+
+
+
+
+
+%% ------------------------------------------------------------
+%% ------------------------------------------------------------
+%% ------------------------------------------------------------
+%% ------------------------------------------------------------
+%% ------------------------------------------------------------
+%% ------------------------------------------------------------
+%% ------------------------------------------------------------
+
+-ifdef(undefined).
+
%% ------------------------------------------------------------
protocol() ->
start_keypair_server(),
@@ -795,14 +1375,14 @@ output_handler_enq(OutStream, SeqParams, Front, Size, Rear, Iov, Bin) ->
%% Collect small binaries into chunks of at most ?CHUNK_SIZE
%%
output_handler_collect(OutStream, SeqParams, [], Zero, []) ->
- 0 = Zero, % Assert
+ 0 = Zero, % ASSERT
%% No more enqueued -> try to get more form VM
output_handler_xfer(OutStream, SeqParams);
output_handler_collect(OutStream, SeqParams, Front, Size, Rear) ->
output_handler_collect(OutStream, SeqParams, Front, Size, Rear, [], 0).
%%
output_handler_collect(OutStream, SeqParams, [], Zero, [], Acc, DataSize) ->
- 0 = Zero, % Assert
+ 0 = Zero, % ASSERT
output_handler_chunk(OutStream, SeqParams, [], Zero, [], Acc, DataSize);
output_handler_collect(OutStream, SeqParams, [], Size, Rear, Acc, DataSize) ->
%% Okasaki queue transfer Rear -> Front
@@ -1205,3 +1785,5 @@ info_report(Report) ->
%% Trace point
trace(Term) -> Term.
+
+-endif.