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.cacertfile = \"" ++ CACertFile ++ "\"" 159: ++ SSLOpts}, 160: {https_config, "tls.certfile = \"priv/ssl/fake_cert.pem\"\n" 161: " tls.keyfile = \"priv/ssl/fake_key.pem\"\n" 162: " tls.password = \"\"\n" 163: " tls.cacertfile = \"" ++ CACertFile ++ "\"" 164: ++ VerifyMode}, 165: {cyrsasl_external, CyrsaslExternalConfig}, 166: {sasl_mechanisms, "\"external\""} | AuthMethods], 167: ejabberd_node_utils:modify_config_file(NewConfigValues, Config), 168: ejabberd_node_utils:restart_application(mongooseim). 169: 170: end_per_group(registered, Config) -> 171: escalus:delete_users(Config, [{bob, generate_user_tcp(Config, username("bob", Config))}]); 172: end_per_group(_, _Config) -> 173: ok. 174: 175: cert_more_xmpp_addrs_identity_correct(C) -> 176: User = username("kate", C), 177: %% More than one xmpp_addr and specified identity, common_name not used 178: UserSpec = [{requested_name, requested_name(User)} | 179: generate_user_tcp(C, User)], 180: {ok, Client, _} = escalus_connection:start(UserSpec), 181: escalus_connection:stop(Client). 182: 183: cert_one_xmpp_addr_identity_correct(C) -> 184: User = username("bob", C), 185: UserSpec = [{requested_name, requested_name(User)} | 186: generate_user_tcp(C, User)], 187: cert_fails_to_authenticate(UserSpec). 188: 189: cert_no_xmpp_addrs_fails(C) -> 190: User = username("john", C), 191: UserSpec = [{requested_name, requested_name(User)} | 192: generate_user_tcp(C, User)], 193: cert_fails_to_authenticate(UserSpec). 194: 195: cert_no_xmpp_addrs_just_use_identity(C) -> 196: User = username("not-mike", C), 197: UserSpec = [{requested_name, requested_name("mike")} | 198: generate_user_tcp(C, User)], 199: {ok, Client, _} = escalus_connection:start(UserSpec), 200: escalus_connection:stop(Client). 201: 202: cert_no_xmpp_addrs_no_identity(C) -> 203: User = username("john", C), 204: UserSpec = generate_user_tcp(C, User), 205: cert_fails_to_authenticate(UserSpec). 206: 207: cert_more_xmpp_addrs_no_identity_fails(C) -> 208: User = username("not-alice", C), 209: UserSpec = generate_user_tcp(C, User), 210: cert_fails_to_authenticate(UserSpec). 211: 212: cert_one_xmpp_addrs_no_identity(C) -> 213: User = username("bob", C), 214: UserSpec = generate_user_tcp(C, User), 215: {ok, Client, _} = escalus_connection:start(UserSpec), 216: escalus_connection:stop(Client). 217: 218: cert_one_xmpp_addrs_no_identity_not_registered(C) -> 219: User = username("bob", C), 220: UserSpec = generate_user_tcp(C, User), 221: cert_fails_to_authenticate(UserSpec). 222: 223: cert_with_cn_no_xmpp_addrs_no_identity(C) -> 224: User = username("john", C), 225: UserSpec = generate_user_tcp(C, User), 226: {ok, Client, _} = escalus_connection:start(UserSpec), 227: escalus_connection:stop(Client). 228: 229: cert_with_jid_cn_no_xmpp_addrs_no_identity(C) -> 230: User = add_domain_str("john"), 231: UserSpec = generate_user_tcp(C, User), 232: {ok, Client, _} = escalus_connection:start(UserSpec), 233: escalus_connection:stop(Client). 234: 235: cert_with_jid_cn_many_xmpp_addrs_no_identity(C) -> 236: User = add_domain_str("grace"), 237: UserSpec = generate_user_tcp(C, User), 238: {ok, Client, _} = escalus_connection:start(UserSpec), 239: escalus_connection:stop(Client). 240: 241: cert_with_cn_no_xmpp_addrs_identity_correct(C) -> 242: User = username("john", C), 243: UserSpec = [{requested_name, requested_name(User)} | 244: generate_user_tcp(C, User)], 245: {ok, Client, _} = escalus_connection:start(UserSpec), 246: escalus_connection:stop(Client). 247: 248: cert_with_cn_no_xmpp_addrs_wrong_identity_fails(C) -> 249: User = username("not-mike", C), 250: UserSpec = [{requested_name, requested_name("mike")} | 251: generate_user_tcp(C, User)], 252: cert_fails_to_authenticate(UserSpec). 253: 254: cert_more_xmpp_addrs_wrong_identity_fails(C) -> 255: User = username("grace", C), 256: UserSpec = [{requested_name, requested_name(User)} | 257: generate_user_tcp(C, User)], 258: cert_fails_to_authenticate(UserSpec). 259: 260: cert_one_xmpp_addr_wrong_hostname(C) -> 261: User = username("bob", C), 262: UserSpec = [{requested_name, requested_name(User)} | 263: generate_user_tcp(C, User)], 264: cert_fails_to_authenticate(UserSpec). 265: 266: ca_signed_cert_is_allowed_with_ws(C) -> 267: UserSpec = generate_user(C, "bob", escalus_ws), 268: {ok, Client, _} = escalus_connection:start(UserSpec), 269: 270: escalus_connection:stop(Client). 271: 272: ca_signed_cert_is_allowed_with_bosh(C) -> 273: UserSpec = generate_user(C, "bob", escalus_bosh), 274: {ok, Client, _} = escalus_connection:start(UserSpec), 275: 276: escalus_connection:stop(Client). 277: 278: self_signed_cert_fails_to_authenticate_with_tls(C) -> 279: self_signed_cert_fails_to_authenticate(C, escalus_tcp). 280: 281: self_signed_cert_fails_to_authenticate_with_ws(C) -> 282: self_signed_cert_fails_to_authenticate(C, escalus_ws). 283: 284: self_signed_cert_fails_to_authenticate_with_bosh(C) -> 285: self_signed_cert_fails_to_authenticate(C, escalus_bosh). 286: 287: cert_fails_to_authenticate(UserSpec) -> 288: Self = self(), 289: F = fun() -> 290: {ok, Client, _} = escalus_connection:start(UserSpec), 291: Self ! escalus_connected, 292: escalus_connection:stop(Client) 293: end, 294: %% We spawn the process trying to connect because otherwise the testcase may crash 295: %% due linked process crash (client's process are started with start_link) 296: Pid = spawn(F), 297: MRef = erlang:monitor(process, Pid), 298: 299: receive 300: {'DOWN', MRef, process, Pid, _Reason} -> 301: ok; 302: escalus_connected -> 303: ct:fail(authenticated_but_should_not) 304: after 10000 -> 305: ct:fail(timeout_waiting_for_authentication_error) 306: end. 307: 308: self_signed_cert_fails_to_authenticate(C, EscalusTransport) -> 309: Self = self(), 310: F = fun() -> 311: UserSpec = generate_user(C, "greg-self-signed", EscalusTransport), 312: {ok, Client, _} = escalus_connection:start(UserSpec), 313: Self ! escalus_connected, 314: escalus_connection:stop(Client) 315: end, 316: %% We spawn the process trying to connect because otherwise the testcase may crash 317: %% due linked process crash (client's process are started with start_link) 318: Pid = spawn(F), 319: MRef = erlang:monitor(process, Pid), 320: 321: receive 322: {'DOWN', MRef, process, Pid, _Reason} -> 323: ok; 324: escalus_connected -> 325: ct:fail(authenticated_but_should_not) 326: after 10000 -> 327: ct:fail(timeout_waiting_for_authentication_error) 328: end. 329: 330: self_signed_cert_is_allowed_with_tls(C) -> 331: self_signed_cert_is_allowed_with(escalus_tcp, C). 332: 333: self_signed_cert_is_allowed_with_ws(C) -> 334: self_signed_cert_is_allowed_with(escalus_ws, C). 335: 336: self_signed_cert_is_allowed_with_bosh(C) -> 337: self_signed_cert_is_allowed_with(escalus_bosh, C). 338: 339: self_signed_cert_is_allowed_with(EscalusTransport, C) -> 340: UserSpec = generate_user(C, "bob-self-signed", EscalusTransport), 341: {ok, Client, _} = escalus_connection:start(UserSpec), 342: escalus_connection:stop(Client). 343: 344: no_cert_fails_to_authenticate(_C) -> 345: UserSpec = [{username, <<"no_cert_user">>}, 346: {server, domain()}, 347: {host, <<"localhost">>}, 348: {port, ct:get_config({hosts, mim, c2s_port})}, 349: {password, <<"break_me">>}, 350: {resource, <<>>}, %% Allow the server to generate the resource 351: {auth, {escalus_auth, auth_sasl_external}}, 352: {starttls, required}], 353: 354: Result = escalus_connection:start(UserSpec), 355: ?assertMatch({error, {connection_step_failed, _, _}}, Result), 356: {error, {connection_step_failed, _Call, Details}} = Result, 357: ?assertMatch({auth_failed, _, #xmlel{name = <<"failure">>}}, Details), 358: ok. 359: 360: generate_certs(C) -> 361: CA = [#{cn => "not-alice", xmpp_addrs => [add_domain_str("alice"), "alice@fed1"]}, 362: #{cn => "kate", xmpp_addrs => [add_domain_str("kate"), "kate@fed1"]}, 363: #{cn => "bob", xmpp_addrs => [add_domain_str("bob")]}, 364: #{cn => "greg", xmpp_addrs => [add_domain_str("greg")]}, 365: #{cn => "john", xmpp_addrs => undefined}, 366: #{cn => add_domain_str("john"), xmpp_addrs => undefined}, 367: #{cn => "not-mike", xmpp_addrs => undefined}, 368: #{cn => "grace", xmpp_addrs => ["grace@fed1", "grace@reg1"]}, 369: #{cn => add_domain_str("grace"), xmpp_addrs => ["grace@fed1", "grace@reg1"]}], 370: SelfSigned = [ M#{cn => CN ++ "-self-signed", signed => self, xmpp_addrs => replace_addrs(Addrs)} 371: || M = #{ cn := CN , xmpp_addrs := Addrs} <- CA], 372: CertSpecs = CA ++ SelfSigned, 373: Certs = [{maps:get(cn, CertSpec), generate_cert(C, CertSpec)} || CertSpec <- CertSpecs], 374: [{certs, maps:from_list(Certs)} | C]. 375: 376: generate_cert(C, #{cn := User} = CertSpec) -> 377: ConfigTemplate = filename:join(?config(mim_data_dir, C), "openssl-user.cnf"), 378: {ok, Template} = file:read_file(ConfigTemplate), 379: XMPPAddrs = maps:get(xmpp_addrs, CertSpec, undefined), 380: TemplateValues = prepare_template_values(User, XMPPAddrs), 381: OpenSSLConfig = bbmustache:render(Template, TemplateValues), 382: UserConfig = filename:join(?config(priv_dir, C), User ++ ".cfg"), 383: ct:log("OpenSSL config: ~ts~n~ts", [UserConfig, OpenSSLConfig]), 384: file:write_file(UserConfig, OpenSSLConfig), 385: UserKey = filename:join(?config(priv_dir, C), User ++ "_key.pem"), 386: 387: case maps:get(signed, CertSpec, ca) of 388: ca -> 389: generate_ca_signed_cert(C, User, UserConfig, UserKey); 390: self -> 391: generate_self_signed_cert(C, User, UserConfig, UserKey) 392: end. 393: 394: generate_ca_signed_cert(C, User, UserConfig, UserKey ) -> 395: UserCsr = filename:join(?config(priv_dir, C), User ++ ".csr"), 396: Cmd = ["openssl req -config ", UserConfig, " -newkey rsa:2048 -sha256 -nodes -out ", 397: UserCsr, " -keyout ", UserKey, " -outform PEM"], 398: Out = os:cmd(Cmd), 399: ct:log("generate_ca_signed_cert 1:~nCmd ~p~nOut ~ts", [Cmd, Out]), 400: UserCert = filename:join(?config(priv_dir, C), User ++ "_cert.pem"), 401: SignCmd = filename:join(?config(mim_data_dir, C), "sign_cert.sh"), 402: Cmd2 = [SignCmd, " --req ", UserCsr, " --out ", UserCert], 403: SSLDir = filename:join([path_helper:repo_dir(C), "tools", "ssl"]), 404: OutLog = os:cmd("cd " ++ SSLDir ++ " && " ++ Cmd2), 405: ct:log("generate_ca_signed_cert 2:~nCmd ~p~nOut ~ts", [Cmd2, OutLog]), 406: #{key => UserKey, 407: cert => UserCert}. 408: 409: generate_self_signed_cert(C, User, UserConfig, UserKey) -> 410: UserCert = filename:join(?config(priv_dir, C), User ++ "_self_signed_cert.pem"), 411: Cmd = ["openssl req -config ", UserConfig, " -newkey rsa:2048 -sha256 -nodes -out ", 412: UserCert, " -keyout ", UserKey, " -x509 -outform PEM -extensions client_req_extensions"], 413: OutLog = os:cmd(Cmd), 414: ct:log("generate_self_signed_cert:~nCmd ~p~nOut ~ts", [Cmd, OutLog]), 415: #{key => UserKey, 416: cert => UserCert}. 417: 418: generate_user_tcp(C, User) -> 419: generate_user(C, User, escalus_tcp). 420: 421: generate_user(C, User, Transport) -> 422: Certs = ?config(certs, C), 423: UserCert = maps:get(User, Certs), 424: Common = [{username, list_to_binary(User)}, 425: {server, domain()}, 426: {host, <<"localhost">>}, 427: {password, <<"break_me">>}, 428: {resource, <<>>}, %% Allow the server to generate the resource 429: {auth, {escalus_auth, auth_sasl_external}}, 430: {transport, Transport}, 431: {ssl_opts, [{versions, ['tlsv1.2']}, 432: {certfile, maps:get(cert, UserCert)}, 433: {keyfile, maps:get(key, UserCert)}]}], 434: Common ++ transport_specific_options(Transport) 435: ++ [{port, ct:get_config({hosts, mim, c2s_port})}]. 436: 437: transport_specific_options(escalus_tcp) -> 438: [{starttls, required}]; 439: transport_specific_options(_) -> 440: [{port, ct:get_config({hosts, mim, cowboy_secure_port})}, 441: {ssl, true}]. 442: 443: prepare_template_values(User, XMPPAddrsIn) -> 444: Defaults = #{"cn" => User, 445: "xmppAddrs" => ""}, 446: XMPPAddrs = maybe_prepare_xmpp_addresses(XMPPAddrsIn), 447: Defaults#{"xmppAddrs" => XMPPAddrs}. 448: 449: maybe_prepare_xmpp_addresses(undefined) -> 450: ""; 451: maybe_prepare_xmpp_addresses(Addrs) when is_list(Addrs) -> 452: AddrsWithSeq = lists:zip(Addrs, lists:seq(1, length(Addrs))), 453: Entries = [make_xmpp_addr_entry(Addr, I) || {Addr, I} <- AddrsWithSeq], 454: string:join(Entries, "\n"). 455: 456: make_xmpp_addr_entry(Addr, I) -> 457: % id-on-xmppAddr OID is specified in the openssl-user.cnf config file 458: "otherName." ++ integer_to_list(I) ++ " = id-on-xmppAddr;UTF8:" ++ Addr. 459: 460: requested_name(User) -> 461: add_domain(list_to_binary(User)). 462: 463: username(Name, Config) -> 464: case escalus_config:get_config(signed, Config, ca) of 465: self -> 466: Name ++ "-self-signed"; 467: ca -> 468: Name 469: end. 470: 471: replace_addrs(undefined) -> 472: undefined; 473: replace_addrs(Addresses) -> 474: lists:map( fun(Addr) -> [User, Hostname] = binary:split(list_to_binary(Addr), <<"@">>), 475: binary_to_list(<<User/binary, <<"-self-signed@">>/binary, Hostname/binary>>) end, Addresses). 476: 477: -spec add_domain_str(User :: string()) -> string(). 478: add_domain_str(User) -> 479: User ++ "@" ++ binary:bin_to_list(domain()). 480: 481: -spec add_domain(User :: binary()) -> binary(). 482: add_domain(User) -> 483: <<User/binary, "@", (domain())/binary>>.