diff options
author | Raimo Niskanen <raimo@erlang.org> | 2023-01-18 22:33:06 +0100 |
---|---|---|
committer | Raimo Niskanen <raimo@erlang.org> | 2023-02-09 15:33:37 +0100 |
commit | 8a6a620b14d9d34b10682eeee6478c2e3b250f4d (patch) | |
tree | 984693fa8a5cd9c3ae2e99b429410e6cfbf7d8a2 /lib/ssl/test | |
parent | 764c7aff16c6a55963500b5494075165a2332fbd (diff) | |
download | erlang-8a6a620b14d9d34b10682eeee6478c2e3b250f4d.tar.gz |
Break out cryptcookie as a utility module
Diffstat (limited to 'lib/ssl/test')
-rw-r--r-- | lib/ssl/test/Makefile | 1 | ||||
-rw-r--r-- | lib/ssl/test/cryptcookie.erl | 665 | ||||
-rw-r--r-- | lib/ssl/test/dist_cryptcookie.erl | 586 |
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. |