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), ca_certificate_helper:generate_cert(C, CertSpec, TemplateValues)}
  364:              || CertSpec <- CertSpecs],
  365:     [{certs, maps:from_list(Certs)} | C].
  366: 
  367: generate_user_tcp(C, User) ->
  368:     generate_user(C, User, escalus_tcp).
  369: 
  370: generate_user(C, User, Transport) ->
  371:     Certs = ?config(certs, C),
  372:     UserCert = maps:get(User, Certs),
  373:     Common = [{username, list_to_binary(User)},
  374:               {server, domain()},
  375:               {host, <<"localhost">>},
  376:               {password, <<"break_me">>},
  377:               {resource, <<>>}, %% Allow the server to generate the resource
  378:               {auth, {escalus_auth, auth_sasl_external}},
  379:               {transport, Transport},
  380:               {ssl_opts, [{verify, verify_none},
  381:                           {versions, ['tlsv1.2']},
  382:                           {certfile, maps:get(cert, UserCert)},
  383:                           {keyfile, maps:get(key, UserCert)}]}],
  384:     Common ++ transport_specific_options(Transport)
  385:     ++ [{port, ct:get_config({hosts, mim, c2s_port})}].
  386: 
  387: transport_specific_options(escalus_tcp) ->
  388:     [{starttls, required}];
  389: transport_specific_options(_) ->
  390:      [{port, ct:get_config({hosts, mim, cowboy_secure_port})},
  391:       {ssl, true}].
  392: 
  393: xmpp_oids() ->
  394:     case os:cmd("openssl list -objects | grep id-on-xmppAddr") of
  395:         "id-on-xmppAddr" ++ _ -> ""; % already defined in OpenSSL 3.*
  396:         _ -> "id-on-xmppAddr = 1.3.6.1.5.5.7.8.5\n"
  397:     end.
  398: 
  399: requested_name(User) ->
  400:     add_domain(list_to_binary(User)).
  401: 
  402: username(Name, Config) ->
  403:     case escalus_config:get_config(signed, Config, ca) of
  404:         self ->
  405:             Name ++ "-self-signed";
  406:         ca ->
  407:             Name
  408:     end.
  409: 
  410: replace_addrs(undefined) ->
  411:     undefined;
  412: replace_addrs(Addresses) ->
  413:     lists:map( fun(Addr) -> [User, Hostname] = binary:split(list_to_binary(Addr), <<"@">>),
  414:                             binary_to_list(<<User/binary, <<"-self-signed@">>/binary, Hostname/binary>>) end, Addresses).
  415: 
  416: -spec add_domain_str(User :: string()) -> string().
  417: add_domain_str(User) ->
  418:     User ++ "@" ++ binary:bin_to_list(domain()).
  419: 
  420: -spec add_domain(User :: binary()) -> binary().
  421: add_domain(User) ->
  422:     <<User/binary, "@", (domain())/binary>>.