%% @doc %% Wrapper for cyrus sasl library for GSSAPI mechanism support in erlang applications. Each function (except kinit) corresponds to %% functions at libsasl2 %% @end -module(sasl_auth). %% API -export([ init/0, kinit/2, kinit/3, client_new/3, client_new/4, client_listmech/1, client_start/1, client_step/2, client_done/1, server_new/2, server_new/3, server_start/2, server_step/2, server_done/1, krb5_kt_default_name/0 ]). -on_load(init/0). -define(SASL_CODES, #{ 0 => sasl_ok, 1 => sasl_continue, 2 => sasl_interact, -1 => sasl_fail, -2 => sasl_nomem, -3 => sasl_bufover, -4 => sass_nomech, -5 => sasl_badprot, -6 => sasl_notdone, -7 => sasl_badparam, -8 => sasl_tryagain, -9 => sasl_badmac, -10 => sasl_badserv, -11 => sasl_wrongmech, -12 => sasl_notinit, -13 => sasl_badauth, -14 => sasl_noauthz, -15 => sasl_tooweak, -16 => sasl_encrypt, -17 => sasl_trans, -18 => sasl_expired, -19 => sasl_disabled, -20 => sasl_nouser, -21 => sasl_pwlock, -22 => sasl_nochange, -23 => sasl_badvers, -24 => sasl_unavail, -26 => sasl_noverify, -27 => sasl_weakpass, -28 => sasl_nouserpass, -29 => sasl_need_old_passwd, -30 => sasl_constraint_violat, -32 => sasl_badbinding, -100 => sasl_configerr }). -type state() :: reference(). -type keytab_path() :: file:filename_all(). -type principal() :: string() | binary(). -type ccname() :: string() | binary(). -type service_name() :: string() | binary(). -type host() :: string() | binary(). -type user() :: string() | binary(). -type available_mechs() :: [binary()]. -type sasl_code() :: sasl_badserv | sasl_noverify | sasl_weakpass | sasl_badauth | sasl_tooweak | sasl_nomem | sasl_need_old_passwd | sasl_badprot | sasl_expired | sasl_nouser | sasl_badbinding | sasl_badvers | sasl_configerr | sasl_noauthz | sasl_ok | sasl_nouserpass | sasl_notdone | sasl_notinit | sasl_trans | sasl_interact | sasl_bufover | sasl_pwlock | sasl_constraint_violat | sasl_continue | sasl_fail | sasl_encrypt | sasl_badparam | sasl_badmac | sasl_tryagain | sasl_unavail | sasl_wrongmech | sasl_nochange | sasl_disabled | sass_nomech | unknown. -spec init() -> ok | {error, {load_failed | bad_lib | load | reload | upgrade | old_code, Text :: string()}}. init() -> NifLib = case code:priv_dir(sasl_auth) of {error, bad_name} -> case code:which(?MODULE) of Filename when is_list(Filename) -> filename:join([filename:dirname(Filename), "../priv", "sasl_auth"]); _ -> filename:join("../priv", "sasl_auth") end; Dir -> filename:join(Dir, "sasl_auth") end, RetVal = erlang:load_nif(NifLib, 0), ErrorMsg = case RetVal of {error, _Reason} = LoadError -> Msg = io_lib:format( "Loading of sasl_auth's shared library failed.\n" "The reason for this is probably missing dependencies or that a\n" "dependency has a different version than the ones sasl_auth was compiled with.\n" "Please see https://github.com/kafka4beam/sasl_auth for information\n" "about sasl_auth's dependencies." "SASL/GSSAPI (Kerberos) authentication will probably not work.\n" "\n" "Return Value for erlang:load_nif(~s, 0):\n" "~p", [NifLib, LoadError] ), persistent_term:put( sasl_auth_shared_lib_load_error_msg, {on_load_error_info, LoadError, Msg} ), Msg; _ -> ok end, case application:get_env(sasl_auth, fail_on_load_if_load_unsuccessful, false) of _ when RetVal =:= ok -> ok; true -> logger:error(ErrorMsg), RetVal; false -> %% We will return ok but log a warning message with logger logger:warning(ErrorMsg), ok end. %% @doc Initialize credentials from a keytab file and principal. %% It makes use of the per Erlang node static MEMORY type ccache. -spec kinit(keytab_path(), principal()) -> ok | {error, {binary(), integer(), binary()}}. kinit(KeyTabPath, Principal) -> kinit(KeyTabPath, Principal, <<>>). %% @hidden Initialize credentials from a keytab file and principal. %% The argument CCname is provided for application's flexibility to decide %% which credentials cache type or name to use. %% When set to empty string, the default cache name `MEMORY:krb5cc_sasl_auth' %% is used. %% e.g. `FILE:/tmp/krb5cc_mycache' or `MEMORY:krbcc5_mycache' %% %% CAUTION: Changing credentials cache name at runtime is not tested! %% CAUTION: There is currently a lack of call to krb5_cc_destroy, creating %% many caches at runtime may lead to memory leak. -spec kinit(keytab_path(), principal(), ccname()) -> ok | {error, {binary(), integer(), binary()}}. kinit(KeyTabPath, Principal, Ccname) -> sasl_kinit( null_terminate(KeyTabPath), null_terminate(Principal), null_terminate(Ccname) ). %% @doc Initialize a client context. User client's principal as client's username. %% This is the default behaviour before version 2.1.1, however may not work when %% principal has realm i.e. not the default realm. %% e.g. using the full principal like `user/foo.bar@EXAMPLE.COM' may get result in this %% error on the server side: %% "SASL(-13): authentication failure: Requested identity not authenticated identity" %% This is because the client claims to be `user/foo.bar@EXAMPLE.COM' but server %% may consider it different from `user/foo.bar' obtained from KDC. %% Call `client_new/4' instead! -spec client_new(ServiceName :: service_name(), ServerFQDN :: host(), Principal :: principal()) -> {ok, state()} | {error, sasl_code()}. client_new(ServiceName, ServerFQDN, Principal) -> client_new(ServiceName, ServerFQDN, Principal, undefined). %% @doc Initialize a client authentication context. %% NOTE: When `User' is `undefined', client principal name is used as username. -spec client_new( ServiceName :: service_name(), ServerFQDN :: host(), Principal :: principal(), User :: undefined | user() ) -> {ok, state()} | {error, sasl_code()}. client_new(ServiceName, ServerFQDN, Principal, User) -> ServiceName0 = null_terminate(ServiceName), Host0 = null_terminate(ServerFQDN), Principal0 = null_terminate(Principal), User0 = case User =:= undefined of true -> binary:copy(Principal0); _ -> null_terminate(User) end, case sasl_client_new(ServiceName0, Host0, Principal0, User0) of {ok, _} = Ret -> Ret; {error, Code} -> {error, code_to_atom(Code)} end. -spec client_listmech(State :: state()) -> {ok, available_mechs()} | {error, {sasl_code(), binary()}}. client_listmech(State) -> case sasl_listmech(State) of {ok, Mechs} -> {ok, binary:split(Mechs, <<" ">>, [global])}; {error, {Code, Detail}} -> {error, {code_to_atom(Code), strip_null_terminate(Detail)}} end. -spec client_start(State :: state()) -> {ok, {sasl_code(), binary()}} | {error, {sasl_code(), binary()}}. client_start(State) -> case sasl_client_start(State) of {ok, {Code, Token}} -> {ok, {code_to_atom(Code), Token}}; {error, {Code, Detail}} -> {error, {code_to_atom(Code), strip_null_terminate(Detail)}} end. -spec client_step(state(), binary()) -> {ok, {sasl_code(), binary()}} | {error, {sasl_code(), binary()}}. client_step(State, Token) -> case sasl_client_step(State, Token) of {ok, {Code, MaybeNewToken}} -> {ok, {code_to_atom(Code), MaybeNewToken}}; {error, {Code, Detail}} -> {error, {code_to_atom(Code), strip_null_terminate(Detail)}} end. -spec client_done(state()) -> ok. client_done(State) -> sasl_client_done(State). %% @doc Initialize server side authentication context. %% NOTE: This depends on the server `gethostname()' to be resolved exactly the %% same as the FQDN the clients intend to connect. -spec server_new(ServiceName :: service_name(), Principal :: principal()) -> {ok, state()} | {error, sasl_code()}. server_new(ServiceName, Principal) -> server_new(ServiceName, Principal, <<>>). %% @doc Initialize server side authentication context. %% ServerFQDN is useful when serer is multi-home. e.g. behind a load-balancer. -spec server_new(ServiceName :: service_name(), Principal :: principal(), ServerFQDN :: host()) -> {ok, state()} | {error, sasl_code()}. server_new(ServiceName, Principal, ServerFQDN) -> ServiceName0 = null_terminate(ServiceName), Principal0 = null_terminate(Principal), ServerFQDN0 = null_terminate(ServerFQDN), case sasl_server_new(ServiceName0, ServerFQDN0, Principal0) of {ok, _} = Ret -> Ret; {error, Code} -> {error, code_to_atom(Code)} end. -spec server_start(State :: state(), binary()) -> {ok, {sasl_code(), binary()}} | {error, {sasl_code(), binary()}}. server_start(State, ClientIn) -> case sasl_server_start(State, ClientIn) of {ok, {Code, Token}} -> {ok, {code_to_atom(Code), Token}}; {error, {Code, Detail}} -> {error, {code_to_atom(Code), strip_null_terminate(Detail)}} end. -spec server_step(state(), binary()) -> {ok, {sasl_code(), binary()}} | {error, {sasl_code(), binary()}}. server_step(State, Token) -> case sasl_server_step(State, Token) of {ok, {Code, MaybeNewToken}} -> {ok, {code_to_atom(Code), MaybeNewToken}}; {error, {Code, Detail}} -> {error, {code_to_atom(Code), strip_null_terminate(Detail)}} end. -spec server_done(state()) -> ok. server_done(State) -> sasl_server_done(State). code_to_atom(Code) -> maps:get(Code, ?SASL_CODES, unknown). null_terminate(Bin) -> iolist_to_binary([Bin, 0]). strip_null_terminate(Bin) -> case binary:split(Bin, <<0>>) of [X, _] -> X; [X] -> X end. krb5_kt_default_name() -> sasl_krb5_kt_default_name(). sasl_krb5_kt_default_name() -> not_loaded(?LINE). sasl_kinit(_KeyTab, _Principal, _CacheName) -> not_loaded(?LINE). sasl_client_new(_Service, _Host, _Principal, _User) -> not_loaded(?LINE). sasl_listmech(_State) -> not_loaded(?LINE). sasl_client_start(_State) -> not_loaded(?LINE). sasl_client_step(_State, _Token) -> not_loaded(?LINE). sasl_client_done(_State) -> not_loaded(?LINE). sasl_server_new(_Service, _ServerFQDN, _Principal) -> not_loaded(?LINE). sasl_server_start(_State, _Token) -> not_loaded(?LINE). sasl_server_step(_State, _Token) -> not_loaded(?LINE). sasl_server_done(_State) -> not_loaded(?LINE). not_loaded(Line) -> erlang:nif_error( {not_loaded, [ {module, ?MODULE}, {line, Line}, persistent_term:get( sasl_auth_shared_lib_load_error_msg, no_load_error_saved ) ]} ).