%%% Copyright (c) 2014, NORDUnet A/S. %%% See LICENSE for licensing information. -module(http_auth). -export([verify_auth/4, create_auth/3, init_key_table/0]). -define(KEY_TABLE, http_auth_keys). init_key_table() -> case ets:info(?KEY_TABLE) of undefined -> ok; _ -> ets:delete(?KEY_TABLE) end, ets:new(?KEY_TABLE, [set, public, named_table]), read_key_table(). read_key_table() -> PublickeyDir = application:get_env(plop, publickey_path, none), ServersACL = application:get_env(plop, allowed_servers, []), ClientsACL = application:get_env(plop, allowed_clients, []), Keys = sets:from_list( lists:flatmap(fun ({_, Keys}) -> case Keys of noauth -> []; _ when is_list(Keys) -> Keys end end, ServersACL ++ ClientsACL)), lists:foreach( fun (KeyName) -> Key = sign:read_keyfile_ec(PublickeyDir ++ "/" ++ KeyName ++ ".pem"), true = ets:insert(?KEY_TABLE, {KeyName, Key}) end, sets:to_list(Keys)), case application:get_env(plop, own_key, none) of {_OwnKeyName, OwnKeyFile} -> OwnKey = sign:read_keyfile_ec(OwnKeyFile), true = ets:insert(?KEY_TABLE, {own_key, OwnKey}); none -> none end. own_key() -> case application:get_env(plop, own_key, none) of {KeyName, _KeyFile} -> [{_, Key}] = ets:lookup(?KEY_TABLE, own_key), {Key, KeyName}; none -> none end. lookup_publickey(nokey) -> nokey; lookup_publickey(KeyName) -> case ets:lookup(?KEY_TABLE, KeyName) of [{_, Key}] -> Key; [] -> failure end. parse_option(S) -> parse_option(S, []). parse_option([], Key) -> {lists:reverse(Key), []}; parse_option([$= | Rest], Key) -> {lists:reverse(Key), Rest}; parse_option([C | Rest], Key) -> parse_option(Rest, [C | Key]). sign(PrivKey, Method, Path, Data) -> public_key:sign(iolist_to_binary([Method, 0, Path, 0, Data]), sha256, PrivKey). verify(Signature, PublicKey, Method, Path, Data) -> public_key:verify(iolist_to_binary([Method, 0, Path, 0, Data]), sha256, Signature, PublicKey). check_acl(Method, KeyName, Path) -> EnvVarName = case Method of "REPLY" -> allowed_servers; _ -> allowed_clients end, ACL = application:get_env(plop, EnvVarName, []), lager:debug("ACL: ~p", [ACL]), case lists:keyfind(Path, 1, ACL) of {_, noauth} -> lager:debug("Anonymous access allowed"), success; {_, AllowedKeys} when is_list(AllowedKeys) -> lager:debug("Checking key ~p, allowed keys: ~p", [KeyName, AllowedKeys]), case lists:member(KeyName, AllowedKeys) of true -> success; false -> failure end; false -> lager:debug("No allowed keys found for: ~p", [Path]), failure end. get_authheader_keyname(AuthHeader) -> case string:tokens(AuthHeader, ";") of [AuthTokenBase64 | OptionsRaw] -> AuthToken = base64:decode(AuthTokenBase64), Options = [parse_option(E) || E <- OptionsRaw], case lists:keyfind("key", 1, Options) of {_, Value} -> {Value, AuthToken}; false -> {nokey, <<>>} end; _ -> {nokey, <<>>} end. verify_auth(undefined, Method, Path, _Data) -> case check_acl(Method, noauth, Path) of success -> noauth; Error -> case Method of "REPLY" -> lager:info("anonymous replies not allowed for path ~p", [Path]); _ -> lager:info("anonymous access not allowed for path ~p", [Path]) end, Error end; verify_auth(AuthHeader, Method, Path, Data) -> {KeyName, AuthToken} = get_authheader_keyname(AuthHeader), AuthSuccess = case lookup_publickey(KeyName) of nokey -> false; failure -> lager:info("key name ~p could not be found", [KeyName]), false; Key -> verify(AuthToken, Key, Method, Path, Data) end, case AuthSuccess of true -> check_acl(Method, KeyName, Path); _ -> lager:info("authentication token ~p was not valid for key name ~p: ~p ~p ~p", [mochihex:to_hex(AuthToken), KeyName, Method, Path, Data]), failure end. create_auth(Method, Path, Data) -> case own_key() of {Key, KeyName} -> AuthToken = sign(Key, Method, Path, Data), base64:encode_to_string(AuthToken) ++ ";key=" ++ KeyName; none -> "" end.