1: -module(sasl_external_SUITE). 2: 3: -compile([export_all, nowarn_export_all]). 4: 5: -include_lib("common_test/include/ct.hrl"). 6: -include_lib("eunit/include/eunit.hrl"). 7: -include_lib("exml/include/exml.hrl"). 8: 9: -import(domain_helper, [domain/0]). 10: 11: all() -> 12: [ 13: {group, fast_tls}, 14: {group, just_tls}]. 15: 16: groups() -> 17: [{standard_keep_auth, [{group, registered}, {group, not_registered}]}, 18: {registered, [parallel], [cert_one_xmpp_addrs_no_identity]}, 19: {not_registered, [parallel], [cert_one_xmpp_addrs_no_identity_not_registered]}, 20: {standard, [parallel], standard_test_cases()}, 21: {use_common_name, [parallel], use_common_name_test_cases()}, 22: {allow_just_user_identity, [parallel], allow_just_user_identity_test_cases()}, 23: {demo_verification_module, [parallel], demo_verification_module_test_cases()}, 24: {self_signed_certs_allowed, [parallel], self_signed_certs_allowed_test_cases()}, 25: {self_signed_certs_not_allowed, [parallel], self_signed_certs_not_allowed_test_cases()}, 26: {ca_signed, [self_signed_certs_not_allowed_group() | base_groups()]}, 27: {self_signed, [self_signed_certs_allowed_group() | base_groups()]}, 28: {fast_tls, [{group, ca_signed}]}, 29: {just_tls, all_groups()} ]. 30: 31: all_groups() -> 32: [{group, self_signed}, 33: {group, ca_signed}]. 34: 35: self_signed_certs_allowed_group() -> 36: {group, self_signed_certs_allowed}. 37: self_signed_certs_not_allowed_group() -> 38: {group, self_signed_certs_not_allowed}. 39: 40: base_groups() -> 41: [{group, standard}, 42: {group, standard_keep_auth}, 43: {group, use_common_name}, 44: {group, allow_just_user_identity}, 45: {group, demo_verification_module}]. 46: 47: standard_test_cases() -> 48: [ 49: cert_no_xmpp_addrs_fails, 50: cert_no_xmpp_addrs_no_identity, 51: cert_one_xmpp_addr_identity_correct, 52: cert_one_xmpp_addrs_no_identity, 53: cert_one_xmpp_addr_wrong_hostname, 54: cert_more_xmpp_addrs_identity_correct, 55: cert_more_xmpp_addrs_no_identity_fails, 56: cert_more_xmpp_addrs_wrong_identity_fails 57: ]. 58: 59: use_common_name_test_cases() -> 60: [ 61: cert_with_cn_no_xmpp_addrs_identity_correct, 62: cert_with_cn_no_xmpp_addrs_wrong_identity_fails, 63: cert_with_cn_no_xmpp_addrs_no_identity 64: ]. 65: 66: allow_just_user_identity_test_cases() -> 67: [ 68: cert_no_xmpp_addrs_just_use_identity 69: ]. 70: 71: demo_verification_module_test_cases()-> 72: [cert_no_xmpp_addrs_just_use_identity, 73: cert_one_xmpp_addrs_no_identity, 74: cert_with_jid_cn_no_xmpp_addrs_no_identity, 75: cert_with_jid_cn_many_xmpp_addrs_no_identity, 76: cert_more_xmpp_addrs_no_identity_fails, 77: cert_no_xmpp_addrs_no_identity]. 78: 79: self_signed_certs_allowed_test_cases() -> 80: [self_signed_cert_is_allowed_with_tls, 81: self_signed_cert_is_allowed_with_ws, 82: self_signed_cert_is_allowed_with_bosh, 83: no_cert_fails_to_authenticate]. 84: 85: self_signed_certs_not_allowed_test_cases() -> 86: [self_signed_cert_fails_to_authenticate_with_tls, 87: self_signed_cert_fails_to_authenticate_with_ws, 88: self_signed_cert_fails_to_authenticate_with_bosh, 89: ca_signed_cert_is_allowed_with_ws, 90: ca_signed_cert_is_allowed_with_bosh, 91: no_cert_fails_to_authenticate]. 92: 93: init_per_suite(Config) -> 94: Config0 = escalus:init_per_suite(Config), 95: Config1 = ejabberd_node_utils:init(Config0), 96: ejabberd_node_utils:backup_config_file(Config1), 97: generate_certs(Config1). 98: 99: end_per_suite(Config) -> 100: ejabberd_node_utils:restore_config_file(Config), 101: ejabberd_node_utils:restart_application(mongooseim), 102: escalus:end_per_suite(Config). 103: 104: init_per_group(just_tls, Config) -> 105: [{tls_module, "just_tls"} | Config]; 106: init_per_group(fast_tls, Config) -> 107: [{tls_module, "fast_tls"} | Config]; 108: init_per_group(ca_signed, Config) -> 109: [{signed, ca}, 110: {ssl_options, "\n tls.disconnect_on_failure = false"}, 111: {verify_mode, "\n tls.verify_mode = \"peer\""} | Config]; 112: init_per_group(self_signed, Config) -> 113: [{signed, self}, 114: {verify_mode, "\n tls.verify_mode = \"selfsigned_peer\""} | Config]; 115: init_per_group(standard, Config) -> 116: modify_config_and_restart("\"standard\"", Config), 117: Config; 118: init_per_group(standard_keep_auth, Config) -> 119: Config1 = [{auth_methods, []} | Config], 120: modify_config_and_restart("\"standard\"", Config1), 121: case mongoose_helper:supports_sasl_module(cyrsasl_external) of 122: false -> {skip, "SASL External not supported"}; 123: true -> Config1 124: end; 125: init_per_group(registered, Config) -> 126: escalus:create_users(Config, [{bob, generate_user_tcp(Config, username("bob", Config))}]); 127: init_per_group(use_common_name, Config) -> 128: modify_config_and_restart("\"standard\", \"common_name\"", Config), 129: Config; 130: init_per_group(allow_just_user_identity, Config) -> 131: modify_config_and_restart("\"standard\", \"auth_id\"", Config), 132: Config; 133: init_per_group(demo_verification_module, Config) -> 134: modify_config_and_restart("\"cyrsasl_external_verification\"", Config), 135: Config; 136: init_per_group(self_signed_certs_allowed, Config) -> 137: modify_config_and_restart("\"standard\"", Config), 138: Config; 139: init_per_group(self_signed_certs_not_allowed, Config) -> 140: modify_config_and_restart("\"standard\"", Config), 141: Config; 142: init_per_group(_, Config) -> 143: Config. 144: 145: modify_config_and_restart(CyrsaslExternalConfig, Config) -> 146: TLSModule = escalus_config:get_config(tls_module, Config, "just_tls"), 147: VerifyMode = escalus_config:get_config(verify_mode, Config, ""), 148: SSLOpts = case TLSModule of 149: "just_tls" -> escalus_config:get_config(ssl_options, Config, "") ++ VerifyMode; 150: "fast_tls" -> "" 151: end, 152: AuthMethods = escalus_config:get_config(auth_methods, Config, 153: [{auth_method, "pki"}, {auth_method_opts, false}]), 154: CACertFile = filename:join([path_helper:repo_dir(Config), 155: "tools", "ssl", "ca-clients", "cacert.pem"]), 156: NewConfigValues = [{tls_config, "tls.module = \"" ++ TLSModule ++ "\"\n" 157: " tls.certfile = \"priv/ssl/fake_server.pem\"\n" 158: " tls.mode = \"starttls\"\n" 159: " tls.verify_peer = true\n" 160: " tls.cacertfile = \"" ++ CACertFile ++ "\"" 161: ++ SSLOpts}, 162: {https_config, "tls.certfile = \"priv/ssl/fake_cert.pem\"\n" 163: " tls.keyfile = \"priv/ssl/fake_key.pem\"\n" 164: " tls.password = \"\"\n" 165: " tls.verify_peer = true\n" 166: " tls.cacertfile = \"" ++ CACertFile ++ "\"" 167: ++ VerifyMode}, 168: {cyrsasl_external, CyrsaslExternalConfig}, 169: {sasl_mechanisms, "\"external\""} | AuthMethods], 170: ejabberd_node_utils:modify_config_file(NewConfigValues, Config), 171: ejabberd_node_utils:restart_application(mongooseim). 172: 173: end_per_group(registered, Config) -> 174: escalus:delete_users(Config, [{bob, generate_user_tcp(Config, username("bob", Config))}]); 175: end_per_group(_, _Config) -> 176: ok. 177: 178: cert_more_xmpp_addrs_identity_correct(C) -> 179: User = username("kate", C), 180: %% More than one xmpp_addr and specified identity, common_name not used 181: UserSpec = [{requested_name, requested_name(User)} | 182: generate_user_tcp(C, User)], 183: {ok, Client, _} = escalus_connection:start(UserSpec), 184: escalus_connection:stop(Client). 185: 186: cert_one_xmpp_addr_identity_correct(C) -> 187: User = username("bob", C), 188: UserSpec = [{requested_name, requested_name(User)} | 189: generate_user_tcp(C, User)], 190: cert_fails_to_authenticate(UserSpec). 191: 192: cert_no_xmpp_addrs_fails(C) -> 193: User = username("john", C), 194: UserSpec = [{requested_name, requested_name(User)} | 195: generate_user_tcp(C, User)], 196: cert_fails_to_authenticate(UserSpec). 197: 198: cert_no_xmpp_addrs_just_use_identity(C) -> 199: User = username("not-mike", C), 200: UserSpec = [{requested_name, requested_name("mike")} | 201: generate_user_tcp(C, User)], 202: {ok, Client, _} = escalus_connection:start(UserSpec), 203: escalus_connection:stop(Client). 204: 205: cert_no_xmpp_addrs_no_identity(C) -> 206: User = username("john", C), 207: UserSpec = generate_user_tcp(C, User), 208: cert_fails_to_authenticate(UserSpec). 209: 210: cert_more_xmpp_addrs_no_identity_fails(C) -> 211: User = username("not-alice", C), 212: UserSpec = generate_user_tcp(C, User), 213: cert_fails_to_authenticate(UserSpec). 214: 215: cert_one_xmpp_addrs_no_identity(C) -> 216: User = username("bob", C), 217: UserSpec = generate_user_tcp(C, User), 218: {ok, Client, _} = escalus_connection:start(UserSpec), 219: escalus_connection:stop(Client). 220: 221: cert_one_xmpp_addrs_no_identity_not_registered(C) -> 222: User = username("bob", C), 223: UserSpec = generate_user_tcp(C, User), 224: cert_fails_to_authenticate(UserSpec). 225: 226: cert_with_cn_no_xmpp_addrs_no_identity(C) -> 227: User = username("john", C), 228: UserSpec = generate_user_tcp(C, User), 229: {ok, Client, _} = escalus_connection:start(UserSpec), 230: escalus_connection:stop(Client). 231: 232: cert_with_jid_cn_no_xmpp_addrs_no_identity(C) -> 233: User = add_domain_str("john"), 234: UserSpec = generate_user_tcp(C, User), 235: {ok, Client, _} = escalus_connection:start(UserSpec), 236: escalus_connection:stop(Client). 237: 238: cert_with_jid_cn_many_xmpp_addrs_no_identity(C) -> 239: User = add_domain_str("grace"), 240: UserSpec = generate_user_tcp(C, User), 241: {ok, Client, _} = escalus_connection:start(UserSpec), 242: escalus_connection:stop(Client). 243: 244: cert_with_cn_no_xmpp_addrs_identity_correct(C) -> 245: User = username("john", C), 246: UserSpec = [{requested_name, requested_name(User)} | 247: generate_user_tcp(C, User)], 248: {ok, Client, _} = escalus_connection:start(UserSpec), 249: escalus_connection:stop(Client). 250: 251: cert_with_cn_no_xmpp_addrs_wrong_identity_fails(C) -> 252: User = username("not-mike", C), 253: UserSpec = [{requested_name, requested_name("mike")} | 254: generate_user_tcp(C, User)], 255: cert_fails_to_authenticate(UserSpec). 256: 257: cert_more_xmpp_addrs_wrong_identity_fails(C) -> 258: User = username("grace", C), 259: UserSpec = [{requested_name, requested_name(User)} | 260: generate_user_tcp(C, User)], 261: cert_fails_to_authenticate(UserSpec). 262: 263: cert_one_xmpp_addr_wrong_hostname(C) -> 264: User = username("bob", C), 265: UserSpec = [{requested_name, requested_name(User)} | 266: generate_user_tcp(C, User)], 267: cert_fails_to_authenticate(UserSpec). 268: 269: ca_signed_cert_is_allowed_with_ws(C) -> 270: UserSpec = generate_user(C, "bob", escalus_ws), 271: {ok, Client, _} = escalus_connection:start(UserSpec), 272: 273: escalus_connection:stop(Client). 274: 275: ca_signed_cert_is_allowed_with_bosh(C) -> 276: UserSpec = generate_user(C, "bob", escalus_bosh), 277: {ok, Client, _} = escalus_connection:start(UserSpec), 278: 279: escalus_connection:stop(Client). 280: 281: self_signed_cert_fails_to_authenticate_with_tls(C) -> 282: self_signed_cert_fails_to_authenticate(C, escalus_tcp). 283: 284: self_signed_cert_fails_to_authenticate_with_ws(C) -> 285: self_signed_cert_fails_to_authenticate(C, escalus_ws). 286: 287: self_signed_cert_fails_to_authenticate_with_bosh(C) -> 288: self_signed_cert_fails_to_authenticate(C, escalus_bosh). 289: 290: cert_fails_to_authenticate(UserSpec) -> 291: Self = self(), 292: F = fun() -> 293: {ok, Client, _} = escalus_connection:start(UserSpec), 294: Self ! escalus_connected, 295: escalus_connection:stop(Client) 296: end, 297: %% We spawn the process trying to connect because otherwise the testcase may crash 298: %% due linked process crash (client's process are started with start_link) 299: Pid = spawn(F), 300: MRef = erlang:monitor(process, Pid), 301: 302: receive 303: {'DOWN', MRef, process, Pid, _Reason} -> 304: ok; 305: escalus_connected -> 306: ct:fail(authenticated_but_should_not) 307: after 10000 -> 308: ct:fail(timeout_waiting_for_authentication_error) 309: end. 310: 311: self_signed_cert_fails_to_authenticate(C, EscalusTransport) -> 312: Self = self(), 313: F = fun() -> 314: UserSpec = generate_user(C, "greg-self-signed", EscalusTransport), 315: {ok, Client, _} = escalus_connection:start(UserSpec), 316: Self ! escalus_connected, 317: escalus_connection:stop(Client) 318: end, 319: %% We spawn the process trying to connect because otherwise the testcase may crash 320: %% due linked process crash (client's process are started with start_link) 321: Pid = spawn(F), 322: MRef = erlang:monitor(process, Pid), 323: 324: receive 325: {'DOWN', MRef, process, Pid, _Reason} -> 326: ok; 327: escalus_connected -> 328: ct:fail(authenticated_but_should_not) 329: after 10000 -> 330: ct:fail(timeout_waiting_for_authentication_error) 331: end. 332: 333: self_signed_cert_is_allowed_with_tls(C) -> 334: self_signed_cert_is_allowed_with(escalus_tcp, C). 335: 336: self_signed_cert_is_allowed_with_ws(C) -> 337: self_signed_cert_is_allowed_with(escalus_ws, C). 338: 339: self_signed_cert_is_allowed_with_bosh(C) -> 340: self_signed_cert_is_allowed_with(escalus_bosh, C). 341: 342: self_signed_cert_is_allowed_with(EscalusTransport, C) -> 343: UserSpec = generate_user(C, "bob-self-signed", EscalusTransport), 344: {ok, Client, _} = escalus_connection:start(UserSpec), 345: escalus_connection:stop(Client). 346: 347: no_cert_fails_to_authenticate(_C) -> 348: UserSpec = [{username, <<"no_cert_user">>}, 349: {server, domain()}, 350: {host, <<"localhost">>}, 351: {port, ct:get_config({hosts, mim, c2s_port})}, 352: {password, <<"break_me">>}, 353: {resource, <<>>}, %% Allow the server to generate the resource 354: {auth, {escalus_auth, auth_sasl_external}}, 355: {starttls, required}], 356: 357: Result = escalus_connection:start(UserSpec), 358: ?assertMatch({error, {connection_step_failed, _, _}}, Result), 359: {error, {connection_step_failed, _Call, Details}} = Result, 360: ?assertMatch({auth_failed, _, #xmlel{name = <<"failure">>}}, Details), 361: ok. 362: 363: generate_certs(C) -> 364: CA = [#{cn => "not-alice", xmpp_addrs => [add_domain_str("alice"), "alice@fed1"]}, 365: #{cn => "kate", xmpp_addrs => [add_domain_str("kate"), "kate@fed1"]}, 366: #{cn => "bob", xmpp_addrs => [add_domain_str("bob")]}, 367: #{cn => "greg", xmpp_addrs => [add_domain_str("greg")]}, 368: #{cn => "john", xmpp_addrs => undefined}, 369: #{cn => add_domain_str("john"), xmpp_addrs => undefined}, 370: #{cn => "not-mike", xmpp_addrs => undefined}, 371: #{cn => "grace", xmpp_addrs => ["grace@fed1", "grace@reg1"]}, 372: #{cn => add_domain_str("grace"), xmpp_addrs => ["grace@fed1", "grace@reg1"]}], 373: SelfSigned = [ M#{cn => CN ++ "-self-signed", signed => self, xmpp_addrs => replace_addrs(Addrs)} 374: || M = #{ cn := CN , xmpp_addrs := Addrs} <- CA], 375: CertSpecs = CA ++ SelfSigned, 376: Certs = [{maps:get(cn, CertSpec), generate_cert(C, CertSpec)} || CertSpec <- CertSpecs], 377: [{certs, maps:from_list(Certs)} | C]. 378: 379: generate_cert(C, #{cn := User} = CertSpec) -> 380: ConfigTemplate = filename:join(?config(mim_data_dir, C), "openssl-user.cnf"), 381: {ok, Template} = file:read_file(ConfigTemplate), 382: XMPPAddrs = maps:get(xmpp_addrs, CertSpec, undefined), 383: TemplateValues = prepare_template_values(User, XMPPAddrs), 384: OpenSSLConfig = bbmustache:render(Template, TemplateValues), 385: UserConfig = filename:join(?config(priv_dir, C), User ++ ".cfg"), 386: ct:log("OpenSSL config: ~ts~n~ts", [UserConfig, OpenSSLConfig]), 387: file:write_file(UserConfig, OpenSSLConfig), 388: UserKey = filename:join(?config(priv_dir, C), User ++ "_key.pem"), 389: 390: case maps:get(signed, CertSpec, ca) of 391: ca -> 392: generate_ca_signed_cert(C, User, UserConfig, UserKey); 393: self -> 394: generate_self_signed_cert(C, User, UserConfig, UserKey) 395: end. 396: 397: generate_ca_signed_cert(C, User, UserConfig, UserKey ) -> 398: UserCsr = filename:join(?config(priv_dir, C), User ++ ".csr"), 399: Cmd = ["openssl req -config ", UserConfig, " -newkey rsa:2048 -sha256 -nodes -out ", 400: UserCsr, " -keyout ", UserKey, " -outform PEM"], 401: Out = os:cmd(Cmd), 402: ct:log("generate_ca_signed_cert 1:~nCmd ~p~nOut ~ts", [Cmd, Out]), 403: UserCert = filename:join(?config(priv_dir, C), User ++ "_cert.pem"), 404: SignCmd = filename:join(?config(mim_data_dir, C), "sign_cert.sh"), 405: Cmd2 = [SignCmd, " --req ", UserCsr, " --out ", UserCert], 406: SSLDir = filename:join([path_helper:repo_dir(C), "tools", "ssl"]), 407: OutLog = os:cmd("cd " ++ SSLDir ++ " && " ++ Cmd2), 408: ct:log("generate_ca_signed_cert 2:~nCmd ~p~nOut ~ts", [Cmd2, OutLog]), 409: #{key => UserKey, 410: cert => UserCert}. 411: 412: generate_self_signed_cert(C, User, UserConfig, UserKey) -> 413: UserCert = filename:join(?config(priv_dir, C), User ++ "_self_signed_cert.pem"), 414: Cmd = ["openssl req -config ", UserConfig, " -newkey rsa:2048 -sha256 -nodes -out ", 415: UserCert, " -keyout ", UserKey, " -x509 -outform PEM -extensions client_req_extensions"], 416: OutLog = os:cmd(Cmd), 417: ct:log("generate_self_signed_cert:~nCmd ~p~nOut ~ts", [Cmd, OutLog]), 418: #{key => UserKey, 419: cert => UserCert}. 420: 421: generate_user_tcp(C, User) -> 422: generate_user(C, User, escalus_tcp). 423: 424: generate_user(C, User, Transport) -> 425: Certs = ?config(certs, C), 426: UserCert = maps:get(User, Certs), 427: 428: Common = [{username, list_to_binary(User)}, 429: {server, domain()}, 430: {host, <<"localhost">>}, 431: {password, <<"break_me">>}, 432: {resource, <<>>}, %% Allow the server to generate the resource 433: {auth, {escalus_auth, auth_sasl_external}}, 434: {transport, Transport}, 435: {ssl_opts, [{certfile, maps:get(cert, UserCert)}, 436: {keyfile, maps:get(key, UserCert)}]}], 437: Common ++ transport_specific_options(Transport) 438: ++ [{port, ct:get_config({hosts, mim, c2s_port})}]. 439: 440: transport_specific_options(escalus_tcp) -> 441: [{starttls, required}]; 442: transport_specific_options(_) -> 443: [{port, ct:get_config({hosts, mim, cowboy_secure_port})}, 444: {ssl, true}]. 445: 446: prepare_template_values(User, XMPPAddrsIn) -> 447: Defaults = #{"cn" => User, 448: "xmppAddrs" => ""}, 449: XMPPAddrs = maybe_prepare_xmpp_addresses(XMPPAddrsIn), 450: Defaults#{"xmppAddrs" => XMPPAddrs}. 451: 452: maybe_prepare_xmpp_addresses(undefined) -> 453: ""; 454: maybe_prepare_xmpp_addresses(Addrs) when is_list(Addrs) -> 455: AddrsWithSeq = lists:zip(Addrs, lists:seq(1, length(Addrs))), 456: Entries = [make_xmpp_addr_entry(Addr, I) || {Addr, I} <- AddrsWithSeq], 457: string:join(Entries, "\n"). 458: 459: make_xmpp_addr_entry(Addr, I) -> 460: % id-on-xmppAddr OID is specified in the openssl-user.cnf config file 461: "otherName." ++ integer_to_list(I) ++ " = id-on-xmppAddr;UTF8:" ++ Addr. 462: 463: requested_name(User) -> 464: add_domain(list_to_binary(User)). 465: 466: username(Name, Config) -> 467: case escalus_config:get_config(signed, Config, ca) of 468: self -> 469: Name ++ "-self-signed"; 470: ca -> 471: Name 472: end. 473: 474: replace_addrs(undefined) -> 475: undefined; 476: replace_addrs(Addresses) -> 477: lists:map( fun(Addr) -> [User, Hostname] = binary:split(list_to_binary(Addr), <<"@">>), 478: binary_to_list(<<User/binary, <<"-self-signed@">>/binary, Hostname/binary>>) end, Addresses). 479: 480: -spec add_domain_str(User :: string()) -> string(). 481: add_domain_str(User) -> 482: User ++ "@" ++ binary:bin_to_list(domain()). 483: 484: -spec add_domain(User :: binary()) -> binary(). 485: add_domain(User) -> 486: <<User/binary, "@", (domain())/binary>>.