Refresh whitelist with delta between old & new directory contents

Store the filename along with certificate issuer name and serial
number, so as to perform a diff on the directory contents, then only
install and remove those entries which need it. We were deleting all
entries + reading in the entire (newer) contents of the directory when
directory modification time had changed.

Along the way it made more sense to optimise ETS for querying the
whitelist than it did to refresh it: the key is still the
unique/distinctive certificate value (issuer name & serial
number). While installing and removing certificates rely on a
`select`.

The client facing interface, `whitelisted/3`, ultimately makes a call
to the ETS table directly. That is, it no longer goes through the
`trust_store` process, which was unecessary.
This commit is contained in:
Joseph Yiasemides 2016-02-12 11:20:06 +01:00
parent a92f993bb3
commit 0ecda46632
1 changed files with 42 additions and 23 deletions

View File

@ -24,6 +24,7 @@
code_change/3]).
-include_lib("kernel/include/file.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-include_lib("public_key/include/public_key.hrl").
-type certificate() :: #'OTPCertificate'{}.
-type event() :: valid_peer
@ -37,7 +38,7 @@
| {fail, Reason :: term()}
| {unknown, state()}.
-record(entry, {identifier :: tuple()}).
-record(entry, {filename :: string(), identifier :: tuple()}).
-record(state, {directory_change_time :: integer()}).
@ -89,8 +90,8 @@ whitelisted(#'OTPCertificate'{}=C, valid_peer, continue) ->
whitelisted(_, {extension, _}, St) ->
{unknown, St}.
whitelisted_(#entry{}=E) ->
gen_server:call(trust_store, {whitelisted, E#entry.identifier}, timeout()).
whitelisted_(#entry{identifier = Id}) ->
ets:member(table_name(), Id).
%% Generic Server Callbacks
@ -105,19 +106,19 @@ init(Settings) ->
erlang:send_after(Expiry, erlang:self(), {refresh, Path, Expiry}),
{ok, #state{directory_change_time = Initial}}.
handle_call({whitelisted, E}, _Sender, St) ->
{reply, ets:member(table_name(), E), St}.
handle_call(_, _, St) ->
{noreply, St}.
handle_cast(_, St) ->
{noreply, St}.
handle_info({refresh, Path, Expiry}=Notification, #state{directory_change_time=Old}=St) ->
handle_info({refresh, Path, Expiry}=Notification,
#state{directory_change_time=Old}=St) ->
New = modification_time(Path),
case New > Old of
false ->
ok;
true ->
true = ets:delete_all_objects(table_name()),
tabulate(Path)
end,
erlang:send_after(Expiry, erlang:self(), Notification),
@ -132,9 +133,6 @@ code_change(_,_,_) ->
%% Ancillary & Constants
timeout() ->
timer:seconds(5).
expiry(Pairs) ->
{expiry, Time} = lists:keyfind(expiry, 1, Pairs),
Time.
@ -147,27 +145,48 @@ table_name() ->
trust_store_whitelist.
table_options() ->
[private, named_table, set, {keypos, #entry.identifier}, {heir, none}].
[protected,
named_table,
set,
{keypos, #entry.identifier},
{heir, none}].
modification_time(Path) ->
{ok, Info} = file:read_file_info(Path, [{time, posix}]),
Info#file_info.mtime.
tabulate(Path) ->
{ok, Filenames} = file:list_dir(Path),
Absolutes = lists:map(fun (Filename) ->
filename:join([Path, Filename])
end, Filenames),
Certificates = lists:map(fun scan_then_parse/1, Absolutes),
Es = lists:map(fun extract_unique_attributes/1, Certificates),
ok = insert(Es).
already_whitelisted_filenames() ->
ets:select(table_name(),
ets:fun2ms(fun (#entry{filename = N}) -> N end)).
insert([]) ->
ok;
insert(Es) when is_list(Es) ->
true = ets:insert_new(table_name(), Es),
one_whitelisted_filename(Name) ->
ets:fun2ms(fun (#entry{filename = Name}) -> true end).
build_entry(Path, Name) ->
Absolute = filename:join(Path, Name),
Certificate = scan_then_parse(Absolute),
Unique = extract_unique_attributes(Certificate),
_Entry = Unique#entry{filename = Name}.
do_additions(Before, After, Path) ->
[ insert(build_entry(Path, Name)) || Name <- After -- Before ].
do_removals(Before, After) ->
[ delete(Name) || Name <- Before -- After ].
tabulate(Path) ->
Old = already_whitelisted_filenames(),
{ok, New} = file:list_dir(Path),
do_additions(Old, New, Path),
do_removals(Old, New),
ok.
delete(Name) ->
true = ets:select_delete(table_name(), one_whitelisted_filename(Name)).
insert(Entry) ->
true = ets:insert_new(table_name(), Entry).
scan_then_parse(Filename) when is_list(Filename) ->
{ok, Bin} = file:read_file(Filename),
[{'Certificate', Data, not_encrypted}] = public_key:pem_decode(Bin),