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 = atom_to_list(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: escalus_connection:stop(Client). 270: 271: ca_signed_cert_is_allowed_with_bosh(C) -> 272: UserSpec = generate_user(C, "bob", escalus_bosh), 273: {ok, Client, _} = escalus_connection:start(UserSpec), 274: escalus_connection:stop(Client). 275: 276: self_signed_cert_fails_to_authenticate_with_tls(C) -> 277: self_signed_cert_fails_to_authenticate(C, escalus_tcp). 278: 279: self_signed_cert_fails_to_authenticate_with_ws(C) -> 280: self_signed_cert_fails_to_authenticate(C, escalus_ws). 281: 282: self_signed_cert_fails_to_authenticate_with_bosh(C) -> 283: self_signed_cert_fails_to_authenticate(C, escalus_bosh). 284: 285: cert_fails_to_authenticate(UserSpec) -> 286: Self = self(), 287: F = fun() -> 288: {ok, Client, _} = escalus_connection:start(UserSpec), 289: Self ! escalus_connected, 290: escalus_connection:stop(Client) 291: end, 292: receive_failed_to_authenticate(F). 293: 294: self_signed_cert_fails_to_authenticate(C, EscalusTransport) -> 295: Self = self(), 296: F = fun() -> 297: UserSpec = generate_user(C, "greg-self-signed", EscalusTransport), 298: {ok, Client, _} = escalus_connection:start(UserSpec), 299: Self ! escalus_connected, 300: escalus_connection:stop(Client) 301: end, 302: receive_failed_to_authenticate(F). 303: 304: receive_failed_to_authenticate(F) -> 305: %% We spawn the process trying to connect because otherwise the testcase may crash 306: %% due linked process crash (client's process are started with start_link) 307: Pid = spawn(F), 308: MRef = erlang:monitor(process, Pid), 309: receive 310: {'DOWN', MRef, process, Pid, _Reason} -> 311: ok; 312: escalus_connected -> 313: ct:fail(authenticated_but_should_not) 314: after 10000 -> 315: ct:fail(timeout_waiting_for_authentication_error) 316: end. 317: 318: self_signed_cert_is_allowed_with_tls(C) -> 319: self_signed_cert_is_allowed_with(escalus_tcp, C). 320: 321: self_signed_cert_is_allowed_with_ws(C) -> 322: self_signed_cert_is_allowed_with(escalus_ws, C). 323: 324: self_signed_cert_is_allowed_with_bosh(C) -> 325: self_signed_cert_is_allowed_with(escalus_bosh, C). 326: 327: self_signed_cert_is_allowed_with(EscalusTransport, C) -> 328: UserSpec = generate_user(C, "bob-self-signed", EscalusTransport), 329: {ok, Client, _} = escalus_connection:start(UserSpec), 330: escalus_connection:stop(Client). 331: 332: no_cert_fails_to_authenticate(_C) -> 333: UserSpec = [{username, <<"no_cert_user">>}, 334: {server, domain()}, 335: {host, <<"localhost">>}, 336: {port, ct:get_config({hosts, mim, c2s_port})}, 337: {password, <<"break_me">>}, 338: {resource, <<>>}, %% Allow the server to generate the resource 339: {auth, {escalus_auth, auth_sasl_external}}, 340: {starttls, required}, 341: {ssl_opts, [{fail_if_no_peer_cert, false}, {verify, verify_none}]}], 342: 343: Result = escalus_connection:start(UserSpec), 344: ?assertMatch({error, {connection_step_failed, _, _}}, Result), 345: {error, {connection_step_failed, _Call, Details}} = Result, 346: ?assertMatch({auth_failed, _, #xmlel{name = <<"failure">>}}, Details), 347: ok. 348: 349: generate_certs(C) -> 350: CA = [#{cn => "not-alice", xmpp_addrs => [add_domain_str("alice"), "alice@fed1"]}, 351: #{cn => "kate", xmpp_addrs => [add_domain_str("kate"), "kate@fed1"]}, 352: #{cn => "bob", xmpp_addrs => [add_domain_str("bob")]}, 353: #{cn => "greg", xmpp_addrs => [add_domain_str("greg")]}, 354: #{cn => "john", xmpp_addrs => undefined}, 355: #{cn => add_domain_str("john"), xmpp_addrs => undefined}, 356: #{cn => "not-mike", xmpp_addrs => undefined}, 357: #{cn => "grace", xmpp_addrs => ["grace@fed1", "grace@reg1"]}, 358: #{cn => add_domain_str("grace"), xmpp_addrs => ["grace@fed1", "grace@reg1"]}], 359: SelfSigned = [ M#{cn => CN ++ "-self-signed", signed => self, xmpp_addrs => replace_addrs(Addrs)} 360: || M = #{ cn := CN , xmpp_addrs := Addrs} <- CA], 361: CertSpecs = CA ++ SelfSigned, 362: TemplateValues = #{"xmppOids" => xmpp_oids()}, 363: Certs = [{maps:get(cn, CertSpec), generate_cert(C, CertSpec, TemplateValues)} 364: || CertSpec <- CertSpecs], 365: [{certs, maps:from_list(Certs)} | C]. 366: 367: generate_cert(C, #{cn := User} = CertSpec, BasicTemplateValues) -> 368: ConfigTemplate = filename:join(?config(mim_data_dir, C), "openssl-user.cnf"), 369: {ok, Template} = file:read_file(ConfigTemplate), 370: XMPPAddrs = maps:get(xmpp_addrs, CertSpec, undefined), 371: TemplateValues = maps:merge(BasicTemplateValues, prepare_template_values(User, XMPPAddrs)), 372: OpenSSLConfig = bbmustache:render(Template, TemplateValues), 373: UserConfig = filename:join(?config(priv_dir, C), User ++ ".cfg"), 374: ct:log("OpenSSL config: ~ts~n~ts", [UserConfig, OpenSSLConfig]), 375: file:write_file(UserConfig, OpenSSLConfig), 376: UserKey = filename:join(?config(priv_dir, C), User ++ "_key.pem"), 377: case maps:get(signed, CertSpec, ca) of 378: ca -> 379: generate_ca_signed_cert(C, User, UserConfig, UserKey); 380: self -> 381: generate_self_signed_cert(C, User, UserConfig, UserKey) 382: end. 383: 384: generate_ca_signed_cert(C, User, UserConfig, UserKey ) -> 385: UserCsr = filename:join(?config(priv_dir, C), User ++ ".csr"), 386: Cmd = ["openssl req -config ", UserConfig, " -newkey rsa:2048 -sha256 -nodes -out ", 387: UserCsr, " -keyout ", UserKey, " -outform PEM"], 388: Out = os:cmd(Cmd), 389: ct:log("generate_ca_signed_cert 1:~nCmd ~p~nOut ~ts", [Cmd, Out]), 390: UserCert = filename:join(?config(priv_dir, C), User ++ "_cert.pem"), 391: SignCmd = filename:join(?config(mim_data_dir, C), "sign_cert.sh"), 392: Cmd2 = [SignCmd, " --req ", UserCsr, " --out ", UserCert], 393: SSLDir = filename:join([path_helper:repo_dir(C), "tools", "ssl"]), 394: OutLog = os:cmd("cd " ++ SSLDir ++ " && " ++ Cmd2), 395: ct:log("generate_ca_signed_cert 2:~nCmd ~p~nOut ~ts", [Cmd2, OutLog]), 396: #{key => UserKey, 397: cert => UserCert}. 398: 399: generate_self_signed_cert(C, User, UserConfig, UserKey) -> 400: UserCert = filename:join(?config(priv_dir, C), User ++ "_self_signed_cert.pem"), 401: Cmd = ["openssl req -config ", UserConfig, " -newkey rsa:2048 -sha256 -nodes -out ", 402: UserCert, " -keyout ", UserKey, " -x509 -outform PEM -extensions client_req_extensions"], 403: OutLog = os:cmd(Cmd), 404: ct:log("generate_self_signed_cert:~nCmd ~p~nOut ~ts", [Cmd, OutLog]), 405: #{key => UserKey, 406: cert => UserCert}. 407: 408: generate_user_tcp(C, User) -> 409: generate_user(C, User, escalus_tcp). 410: 411: generate_user(C, User, Transport) -> 412: Certs = ?config(certs, C), 413: UserCert = maps:get(User, Certs), 414: Common = [{username, list_to_binary(User)}, 415: {server, domain()}, 416: {host, <<"localhost">>}, 417: {password, <<"break_me">>}, 418: {resource, <<>>}, %% Allow the server to generate the resource 419: {auth, {escalus_auth, auth_sasl_external}}, 420: {transport, Transport}, 421: {ssl_opts, [{verify, verify_none}, 422: {versions, ['tlsv1.2']}, 423: {certfile, maps:get(cert, UserCert)}, 424: {keyfile, maps:get(key, UserCert)}]}], 425: Common ++ transport_specific_options(Transport) 426: ++ [{port, ct:get_config({hosts, mim, c2s_port})}]. 427: 428: transport_specific_options(escalus_tcp) -> 429: [{starttls, required}]; 430: transport_specific_options(_) -> 431: [{port, ct:get_config({hosts, mim, cowboy_secure_port})}, 432: {ssl, true}]. 433: 434: prepare_template_values(User, XMPPAddrsIn) -> 435: XMPPAddrs = maybe_prepare_xmpp_addresses(XMPPAddrsIn), 436: #{"cn" => User, "xmppAddrs" => XMPPAddrs}. 437: 438: xmpp_oids() -> 439: case os:cmd("openssl list -objects | grep id-on-xmppAddr") of 440: "id-on-xmppAddr" ++ _ -> ""; % already defined in OpenSSL 3.* 441: _ -> "id-on-xmppAddr = 1.3.6.1.5.5.7.8.5\n" 442: end. 443: 444: maybe_prepare_xmpp_addresses(undefined) -> 445: ""; 446: maybe_prepare_xmpp_addresses(Addrs) when is_list(Addrs) -> 447: AddrsWithSeq = lists:zip(Addrs, lists:seq(1, length(Addrs))), 448: Entries = [make_xmpp_addr_entry(Addr, I) || {Addr, I} <- AddrsWithSeq], 449: string:join(Entries, "\n"). 450: 451: make_xmpp_addr_entry(Addr, I) -> 452: % id-on-xmppAddr OID is specified in the openssl-user.cnf config file 453: "otherName." ++ integer_to_list(I) ++ " = id-on-xmppAddr;UTF8:" ++ Addr. 454: 455: requested_name(User) -> 456: add_domain(list_to_binary(User)). 457: 458: username(Name, Config) -> 459: case escalus_config:get_config(signed, Config, ca) of 460: self -> 461: Name ++ "-self-signed"; 462: ca -> 463: Name 464: end. 465: 466: replace_addrs(undefined) -> 467: undefined; 468: replace_addrs(Addresses) -> 469: lists:map( fun(Addr) -> [User, Hostname] = binary:split(list_to_binary(Addr), <<"@">>), 470: binary_to_list(<<User/binary, <<"-self-signed@">>/binary, Hostname/binary>>) end, Addresses). 471: 472: -spec add_domain_str(User :: string()) -> string(). 473: add_domain_str(User) -> 474: User ++ "@" ++ binary:bin_to_list(domain()). 475: 476: -spec add_domain(User :: binary()) -> binary(). 477: add_domain(User) -> 478: <<User/binary, "@", (domain())/binary>>.