1%% 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 2004-2020. All Rights Reserved. 5%% 6%% Licensed under the Apache License, Version 2.0 (the "License"); 7%% you may not use this file except in compliance with the License. 8%% You may obtain a copy of the License at 9%% 10%% http://www.apache.org/licenses/LICENSE-2.0 11%% 12%% Unless required by applicable law or agreed to in writing, software 13%% distributed under the License is distributed on an "AS IS" BASIS, 14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15%% See the License for the specific language governing permissions and 16%% limitations under the License. 17%% 18%% %CopyrightEnd% 19%% 20 21%% 22%%---------------------------------------------------------------------- 23-module(ssh_test_lib). 24 25-export([ 26connect/2, 27connect/3, 28daemon/1, 29daemon/2, 30daemon/3, 31daemon_port/1, 32daemon_port/2, 33gen_tcp_connect/3, 34open_sshc/3, 35open_sshc/4, 36open_sshc_cmd/3, 37open_sshc_cmd/4, 38std_daemon/2, 39std_daemon1/2, 40std_connect/4, 41std_simple_sftp/3, 42std_simple_sftp/4, 43std_simple_exec/3, 44std_simple_exec/4, 45start_shell/2, 46start_shell/3, 47start_io_server/0, 48init_io_server/1, 49loop_io_server/2, 50io_request/5, 51io_reply/3, 52reply/2, 53rcv_expected/3, 54rcv_lingering/1, 55receive_exec_result/1, 56receive_exec_result_or_fail/1, 57receive_exec_end/2, 58receive_exec_end/3, 59receive_exec_result/3, 60failfun/2, 61hostname/0, 62del_dirs/1, 63del_dir_contents/1, 64do_del_files/2, 65openssh_sanity_check/1, 66default_algorithms/1, 67default_algorithms/3, 68default_algorithms/2, 69run_fake_ssh/1, 70extract_algos/1, 71get_atoms/1, 72intersection/2, 73intersect/2, 74intersect_bi_dir/1, 75some_empty/1, 76sort_spec/1, 77sshc/1, 78ssh_type/0, 79ssh_type1/0, 80installed_ssh_version/1, 81algo_intersection/2, 82to_atoms/1, 83ssh_supports/2, 84has_inet6_address/0, 85open_port/1, 86open_port/2, 87sleep_millisec/1, 88sleep_microsec/1, 89busy_wait/2, 90get_kex_init/1, 91get_kex_init/3, 92expected_state/1, 93random_chars/1, 94create_random_dir/1, 95match_ip/2, 96match_ip0/2, 97match_ip1/2, 98mangle_connect_address/1, 99mangle_connect_address/2, 100loopback/1, 101mangle_connect_address1/2, 102ntoa/1, 103try_enable_fips_mode/0, 104is_cryptolib_fips_capable/0, 105report/2, 106lc_name_in/1, 107ptty_supported/0, 108has_WSL/0, 109winpath_to_linuxpath/1, 110copy_recursive/2, 111mk_dir_path/1, 112setup_all_user_host_keys/1, 113setup_all_user_host_keys/2, 114setup_all_user_host_keys/3, 115setup_all_host_keys/1, 116setup_all_host_keys/2, 117setup_all_user_keys/2, 118setup_user_key/3, 119setup_host_key_create_dir/3, 120setup_host_key/3, 121setup_known_host/3, 122get_addr_str/0, 123file_base_name/2 124 ]). 125 126-include_lib("common_test/include/ct.hrl"). 127-include("ssh_transport.hrl"). 128-include_lib("kernel/include/file.hrl"). 129-include("ssh_test_lib.hrl"). 130 131%%%---------------------------------------------------------------- 132connect(Port, Options) when is_integer(Port) -> 133 connect(hostname(), Port, Options). 134 135connect(any, Port, Options) -> 136 connect(hostname(), Port, Options); 137connect(Host, Port, Options) -> 138 R = ssh:connect(Host, Port, Options), 139 ct:log("~p:~p ssh:connect(~p, ~p, ~p)~n -> ~p",[?MODULE,?LINE,Host, Port, Options, R]), 140 {ok, ConnectionRef} = R, 141 ConnectionRef. 142 143%%%---------------------------------------------------------------- 144daemon(Options) -> 145 daemon(any, 0, Options). 146 147daemon(Port, Options) when is_integer(Port) -> 148 daemon(any, Port, Options); 149daemon(Host, Options) -> 150 daemon(Host, 0, Options). 151 152 153daemon(Host, Port, Options) -> 154 ct:log("~p:~p Calling ssh:daemon(~p, ~p, ~p)",[?MODULE,?LINE,Host,Port,Options]), 155 case ssh:daemon(Host, Port, Options) of 156 {ok, Pid} -> 157 R = ssh:daemon_info(Pid), 158 ct:log("~p:~p ssh:daemon_info(~p) ->~n ~p",[?MODULE,?LINE,Pid,R]), 159 {ok,L} = R, 160 ListenPort = proplists:get_value(port, L), 161 ListenIP = proplists:get_value(ip, L), 162 {Pid, ListenIP, ListenPort}; 163 Error -> 164 ct:log("ssh:daemon error ~p",[Error]), 165 Error 166 end. 167 168%%%---------------------------------------------------------------- 169daemon_port(Pid) -> daemon_port(0, Pid). 170 171 172daemon_port(0, Pid) -> {ok,Dinf} = ssh:daemon_info(Pid), 173 proplists:get_value(port, Dinf); 174daemon_port(Port, _) -> Port. 175 176%%%---------------------------------------------------------------- 177gen_tcp_connect(Host0, Port, Options) -> 178 Host = ssh_test_lib:ntoa(ssh_test_lib:mangle_connect_address(Host0)), 179 ct:log("~p:~p gen_tcp:connect(~p, ~p, ~p)~nHost0 = ~p", 180 [?MODULE,?LINE, Host, Port, Options, Host0]), 181 Result = gen_tcp:connect(Host, Port, Options), 182 ct:log("~p:~p Result = ~p", [?MODULE,?LINE, Result]), 183 Result. 184 185%%%---------------------------------------------------------------- 186open_sshc(Host0, Port, OptStr) -> 187 open_sshc(Host0, Port, OptStr, ""). 188 189open_sshc(Host0, Port, OptStr, ExecStr) -> 190 Cmd = open_sshc_cmd(Host0, Port, OptStr, ExecStr), 191 Result = os:cmd(Cmd), 192 ct:log("~p:~p Result = ~p", [?MODULE,?LINE, Result]), 193 Result. 194 195 196open_sshc_cmd(Host, Port, OptStr) -> 197 open_sshc_cmd(Host, Port, OptStr, ""). 198 199open_sshc_cmd(Host0, Port, OptStr, ExecStr) -> 200 Host = ssh_test_lib:ntoa(ssh_test_lib:mangle_connect_address(Host0)), 201 Cmd = lists:flatten(["ssh -p ", integer_to_list(Port), 202 " ", OptStr, 203 " ", Host, 204 " ", ExecStr]), 205 ct:log("~p:~p OpenSSH Cmd = ~p", [?MODULE,?LINE, Cmd]), 206 Cmd. 207 208%%%---------------------------------------------------------------- 209std_daemon(Config, ExtraOpts) -> 210 PrivDir = proplists:get_value(priv_dir, Config), 211 UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth 212 file:make_dir(UserDir), 213 std_daemon1(Config, 214 ExtraOpts ++ 215 [{user_dir, UserDir}, 216 {user_passwords, [{"usr1","pwd1"}]}]). 217 218std_daemon1(Config, ExtraOpts) -> 219 SystemDir = proplists:get_value(data_dir, Config), 220 {_Server, _Host, _Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, 221 {failfun, fun ssh_test_lib:failfun/2} 222 | ExtraOpts]). 223 224%%%---------------------------------------------------------------- 225std_connect(Config, Host, Port, ExtraOpts) -> 226 UserDir = proplists:get_value(priv_dir, Config), 227 _ConnectionRef = 228 ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, 229 {user_dir, UserDir}, 230 {user, "usr1"}, 231 {password, "pwd1"}, 232 {user_interaction, false} 233 | ExtraOpts]). 234 235%%%---------------------------------------------------------------- 236std_simple_sftp(Host, Port, Config) -> 237 std_simple_sftp(Host, Port, Config, []). 238 239std_simple_sftp(Host, Port, Config, Opts) -> 240 UserDir = proplists:get_value(priv_dir, Config), 241 DataFile = filename:join(UserDir, "test.data"), 242 ConnectionRef = ssh_test_lib:std_connect(Config, Host, Port, Opts), 243 {ok, ChannelRef} = ssh_sftp:start_channel(ConnectionRef), 244 Data = crypto:strong_rand_bytes(proplists:get_value(std_simple_sftp_size,Config,10)), 245 ok = ssh_sftp:write_file(ChannelRef, DataFile, Data), 246 {ok,ReadData} = file:read_file(DataFile), 247 ok = ssh:close(ConnectionRef), 248 Data == ReadData. 249 250%%%---------------------------------------------------------------- 251std_simple_exec(Host, Port, Config) -> 252 std_simple_exec(Host, Port, Config, []). 253 254std_simple_exec(Host, Port, Config, Opts) -> 255 ct:log("~p:~p std_simple_exec",[?MODULE,?LINE]), 256 ConnectionRef = ssh_test_lib:std_connect(Config, Host, Port, Opts), 257 ct:log("~p:~p connected! ~p",[?MODULE,?LINE,ConnectionRef]), 258 {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity), 259 ct:log("~p:~p session_channel ok ~p",[?MODULE,?LINE,ChannelId]), 260 ExecResult = ssh_connection:exec(ConnectionRef, ChannelId, "23+21-2.", infinity), 261 ct:log("~p:~p exec ~p",[?MODULE,?LINE,ExecResult]), 262 case ExecResult of 263 success -> 264 Expected = {ssh_cm, ConnectionRef, {data,ChannelId,0,<<"42">>}}, 265 case receive_exec_result(Expected) of 266 expected -> 267 ok; 268 Other -> 269 ct:fail(Other) 270 end, 271 receive_exec_end(ConnectionRef, ChannelId), 272 ssh:close(ConnectionRef); 273 _ -> 274 ct:fail(ExecResult) 275 end. 276 277%%%---------------------------------------------------------------- 278start_shell(Port, IOServer) -> 279 start_shell(Port, IOServer, []). 280 281start_shell(Port, IOServer, ExtraOptions) -> 282 spawn_link( 283 fun() -> 284 ct:log("~p:~p:~p ssh_test_lib:start_shell(~p, ~p, ~p)", 285 [?MODULE,?LINE,self(), Port, IOServer, ExtraOptions]), 286 Options = [{user_interaction, false}, 287 {silently_accept_hosts,true} | ExtraOptions], 288 try 289 group_leader(IOServer, self()), 290 case Port of 291 22 -> 292 Host = hostname(), 293 ct:log("Port==22 Call ssh:shell(~p, ~p)", 294 [Host, Options]), 295 ssh:shell(Host, Options); 296 _ when is_integer(Port) -> 297 Host = hostname(), 298 ct:log("is_integer(Port) Call ssh:shell(~p, ~p, ~p)", 299 [Host, Port, Options]), 300 ssh:shell(Host, Port, Options); 301 Socket when is_port(Socket) -> 302 receive 303 start -> ok 304 end, 305 ct:log("is_port(Socket) Call ssh:shell(~p, ~p)", 306 [Socket, Options]), 307 ssh:shell(Socket, Options); 308 ConnRef when is_pid(ConnRef) -> 309 ct:log("is_pid(ConnRef) Call ssh:shell(~p)", 310 [ConnRef]), 311 ssh:shell(ConnRef) % Options were given in ssh:connect 312 end 313 of 314 R -> 315 ct:log("~p:~p ssh_test_lib:start_shell(~p, ~p, ~p) -> ~p", 316 [?MODULE,?LINE,Port, IOServer, ExtraOptions, R]) 317 catch 318 C:E:S -> 319 ct:log("Exception ~p:~p~n~p", [C,E,S]), 320 ct:fail("Exception",[]) 321 end 322 end). 323 324 325%%%---------------------------------------------------------------- 326start_io_server() -> 327 spawn_link(?MODULE, init_io_server, [self()]). 328 329init_io_server(TestCase) -> 330 process_flag(trap_exit, true), 331 loop_io_server(TestCase, []). 332 333loop_io_server(TestCase, Buff0) -> 334 receive 335 {input, TestCase, Line} = _INP -> 336 %%ct:log("io_server ~p:~p ~p got ~p",[?MODULE,?LINE,self(),_INP]), 337 loop_io_server(TestCase, Buff0 ++ [Line]); 338 {io_request, From, ReplyAs, Request} = _REQ-> 339 %%ct:log("io_server ~p:~p ~p got ~p",[?MODULE,?LINE,self(),_REQ]), 340 {ok, Reply, Buff} = io_request(Request, TestCase, From, 341 ReplyAs, Buff0), 342 %%ct:log("io_server ~p:~p ~p going to reply ~p",[?MODULE,?LINE,self(),Reply]), 343 io_reply(From, ReplyAs, Reply), 344 loop_io_server(TestCase, Buff); 345 {'EXIT',_, _} = _Exit -> 346 ct:log("ssh_test_lib:loop_io_server/2 got ~p",[_Exit]), 347 ok 348 after 349 30000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) 350 end. 351 352io_request(getopts,_TestCase, _, _, Buff) -> 353 {ok, [], Buff}; 354io_request({get_geometry,columns},_TestCase, _, _, Buff) -> 355 {ok, 80, Buff}; 356io_request({get_geometry,rows},_TestCase, _, _, Buff) -> 357 {ok, 24, Buff}; 358io_request({put_chars, Chars}, TestCase, _, _, Buff) -> 359 reply(TestCase, Chars), 360 {ok, ok, Buff}; 361io_request({put_chars, unicode, Chars}, TestCase, _, _, Buff) when is_binary(Chars) -> 362 reply(TestCase, Chars), 363 {ok, ok, Buff}; 364io_request({put_chars, unicode, io_lib, format, [Fmt,Args]}, TestCase, _, _, Buff) -> 365 reply(TestCase, unicode:characters_to_binary(io_lib:format(Fmt,Args))), 366 {ok, ok, Buff}; 367io_request({put_chars, Enc, Chars}, TestCase, _, _, Buff) -> 368 reply(TestCase, unicode:characters_to_binary(Chars,Enc,latin1)), 369 {ok, ok, Buff}; 370 371io_request({get_line, _} = Request, _, From, ReplyAs, [] = Buff) -> 372 erlang:send_after(1000, self(), {io_request, From, ReplyAs, Request}), 373 {ok, [], Buff}; 374io_request({get_line, _Enc, _Prompt} = Request, _, From, ReplyAs, [] = Buff) -> 375 erlang:send_after(1000, self(), {io_request, From, ReplyAs, Request}), 376 {ok, [], Buff}; 377 378io_request({get_line, _Enc,_}, _, _, _, [Line | Buff]) -> 379 {ok, Line, Buff}. 380 381 382io_reply(_, _, []) -> 383 ok; 384io_reply(From, ReplyAs, Reply) -> 385%%ct:log("io_reply ~p sending ~p ! ~p",[self(),From, {io_reply, ReplyAs, Reply}]), 386 From ! {io_reply, ReplyAs, Reply}. 387 388reply(_, []) -> 389 ok; 390reply(TestCase, Result) -> 391%%ct:log("reply ~p sending ~p ! ~p",[self(), TestCase, Result]), 392 TestCase ! Result. 393 394%%%---------------------------------------------------------------- 395rcv_expected(Expect, SshPort, Timeout) -> 396 receive 397 {SshPort, Recvd} when is_function(Expect) -> 398 case Expect(Recvd) of 399 true -> 400 ct:log("Got expected ~p from ~p",[Recvd,SshPort]), 401 catch port_close(SshPort), 402 rcv_lingering(50); 403 false -> 404 ct:log("Got UNEXPECTED ~p~n",[Recvd]), 405 rcv_expected(Expect, SshPort, Timeout) 406 end; 407 {SshPort, Expect} -> 408 ct:log("Got expected ~p from ~p",[Expect,SshPort]), 409 catch port_close(SshPort), 410 rcv_lingering(50); 411 Other -> 412 ct:log("Got UNEXPECTED ~p~nExpect ~p",[Other, {SshPort,Expect}]), 413 rcv_expected(Expect, SshPort, Timeout) 414 415 after Timeout -> 416 catch port_close(SshPort), 417 ct:fail("Did not receive answer") 418 end. 419 420rcv_lingering(Timeout) -> 421 receive 422 Msg -> 423 ct:log("Got LINGERING ~p",[Msg]), 424 rcv_lingering(Timeout) 425 426 after Timeout -> 427 ct:log("No more lingering messages",[]), 428 ok 429 end. 430 431 432receive_exec_result([]) -> 433 expected; 434receive_exec_result(Msgs) when is_list(Msgs) -> 435 ct:log("~p:~p Expect data! ~p", [?MODULE,?FUNCTION_NAME,Msgs]), 436 receive 437 Msg -> 438 case lists:member(Msg, Msgs) 439 orelse lists:member({optional,Msg}, Msgs) 440 of 441 true -> 442 ct:log("~p:~p Collected data ~p", [?MODULE,?FUNCTION_NAME,Msg]), 443 receive_exec_result(Msgs--[Msg,{optional,Msg}]); 444 false -> 445 case Msg of 446 {ssh_cm,_,{data,_,1, Data}} -> 447 ct:log("~p:~p unexpected StdErr: ~p~n~p~n", [?MODULE,?FUNCTION_NAME,Data,Msg]), 448 receive_exec_result(Msgs); 449 Other -> 450 ct:log("~p:~p unexpected Other ~p", [?MODULE,?FUNCTION_NAME,Other]), 451 {unexpected_msg, Other} 452 end 453 end 454 after 455 30000 -> 456 case lists:all(fun(M) -> 457 is_tuple(M) andalso (element(1,M) == optional) 458 end, Msgs) 459 of 460 false -> 461 ct:fail("timeout ~p:~p",[?MODULE,?FUNCTION_NAME]); 462 true -> 463 ct:log("~p:~p Only optional messages expected!~n ~p", [?MODULE,?FUNCTION_NAME,Msgs]), 464 expected 465 end 466 end; 467receive_exec_result(Msg) -> 468 receive_exec_result([Msg]). 469 470 471receive_exec_result_or_fail(Msg) -> 472 case receive_exec_result(Msg) of 473 expected -> expected; 474 Other -> ct:fail(Other) 475 end. 476 477receive_exec_end(ConnectionRef, ChannelId) -> 478 receive_exec_end(ConnectionRef, ChannelId, 0). 479 480receive_exec_end(ConnectionRef, ChannelId, ExitStatus) -> 481 receive_exec_result( 482 [{ssh_cm, ConnectionRef, {eof, ChannelId}}, 483 {optional, {ssh_cm, ConnectionRef, {exit_status, ChannelId, ExitStatus}}}, 484 {ssh_cm, ConnectionRef, {closed, ChannelId}} 485 ]). 486 487receive_exec_result(Data, ConnectionRef, ChannelId) -> 488 Eof = {ssh_cm, ConnectionRef, {eof, ChannelId}}, 489 Closed = {ssh_cm, ConnectionRef,{closed, ChannelId}}, 490 expected = receive_exec_result(Data), 491 expected = receive_exec_result(Eof), 492 expected = receive_exec_result(Closed). 493 494 495failfun(_User, {authmethod,none}) -> 496 ok; 497failfun(User, Reason) -> 498 error_logger:format("~p failed XXX to login: ~p~n", [User, Reason]). 499 500hostname() -> 501 {ok,Host} = inet:gethostname(), 502 Host. 503 504del_dirs(Dir) -> 505 del_dir_contents(Dir), 506 file:del_dir(Dir), 507 ok. 508 509 510del_dir_contents(Dir) -> 511 case file:list_dir(Dir) of 512 {ok, Files} -> 513 do_del_files(Dir, Files); 514 _ -> 515 ok 516 end. 517 518do_del_files(Dir, Files) -> 519 lists:foreach(fun(File) -> 520 FullPath = filename:join(Dir,File), 521 case filelib:is_dir(FullPath) of 522 true -> 523 del_dirs(FullPath); 524 false -> 525 file:delete(FullPath) 526 end 527 end, Files). 528 529 530openssh_sanity_check(Config) -> 531 ssh:start(), 532 case ssh:connect("localhost", 22, [{password,""}, 533 {save_accepted_host, false}, 534 {silently_accept_hosts, true} ]) of 535 {ok, Pid} -> 536 ssh:close(Pid), 537 ssh:stop(), 538 Config; 539 Err -> 540 Str = lists:append(io_lib:format("~p", [Err])), 541 ssh:stop(), 542 {skip, Str} 543 end. 544 545%%%-------------------------------------------------------------------- 546%%% Probe a server or a client about algorithm support 547 548default_algorithms(sshd) -> 549 default_algorithms(sshd, "localhost", 22); 550 551default_algorithms(sshc) -> 552 default_algorithms(sshc, []). 553 554default_algorithms(sshd, Host, Port) -> 555 try run_fake_ssh( 556 ssh_trpt_test_lib:exec( 557 [{connect,Host,Port, [{silently_accept_hosts, true}, 558 {user_interaction, false}]}])) 559 catch 560 _C:_E -> 561 ct:log("***~p:~p: ~p:~p",[?MODULE,?LINE,_C,_E]), 562 [] 563 end. 564 565default_algorithms(sshc, DaemonOptions) -> 566 Parent = self(), 567 %% Start a process handling one connection on the server side: 568 Srvr = 569 spawn_link( 570 fun() -> 571 Parent ! 572 {result, self(), 573 try 574 {ok,InitialState} = ssh_trpt_test_lib:exec(listen), 575 Parent ! {hostport,self(),ssh_trpt_test_lib:server_host_port(InitialState)}, 576 run_fake_ssh( 577 ssh_trpt_test_lib:exec([{accept, DaemonOptions}], 578 InitialState)) 579 catch 580 _C:_E -> 581 ct:log("***~p:~p: ~p:~p",[?MODULE,?LINE,_C,_E]), 582 [] 583 end} 584 end), 585 586 receive 587 {hostport,Srvr,{_Host,Port}} -> 588 spawn(fun()-> os:cmd(lists:concat(["ssh -o \"StrictHostKeyChecking no\" -p ",Port," localhost"])) end) 589 after ?TIMEOUT -> 590 ct:fail("No server respons (timeout) 1") 591 end, 592 593 receive 594 {result,Srvr,L} -> 595 L 596 after ?TIMEOUT -> 597 ct:fail("No server respons (timeout) 2") 598 end. 599 600run_fake_ssh({ok,InitialState}) -> 601 KexInitPattern = 602 #ssh_msg_kexinit{ 603 kex_algorithms = '$kex_algorithms', 604 server_host_key_algorithms = '$server_host_key_algorithms', 605 encryption_algorithms_client_to_server = '$encryption_algorithms_client_to_server', 606 encryption_algorithms_server_to_client = '$encryption_algorithms_server_to_client', 607 mac_algorithms_client_to_server = '$mac_algorithms_client_to_server', 608 mac_algorithms_server_to_client = '$mac_algorithms_server_to_client', 609 compression_algorithms_client_to_server = '$compression_algorithms_client_to_server', 610 compression_algorithms_server_to_client = '$compression_algorithms_server_to_client', 611 _ = '_' 612 }, 613 {ok,E} = ssh_trpt_test_lib:exec([{set_options,[silent]}, 614 {send, hello}, 615 receive_hello, 616 {send, ssh_msg_kexinit}, 617 {match, KexInitPattern, receive_msg}, 618 close_socket 619 ], 620 InitialState), 621 [Kex, PubKey, EncC2S, EncS2C, MacC2S, MacS2C, CompC2S, CompS2C] = 622 ssh_trpt_test_lib:instantiate(['$kex_algorithms', 623 '$server_host_key_algorithms', 624 '$encryption_algorithms_client_to_server', 625 '$encryption_algorithms_server_to_client', 626 '$mac_algorithms_client_to_server', 627 '$mac_algorithms_server_to_client', 628 '$compression_algorithms_client_to_server', 629 '$compression_algorithms_server_to_client' 630 ], E), 631 [{kex, to_atoms(Kex)}, 632 {public_key, to_atoms(PubKey)}, 633 {cipher, [{client2server, to_atoms(EncC2S)}, 634 {server2client, to_atoms(EncS2C)}]}, 635 {mac, [{client2server, to_atoms(MacC2S)}, 636 {server2client, to_atoms(MacS2C)}]}, 637 {compression, [{client2server, to_atoms(CompC2S)}, 638 {server2client, to_atoms(CompS2C)}]}]. 639 640 641%%%---------------------------------------------------------------- 642extract_algos(Spec) -> 643 [{Tag,get_atoms(List)} || {Tag,List} <- Spec]. 644 645get_atoms(L) -> 646 lists:usort( 647 [ A || X <- L, 648 A <- case X of 649 {_,L1} when is_list(L1) -> L1; 650 Y when is_atom(Y) -> [Y] 651 end]). 652 653 654intersection(AlgoSpec1, AlgoSpec2) -> intersect(sort_spec(AlgoSpec1), sort_spec(AlgoSpec2)). 655 656intersect([{Tag,S1}|Ss1], [{Tag,S2}|Ss2]) -> 657 [{Tag,intersect(S1,S2)} | intersect(Ss1,Ss2)]; 658intersect(L1=[A1|_], L2=[A2|_]) when is_atom(A1),is_atom(A2) -> 659 Diff = L1 -- L2, 660 L1 -- Diff; 661intersect(_, _) -> 662 []. 663 664intersect_bi_dir([{Tag,[{client2server,L1},{server2client,L2}]}|T]) -> 665 [{Tag,intersect(L1,L2)} | intersect_bi_dir(T)]; 666intersect_bi_dir([H={_,[A|_]}|T]) when is_atom(A) -> 667 [H | intersect_bi_dir(T)]; 668intersect_bi_dir([]) -> 669 []. 670 671some_empty([]) -> 672 false; 673some_empty([{_,[]}|_]) -> 674 true; 675some_empty([{_,L}|T]) when is_atom(hd(L)) -> 676 some_empty(T); 677some_empty([{_,L}|T]) when is_tuple(hd(L)) -> 678 some_empty(L) orelse some_empty(T). 679 680 681sort_spec(L = [{_,_}|_] ) -> [{Tag,sort_spec(Es)} || {Tag,Es} <- L]; 682sort_spec(L) -> lists:usort(L). 683 684%%-------------------------------------------------------------------- 685sshc(Tag) -> 686 to_atoms( 687 string:tokens(os:cmd(lists:concat(["ssh -Q ",Tag])), "\n") 688 ). 689 690ssh_type() -> 691 Parent = self(), 692 Pid = spawn(fun() -> 693 Parent ! {ssh_type,self(),ssh_type1()} 694 end), 695 MonitorRef = monitor(process, Pid), 696 receive 697 {ssh_type, Pid, Result} -> 698 demonitor(MonitorRef), 699 Result; 700 {'DOWN', MonitorRef, process, Pid, _Info} -> 701 ct:log("~p:~p Process DOWN",[?MODULE,?LINE]), 702 not_found 703 after 704 10000 -> 705 ct:log("~p:~p Timeout",[?MODULE,?LINE]), 706 demonitor(MonitorRef), 707 not_found 708 end. 709 710 711ssh_type1() -> 712 try 713 ct:log("~p:~p os:find_executable(\"ssh\")",[?MODULE,?LINE]), 714 case os:find_executable("ssh") of 715 false -> 716 ct:log("~p:~p Executable \"ssh\" not found",[?MODULE,?LINE]), 717 not_found; 718 Path -> 719 ct:log("~p:~p Found \"ssh\" at ~p",[?MODULE,?LINE,Path]), 720 case installed_ssh_version(timeout) of 721 Version = "OpenSSH" ++ _ -> 722 ct:log("~p:~p Found OpenSSH ~p",[?MODULE,?LINE,Version]), 723 openSSH; 724 Other -> 725 ct:log("ssh client ~p is unknown",[Other]), 726 unknown 727 end 728 end 729 catch 730 Class:Exception -> 731 ct:log("~p:~p Exception ~p:~p",[?MODULE,?LINE,Class,Exception]), 732 not_found 733 end. 734 735installed_ssh_version(TimeoutReturn) -> 736 Parent = self(), 737 Pid = spawn(fun() -> 738 Parent ! {open_ssh_version, os:cmd("ssh -V")} 739 end), 740 receive 741 {open_ssh_version, V} -> 742 V 743 after ?TIMEOUT -> 744 exit(Pid, kill), 745 TimeoutReturn 746 end. 747 748 749 750 751algo_intersection([], _) -> []; 752algo_intersection(_, []) -> []; 753algo_intersection(L1=[A1|_], L2=[A2|_]) when is_atom(A1), is_atom(A2) -> 754 true = lists:all(fun erlang:is_atom/1, L1++L2), 755 lists:foldr(fun(A,Acc) -> 756 case lists:member(A,L2) of 757 true -> [A|Acc]; 758 false -> Acc 759 end 760 end, [], L1); 761algo_intersection([{K,V1}|T1], L2) -> 762 case lists:keysearch(K,1,L2) of 763 {value, {K,V2}} -> 764 [{K,algo_intersection(V1,V2)} | algo_intersection(T1,L2)]; 765 false -> 766 algo_intersection(T1,L2) 767 end; 768algo_intersection(_, _) -> 769 []. 770 771 772to_atoms(L) -> lists:map(fun erlang:list_to_atom/1, L). 773 774%%%---------------------------------------------------------------- 775ssh_supports(Alg, SshDefaultAlg_tag) -> 776 SupAlgs = 777 case proplists:get_value(SshDefaultAlg_tag, 778 ssh:default_algorithms()) of 779 [{_K1,L1}, {_K2,L2}] -> 780 lists:usort(L1++L2); 781 L -> 782 L 783 end, 784 if 785 is_atom(Alg) -> 786 lists:member(Alg, SupAlgs); 787 is_list(Alg) -> 788 case Alg--SupAlgs of 789 [] -> 790 true; 791 UnSup -> 792 {false,UnSup} 793 end 794 end. 795 796%%%---------------------------------------------------------------- 797has_inet6_address() -> 798 try 799 [throw(6) || {ok,L} <- [inet:getifaddrs()], 800 {_,L1} <- L, 801 {addr,{_,_,_,_,_,_,_,_}} <- L1] 802 of 803 [] -> false 804 catch 805 throw:6 -> true 806 end. 807 808%%%---------------------------------------------------------------- 809open_port(Arg1) -> 810 ?MODULE:open_port(Arg1, []). 811 812open_port(Arg1, ExtraOpts) -> 813 erlang:open_port(Arg1, 814 [binary, 815 stderr_to_stdout, 816 exit_status, 817 use_stdio, 818 overlapped_io, hide %only affects windows 819 | ExtraOpts]). 820 821%%%---------------------------------------------------------------- 822%%% Sleeping 823 824%%% Milli sec 825sleep_millisec(Nms) -> receive after Nms -> ok end. 826 827%%% Micro sec 828sleep_microsec(Nus) -> 829 busy_wait(Nus, erlang:system_time(microsecond)). 830 831busy_wait(Nus, T0) -> 832 T = erlang:system_time(microsecond) - T0, 833 Tleft = Nus - T, 834 if 835 Tleft > 2000 -> 836 sleep_millisec((Tleft-1500) div 1000), % μs -> ms 837 busy_wait(Nus,T0); 838 Tleft > 1 -> 839 busy_wait(Nus, T0); 840 true -> 841 T 842 end. 843 844%%%---------------------------------------------------------------- 845%% get_kex_init - helper function to get key_exchange_init_msg 846 847get_kex_init(Conn) -> 848 Ref = make_ref(), 849 {ok,TRef} = timer:send_after(15000, {reneg_timeout,Ref}), 850 get_kex_init(Conn, Ref, TRef). 851 852get_kex_init(Conn, Ref, TRef) -> 853 %% First, validate the key exchange is complete (StateName == connected) 854 {State, S} = sys:get_state(Conn), 855 case expected_state(State) of 856 true -> 857 timer:cancel(TRef), 858 %% Next, walk through the elements of the #state record looking 859 %% for the #ssh_msg_kexinit record. This method is robust against 860 %% changes to either record. The KEXINIT message contains a cookie 861 %% unique to each invocation of the key exchange procedure (RFC4253) 862 SL = tuple_to_list(S), 863 case lists:keyfind(ssh_msg_kexinit, 1, SL) of 864 false -> 865 throw(not_found); 866 KexInit -> 867 KexInit 868 end; 869 870 false -> 871 receive 872 {reneg_timeout,Ref} -> 873 ct:log("~p:~p Not in 'connected' state: ~p but reneg_timeout received. Fail.", 874 [?MODULE,?LINE,State]), 875 ct:log("S = ~p", [S]), 876 ct:fail(reneg_timeout) 877 after 0 -> 878 ct:log("~p:~p Not in 'connected' state: ~p, Will try again after 100ms",[?MODULE,?LINE,State]), 879 timer:sleep(100), % If renegotiation is complete we do not 880 % want to exit on the reneg_timeout 881 get_kex_init(Conn, Ref, TRef) 882 end 883 end. 884 885expected_state({ext_info,_,_}) -> true; 886expected_state({connected,_}) -> true; 887expected_state(_) -> false. 888 889%%%---------------------------------------------------------------- 890%%% Return a string with N random characters 891%%% 892random_chars(N) -> [($a-1)+rand:uniform($z-$a) || _<-lists:duplicate(N,x)]. 893 894 895create_random_dir(Config) -> 896 PrivDir = proplists:get_value(priv_dir, Config), 897 Name = filename:join(PrivDir, random_chars(15)), 898 case file:make_dir(Name) of 899 ok -> 900 Name; 901 {error,eexist} -> 902 %% The Name already denotes an existing file system object, try again. 903 %% The likelyhood of always generating an existing file name is low 904 create_random_dir(Config) 905 end. 906 907%%%---------------------------------------------------------------- 908match_ip(A, B) -> 909 R = match_ip0(A,B) orelse match_ip0(B,A), 910 ct:log("match_ip(~p, ~p) -> ~p",[A, B, R]), 911 R. 912 913match_ip0(A, A) -> 914 true; 915match_ip0(any, _) -> 916 true; 917match_ip0(A, B) -> 918 case match_ip1(A, B) of 919 true -> 920 true; 921 false when is_list(A) -> 922 case inet:parse_address(A) of 923 {ok,IPa} -> match_ip0(IPa, B); 924 _ -> false 925 end; 926 false when is_list(B) -> 927 case inet:parse_address(B) of 928 {ok,IPb} -> match_ip0(A, IPb); 929 _ -> false 930 end; 931 false -> 932 false 933 end. 934 935match_ip1(any, _) -> true; 936match_ip1(loopback, {127,_,_,_}) -> true; 937match_ip1({0,0,0,0}, {127,_,_,_}) -> true; 938match_ip1(loopback, {0,0,0,0,0,0,0,1}) -> true; 939match_ip1({0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,1}) -> true; 940match_ip1(_, _) -> false. 941 942%%%---------------------------------------------------------------- 943mangle_connect_address(A) -> 944 mangle_connect_address(A, []). 945 946mangle_connect_address(A, SockOpts) -> 947 mangle_connect_address1(A, proplists:get_value(inet6,SockOpts,false)). 948 949loopback(true) -> {0,0,0,0,0,0,0,1}; 950loopback(false) -> {127,0,0,1}. 951 952mangle_connect_address1( loopback, V6flg) -> loopback(V6flg); 953mangle_connect_address1( any, V6flg) -> loopback(V6flg); 954mangle_connect_address1({0,0,0,0}, _) -> loopback(false); 955mangle_connect_address1({0,0,0,0,0,0,0,0}, _) -> loopback(true); 956mangle_connect_address1( IP, _) when is_tuple(IP) -> IP; 957mangle_connect_address1(A, _) -> 958 case catch inet:parse_address(A) of 959 {ok, {0,0,0,0}} -> loopback(false); 960 {ok, {0,0,0,0,0,0,0,0}} -> loopback(true); 961 _ -> A 962 end. 963 964%%%---------------------------------------------------------------- 965ntoa(A) -> 966 try inet:ntoa(A) 967 of 968 {error,_} when is_atom(A) -> atom_to_list(A); 969 {error,_} when is_list(A) -> A; 970 S when is_list(S) -> S 971 catch 972 _:_ when is_atom(A) -> atom_to_list(A); 973 _:_ when is_list(A) -> A 974 end. 975 976%%%---------------------------------------------------------------- 977try_enable_fips_mode() -> 978 case crypto:info_fips() of 979 enabled -> 980 report("FIPS mode already enabled", ?LINE), 981 ok; 982 not_enabled -> 983 %% Erlang/crypto configured with --enable-fips 984 case crypto:enable_fips_mode(true) of 985 true -> 986 %% and also the cryptolib is fips enabled 987 report("FIPS mode enabled", ?LINE), 988 enabled = crypto:info_fips(), 989 ok; 990 false -> 991 case is_cryptolib_fips_capable() of 992 false -> 993 report("No FIPS mode in cryptolib", ?LINE), 994 {skip, "FIPS mode not supported in cryptolib"}; 995 true -> 996 ct:fail("Failed to enable FIPS mode", []) 997 end 998 end; 999 not_supported -> 1000 report("FIPS mode not supported by Erlang/OTP", ?LINE), 1001 {skip, "FIPS mode not supported"} 1002 end. 1003 1004is_cryptolib_fips_capable() -> 1005 [{_,_,Inf}] = crypto:info_lib(), 1006 nomatch =/= re:run(Inf, "(F|f)(I|i)(P|p)(S|s)"). 1007 1008report(Comment, Line) -> 1009 ct:comment(Comment), 1010 ct:log("~p:~p try_enable_fips_mode~n" 1011 "crypto:info_lib() = ~p~n" 1012 "crypto:info_fips() = ~p~n" 1013 "crypto:supports() =~n~p~n", 1014 [?MODULE, Line, 1015 crypto:info_lib(), 1016 crypto:info_fips(), 1017 crypto:supports()]). 1018 1019%%%---------------------------------------------------------------- 1020lc_name_in(Names) -> 1021 case inet:gethostname() of 1022 {ok,Name} -> 1023 lists:member(string:to_lower(Name), Names); 1024 Other -> 1025 ct:log("~p:~p inet:gethostname() returned ~p", [?MODULE,?LINE,Other]), 1026 false 1027 end. 1028 1029ptty_supported() -> not lc_name_in([]). %%["fobi"]). 1030 1031%%%---------------------------------------------------------------- 1032has_WSL() -> 1033 os:getenv("WSLENV") =/= false. % " =/= false" =/= "== true" :) 1034 1035winpath_to_linuxpath(Path) -> 1036 case {has_WSL(), Path} of 1037 {true, [_,$:|WithoutWinInit]} -> 1038 "/mnt/c" ++ WithoutWinInit; 1039 _ -> 1040 Path 1041 end. 1042 1043%%%---------------------------------------------------------------- 1044copy_recursive(Src, Dst) -> 1045 {ok,S} = file:read_file_info(Src), 1046 case S#file_info.type of 1047 directory -> 1048 %%ct:log("~p:~p copy dir ~ts -> ~ts", [?MODULE,?LINE,Src,Dst]), 1049 {ok,Names} = file:list_dir(Src), 1050 mk_dir_path(Dst), 1051 %%ct:log("~p:~p Names = ~p", [?MODULE,?LINE,Names]), 1052 lists:foreach(fun(Name) -> 1053 copy_recursive(filename:join(Src, Name), 1054 filename:join(Dst, Name)) 1055 end, Names); 1056 _ -> 1057 %%ct:log("~p:~p copy file ~ts -> ~ts", [?MODULE,?LINE,Src,Dst]), 1058 {ok,_NumBytesCopied} = file:copy(Src, Dst) 1059 end. 1060 1061%%%---------------------------------------------------------------- 1062%% Make a directory even if parts of the path does not exist 1063 1064mk_dir_path(DirPath) -> 1065 case file:make_dir(DirPath) of 1066 {error,eexist} -> 1067 %%ct:log("~p:~p dir exists ~ts", [?MODULE,?LINE,DirPath]), 1068 ok; 1069 {error,enoent} -> 1070 %%ct:log("~p:~p try make dirname of ~ts", [?MODULE,?LINE,DirPath]), 1071 case mk_dir_path( filename:dirname(DirPath) ) of 1072 ok -> 1073 %%ct:log("~p:~p redo ~ts", [?MODULE,?LINE,DirPath]), 1074 file:make_dir(DirPath); 1075 Error -> 1076 %%ct:log("~p:~p return Error ~p ~ts", [?MODULE,?LINE,Error,DirPath]), 1077 Error 1078 end; 1079 Other -> 1080 %%ct:log("~p:~p return Other ~p ~ts", [?MODULE,?LINE,Other,DirPath]), 1081 Other 1082 end. 1083 1084%%%---------------------------------------------------------------- 1085%%% New 1086 1087setup_all_user_host_keys(Config) -> 1088 DataDir = proplists:get_value(data_dir, Config), 1089 PrivDir = proplists:get_value(priv_dir, Config), 1090 setup_all_user_host_keys(DataDir, PrivDir). 1091 1092setup_all_user_host_keys(DataDir, PrivDir) -> 1093 setup_all_user_host_keys(DataDir, PrivDir, filename:join(PrivDir,"system")). 1094 1095setup_all_user_host_keys(DataDir, UserDir, SysDir) -> 1096 lists:foldl(fun(Alg, OkAlgs) -> 1097 try 1098 ok = ssh_test_lib:setup_user_key(Alg, DataDir, UserDir), 1099 ok = ssh_test_lib:setup_host_key(Alg, DataDir, SysDir) 1100 of 1101 ok -> [Alg|OkAlgs] 1102 catch 1103 error:{badmatch, {error,enoent}} -> 1104 OkAlgs; 1105 C:E:S -> 1106 ct:log("Exception in ~p:~p for alg ~p: ~p:~p~n~p", 1107 [?MODULE,?FUNCTION_NAME,Alg,C,E,S]), 1108 OkAlgs 1109 end 1110 end, [], ssh_transport:supported_algorithms(public_key)). 1111 1112 1113setup_all_host_keys(Config) -> 1114 DataDir = proplists:get_value(data_dir, Config), 1115 PrivDir = proplists:get_value(priv_dir, Config), 1116 setup_all_host_keys(DataDir, filename:join(PrivDir,"system")). 1117 1118setup_all_host_keys(DataDir, SysDir) -> 1119 lists:foldl(fun(Alg, OkAlgs) -> 1120 try 1121 ok = ssh_test_lib:setup_host_key(Alg, DataDir, SysDir) 1122 of 1123 ok -> [Alg|OkAlgs] 1124 catch 1125 error:{badmatch, {error,enoent}} -> 1126 OkAlgs; 1127 C:E:S -> 1128 ct:log("Exception in ~p:~p for alg ~p: ~p:~p~n~p", 1129 [?MODULE,?FUNCTION_NAME,Alg,C,E,S]), 1130 OkAlgs 1131 end 1132 end, [], ssh_transport:supported_algorithms(public_key)). 1133 1134 1135setup_all_user_keys(DataDir, UserDir) -> 1136 lists:foldl(fun(Alg, OkAlgs) -> 1137 try 1138 ok = ssh_test_lib:setup_user_key(Alg, DataDir, UserDir) 1139 of 1140 ok -> [Alg|OkAlgs] 1141 catch 1142 error:{badmatch, {error,enoent}} -> 1143 OkAlgs; 1144 C:E:S -> 1145 ct:log("Exception in ~p:~p for alg ~p: ~p:~p~n~p", 1146 [?MODULE,?FUNCTION_NAME,Alg,C,E,S]), 1147 OkAlgs 1148 end 1149 end, [], ssh_transport:supported_algorithms(public_key)). 1150 1151 1152setup_user_key(SshAlg, DataDir, UserDir) -> 1153 file:make_dir(UserDir), 1154 %% Copy private user key to user's dir 1155 {ok,_} = file:copy(filename:join(DataDir, file_base_name(user_src,SshAlg)), 1156 filename:join(UserDir, file_base_name(user,SshAlg))), 1157 %% Setup authorized_keys in user's dir 1158 {ok,Pub} = file:read_file(filename:join(DataDir, file_base_name(user_src,SshAlg)++".pub")), 1159 ok = file:write_file(filename:join(UserDir, "authorized_keys"), 1160 io_lib:format("~n~s~n",[Pub]), 1161 [append]), 1162 ?ct_log_show_file( filename:join(DataDir, file_base_name(user_src,SshAlg)++".pub") ), 1163 ?ct_log_show_file( filename:join(UserDir, "authorized_keys") ), 1164 ok. 1165 1166setup_host_key_create_dir(SshAlg, DataDir, BaseDir) -> 1167 SysDir = filename:join(BaseDir,"system"), 1168 ct:log("~p:~p SshAlg=~p~nDataDir = ~p~nBaseDir = ~p~nSysDir = ~p",[?MODULE,?LINE,SshAlg, DataDir, BaseDir,SysDir]), 1169 file:make_dir(SysDir), 1170 setup_host_key(SshAlg, DataDir, SysDir), 1171 SysDir. 1172 1173setup_host_key(SshAlg, DataDir, SysDir) -> 1174 mk_dir_path(SysDir), 1175 %% Copy private host key to system's dir 1176 {ok,_} = file:copy(filename:join(DataDir, file_base_name(system_src,SshAlg)), 1177 filename:join(SysDir, file_base_name(system,SshAlg))), 1178 ?ct_log_show_file( filename:join(SysDir, file_base_name(system,SshAlg)) ), 1179 ok. 1180 1181setup_known_host(SshAlg, DataDir, UserDir) -> 1182 {ok,Pub} = file:read_file(filename:join(DataDir, file_base_name(system_src,SshAlg)++".pub")), 1183 S = lists:join(" ", lists:reverse(tl(lists:reverse(string:tokens(binary_to_list(Pub), " "))))), 1184 ok = file:write_file(filename:join(UserDir, "known_hosts"), 1185 io_lib:format("~p~n",[S])), 1186 ?ct_log_show_file( filename:join(UserDir, "known_hosts") ), 1187 ok. 1188 1189 1190get_addr_str() -> 1191 {ok, Hostname} = inet:gethostname(), 1192 {ok, {A, B, C, D}} = inet:getaddr(Hostname, inet), 1193 IP = lists:concat([A, ".", B, ".", C, ".", D]), 1194 lists:concat([Hostname,",",IP]). 1195 1196 1197file_base_name(user, 'ecdsa-sha2-nistp256') -> "id_ecdsa"; 1198file_base_name(user, 'ecdsa-sha2-nistp384') -> "id_ecdsa"; 1199file_base_name(user, 'ecdsa-sha2-nistp521') -> "id_ecdsa"; 1200file_base_name(user, 'rsa-sha2-256' ) -> "id_rsa"; 1201file_base_name(user, 'rsa-sha2-384' ) -> "id_rsa"; 1202file_base_name(user, 'rsa-sha2-512' ) -> "id_rsa"; 1203file_base_name(user, 'ssh-dss' ) -> "id_dsa"; 1204file_base_name(user, 'ssh-ed25519' ) -> "id_ed25519"; 1205file_base_name(user, 'ssh-ed448' ) -> "id_ed448"; 1206file_base_name(user, 'ssh-rsa' ) -> "id_rsa"; 1207 1208file_base_name(user_src, 'ecdsa-sha2-nistp256') -> "id_ecdsa256"; 1209file_base_name(user_src, 'ecdsa-sha2-nistp384') -> "id_ecdsa384"; 1210file_base_name(user_src, 'ecdsa-sha2-nistp521') -> "id_ecdsa521"; 1211file_base_name(user_src, Alg) -> file_base_name(user, Alg); 1212 1213file_base_name(system, 'ecdsa-sha2-nistp256') -> "ssh_host_ecdsa_key"; 1214file_base_name(system, 'ecdsa-sha2-nistp384') -> "ssh_host_ecdsa_key"; 1215file_base_name(system, 'ecdsa-sha2-nistp521') -> "ssh_host_ecdsa_key"; 1216file_base_name(system, 'rsa-sha2-256' ) -> "ssh_host_rsa_key"; 1217file_base_name(system, 'rsa-sha2-384' ) -> "ssh_host_rsa_key"; 1218file_base_name(system, 'rsa-sha2-512' ) -> "ssh_host_rsa_key"; 1219file_base_name(system, 'ssh-dss' ) -> "ssh_host_dsa_key"; 1220file_base_name(system, 'ssh-ed25519' ) -> "ssh_host_ed25519_key"; 1221file_base_name(system, 'ssh-ed448' ) -> "ssh_host_ed448_key"; 1222file_base_name(system, 'ssh-rsa' ) -> "ssh_host_rsa_key"; 1223 1224file_base_name(system_src, 'ecdsa-sha2-nistp256') -> "ssh_host_ecdsa_key256"; 1225file_base_name(system_src, 'ecdsa-sha2-nistp384') -> "ssh_host_ecdsa_key384"; 1226file_base_name(system_src, 'ecdsa-sha2-nistp521') -> "ssh_host_ecdsa_key521"; 1227file_base_name(system_src, Alg) -> file_base_name(system, Alg). 1228 1229%%%---------------------------------------------------------------- 1230