From 662ea802f40062d6f095bdeea61e69d7b665de25 Mon Sep 17 00:00:00 2001 From: Magnus Ahltorp Date: Thu, 26 Feb 2015 16:50:41 +0100 Subject: Added authentication --- src/http_auth.erl | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/plop.erl | 44 ++++++++++++++--- src/plop_app.erl | 1 + 3 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/http_auth.erl diff --git a/src/http_auth.erl b/src/http_auth.erl new file mode 100644 index 0000000..6a076fa --- /dev/null +++ b/src/http_auth.erl @@ -0,0 +1,138 @@ +%%% 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)), + {_OwnKeyName, OwnKeyFile} = application:get_env(plop, own_key, none), + OwnKey = sign:read_keyfile_ec(OwnKeyFile), + true = ets:insert(?KEY_TABLE, {own_key, OwnKey}). + + +own_key() -> + {KeyName, _KeyFile} = application:get_env(plop, own_key, none), + [{_, Key}] = ets:lookup(?KEY_TABLE, own_key), + {Key, KeyName}. + +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. + +verify_auth(undefined, Method, Path, _Data) -> + case check_acl(Method, noauth, Path) of + success -> + noauth; + Error -> + lager:info("anonymous access not allowed for path ~p", [Path]), + Error + end; +verify_auth(AuthHeader, Method, Path, Data) -> + [AuthTokenBase64 | OptionsRaw] = string:tokens(AuthHeader, ";"), + AuthToken = base64:decode(AuthTokenBase64), + Options = [parse_option(E) || E <- OptionsRaw], + KeyName = case lists:keyfind("key", 1, Options) of + {_, Value} -> + Value; + false -> + nokey + end, + 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) -> + {Key, KeyName} = own_key(), + AuthToken = sign(Key, Method, Path, Data), + base64:encode_to_string(AuthToken) ++ ";key=" ++ KeyName. diff --git a/src/plop.erl b/src/plop.erl index f90d287..cfca343 100644 --- a/src/plop.erl +++ b/src/plop.erl @@ -194,6 +194,16 @@ storage_nodes_quorum() -> {ok, Value} = application:get_env(plop, storage_nodes_quorum), Value. +add_auth(Method, Path, Headers, Data) -> + AuthHeader = http_auth:create_auth(Method, Path, Data), + lager:debug("sent auth header: ~p", [AuthHeader]), + [{"X-Catlfish-Auth", AuthHeader} | Headers]. + +get_auth_header(Headers) -> + Result = binary_to_list(hackney_headers:get_value("X-Catlfish-Auth", Headers)), + lager:debug("received auth header: ~p", [Result]), + Result. + send_http_request(TreeLeafHash, URL, Headers, RequestBody) -> ParentPid = self(), RequestId = make_ref(), @@ -202,17 +212,39 @@ send_http_request(TreeLeafHash, URL, Headers, RequestBody) -> Starttime = os:timestamp(), ParsedURL = hackney_url:parse_url(URL), #hackney_url{path = Path} = ParsedURL, - lager:debug("leafhash ~s: sending http request to ~p", [mochihex:to_hex(TreeLeafHash), URL]), + lager:debug("leafhash ~s: sending http request to ~p", + [mochihex:to_hex(TreeLeafHash), URL]), {ok, ConnRef} = hackney:connect(ParsedURL, [{ssl_options, [{cacertfile, CACertFile}]}]), - lager:debug("leafhash ~s: connected to ~p", [mochihex:to_hex(TreeLeafHash), URL]), - {ok, StatusCode, RespHeaders, ClientRef} = hackney:send_request(ConnRef, {post, Path, Headers, RequestBody}), - lager:debug("leafhash ~s: received headers for ~p", [mochihex:to_hex(TreeLeafHash), URL]), + lager:debug("leafhash ~s: connected to ~p", + [mochihex:to_hex(TreeLeafHash), URL]), + {ok, StatusCode, RespHeaders, ClientRef} = + hackney:send_request(ConnRef, + {post, Path, + add_auth("POST", Path, Headers, + RequestBody), + RequestBody}), + lager:debug("leafhash ~s: received headers for ~p: ~p", + [mochihex:to_hex(TreeLeafHash), URL, RespHeaders]), {ok, Body} = hackney:body(ClientRef), Stoptime = os:timestamp(), hackney:close(ClientRef), - lager:debug("leafhash ~s: received body for ~p: time ~p", [mochihex:to_hex(TreeLeafHash), URL, timer:now_diff(Stoptime, Starttime)]), + lager:debug("leafhash ~s: received body for ~p: time ~p", + [mochihex:to_hex(TreeLeafHash), URL, timer:now_diff(Stoptime, Starttime)]), StatusLine = {none, StatusCode, none}, - ParentPid ! {http, {RequestId, {StatusLine, RespHeaders, Body}}} + AuthHeader = get_auth_header(hackney_headers:new(RespHeaders)), + case http_auth:verify_auth(AuthHeader, "REPLY", + binary_to_list(Path), Body) of + failure -> + lager:debug("auth check failed"), + drop; + success -> + lager:debug("auth check succeeded"), + ParentPid ! {http, {RequestId, + {StatusLine, RespHeaders, Body}}}; + noauth -> + lager:debug("no auth"), + drop + end end), RequestId. diff --git a/src/plop_app.erl b/src/plop_app.erl index 9cb5558..5aae9d9 100644 --- a/src/plop_app.erl +++ b/src/plop_app.erl @@ -7,6 +7,7 @@ start(normal, Args) -> hackney:start(), + http_auth:init_key_table(), plop_sup:start_link(Args). stop(_State) -> -- cgit v1.1