summaryrefslogtreecommitdiff
path: root/merge/src/merge_sth.erl
blob: 4b77864407af9f1d06f4235d3d2055ae466dcdeb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
%%% Copyright (c) 2017, NORDUnet A/S.
%%% See LICENSE for licensing information.

-module(merge_sth).
-behaviour(gen_server).

-export([start_link/1]).
-export([init/1, handle_call/3, terminate/2, handle_cast/2, handle_info/2,
         code_change/3]).

-include("plop.hrl").

-record(state, {
          timer :: reference()
         }).

start_link(Args) ->
    gen_server:start_link(?MODULE, Args, []).

init([]) ->
    lager:info("~p: starting", [?MODULE]),
    {ok, #state{timer = erlang:start_timer(1000, self(), make_sth)}}.

handle_call(stop, _From, State) ->
    {stop, normal, stopped, State}.
handle_cast(_Request, State) ->
    {noreply, State}.

handle_info({timeout, _Timer, make_sth}, State) ->
    make_sth(read_sth_treesize(), State).

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

terminate(Reason, #state{timer = Timer}) ->
    lager:info("~p terminating: ~p", [?MODULE, Reason]),
    erlang:cancel_timer(Timer),
    ok.

%%%%%%%%%%%%%%%%%%%%

make_sth(noentry, State) ->
    case merge_util:readfile(minsize_path) of
        noentry ->
            lager:error("This log is invalid, missing both sth and minsize."),
            exit({shutdown, invalid_log});
        {struct, PropList} ->
            MinSize = proplists:get_value(<<"size">>, PropList),
            make_sth(MinSize, State)
    end;
make_sth(CurSize, State) ->
    {MergeSecondaryNames, _MergeSecondaryAddrs} =
        lists:unzip(plopconfig:get_env(merge_secondaries, [])),
    lager:info("Current STH at ~B with ~B secondary merge nodes.", 
               [CurSize, length(MergeSecondaryNames)]),

    %% Collect tree sizes from verified.* files in a list, add an
    %% entry with the size found in the 'fetched' file, sort the list
    %% (highest tree size first) and index it with backup quorum to
    %% get our new tree size.
    Sizes = [merge_util:nfetched() | verified_sizes(MergeSecondaryNames)],
    BackupQuorumSize = plopconfig:get_env(backup_quorum, 0),
    true = BackupQuorumSize =< length(MergeSecondaryNames),
    NewSize = lists:nth(BackupQuorumSize + 1, Sizes),
    lager:debug("new size at backup quorum ~B: ~B", [BackupQuorumSize, NewSize]),
    Wait = 
        case NewSize < CurSize of
            true ->
                statusreport:report("merge_sth", http_auth:own_name(), "sth", null),
                lager:debug("waiting for enough backups to reach ~B, now at ~B",
                            [CurSize, NewSize]),
                1;
            false ->
                ok = do_make_sth(NewSize),
                application:get_env(plop, merge_delay, 600)
        end,
    Timer = erlang:start_timer(Wait * 1000, self(), make_sth),
    {noreply, State#state{timer = Timer}}.

do_make_sth(Size) -> 
    %% Build a new sth file in memory, get a signature for it and
    %% verify both the new sth file against the signature and the new
    %% root against ht before writing to disk.
    NewTimestamp = plop:generate_timestamp(),
    NewRoot = root(Size),
    PackedSignature = make_signature(NewTimestamp, Size, NewRoot),
    ok = case plop:verify_sth(Size, NewTimestamp, NewRoot, PackedSignature) of
             true ->
                 NewSTH = [{"tree_size", Size},
                           {"timestamp", NewTimestamp},
                           {"sha256_root_hash", base64:encode(NewRoot)},
                           {"tree_head_signature", base64:encode(PackedSignature)}],
                 ok = plop:save_sth({struct, NewSTH}),
                 statusreport:report("merge_sth", http_auth:own_name(), "sth", Size),
                 ok;
             false ->
                 lager:error("The signature we got for new tree of size ~B doesn't " ++
                                 "verify corectly; timestamp=~p; tree head: ~p",
                             [Size, NewTimestamp, NewRoot]),
                 sig_mismatch
         end.

make_signature(Timestamp, Size, Roothash) ->
    SigType = plop:signature_type(tree_hash),
    Sig = sign:sign_sth(
            <<0:8,                         % CT protocol version (v1).
              SigType:8,
              Timestamp:64,
              Size:64,
              Roothash/binary>>),
    plop:serialise(#signature{
                      algorithm = #sig_and_hash_alg{
                                     hash_alg = sha256,
                                     signature_alg = ecdsa},
                      signature = Sig}).

verified_sizes(MergeSecondaryNodeNames) ->
    {ok, BasePath} = application:get_env(plop, verified_path),
    L = lists:map(fun(NodeName) ->
                          verified_size(BasePath ++ "." ++ NodeName)
                  end, MergeSecondaryNodeNames),
    lists:reverse(lists:sort(L)).

verified_size(Path) ->
    case atomic:readfile(Path) of
        noentry ->
            0;
        Contents ->
            case mochijson2:decode(Contents) of
                {struct, PropList} ->
                    Size = proplists:get_value(<<"tree_size">>, PropList),
                    Hash = hex:hexstr_to_bin(binary_to_list(proplists:get_value(<<"sha256_root_hash">>, PropList))),
                    ok = validate_tree_head(Size, Hash),
                    Size
            end
    end.
    
validate_tree_head(Treesize, Roothash) ->
    ok = case root(Treesize) of
             Roothash ->
                 ok;
             RoothashInTree ->
                 lager:error("Root hash doesn't match tree head version ~B: ~p != ~p", [Treesize - 1, Roothash, RoothashInTree]),
                 root_mismatch
         end.

read_sth_treesize() ->
    case plop:sth() of
        noentry ->
            noentry;
        {struct, STH} ->
            Treesize = proplists:get_value(<<"tree_size">>, STH),
            Timestamp = proplists:get_value(<<"timestamp">>, STH),
            RootHash = base64:decode(proplists:get_value(<<"sha256_root_hash">>, STH)),
            Signature = base64:decode(proplists:get_value(<<"tree_head_signature">>, STH)),
            true = plop:verify_sth(Treesize, Timestamp, RootHash, Signature),
            Treesize
    end.

root(TreeSize) ->
    ht:load_tree(TreeSize - 1),
    ht:root(TreeSize - 1).