summaryrefslogtreecommitdiff
path: root/tools/merge_sth.py
blob: cf1e9946be8d2f683e6b50d8e528f6f70df6c2b6 (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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015, NORDUnet A/S.
# See LICENSE for licensing information.

import sys
import json
import urllib2
import time
import requests
from base64 import b64encode
from mergetools import parse_args, get_nfetched, hexencode, hexdecode, \
     get_logorder, get_sth
from certtools import create_ssl_context, get_public_key_from_file, \
     timing_point, create_sth_signature, write_file, check_sth_signature, \
     build_merkle_tree

def merge_sth(args, config, localconfig):
    paths = localconfig["paths"]
    own_key = (localconfig["nodename"],
               "%s/%s-private.pem" % (paths["privatekeys"],
                                      localconfig["nodename"]))
    ctbaseurl = config["baseurl"]
    signingnodes = config["signingnodes"]
    mergenodes = config.get("mergenodes", [])
    mergedb = paths["mergedb"]
    sthfile = mergedb + "/sth"
    logorderfile = mergedb + "/logorder"
    currentsizefile = mergedb + "/fetched"
    logpublickey = get_public_key_from_file(paths["logpublickey"])
    backupquorum = config.get("backup-quorum-size", 0)
    assert backupquorum <= len(mergenodes) - 1
    create_ssl_context(cafile=paths["https_cacertfile"])
    timing = timing_point()

    trees = [{'tree_size': get_nfetched(currentsizefile, logorderfile),
              'sha256_root_hash': ''}]
    for mergenode in mergenodes:
        if mergenode["name"] == config["primarymergenode"]:
            continue
        verifiedfile = mergedb + "/verified." + mergenode["name"]
        try:
            tree = json.loads(open(verifiedfile, "r").read())
        except (IOError, ValueError):
            tree = {'tree_size': 0, "sha256_root_hash": ''}
        trees.append(tree)
    trees.sort(key=lambda e: e['tree_size'], reverse=True)
    print >>sys.stderr, "DEBUG: trees:", trees

    if backupquorum > len(trees) - 1:
        print >>sys.stderr, "backup quorum > number of secondaries:", \
          backupquorum, ">", len(trees) - 1
        return
    tree_size = trees[backupquorum]['tree_size']
    root_hash = hexdecode(trees[backupquorum]['sha256_root_hash'])
    print >>sys.stderr, "DEBUG: tree size candidate at backupquorum", \
      backupquorum, ":", tree_size

    cur_sth = get_sth(sthfile)
    if tree_size < cur_sth['tree_size']:
        print >>sys.stderr, "candidate tree < current tree:", \
          tree_size, "<", cur_sth['tree_size']
        return

    assert tree_size >= 0         # Don't read logorder without limit.
    logorder = get_logorder(logorderfile, tree_size)
    timing_point(timing, "get logorder")
    if tree_size == -1:
        tree_size = len(logorder)
    print >>sys.stderr, "new tree size will be", tree_size

    root_hash_calc = build_merkle_tree(logorder)[-1][0]
    assert root_hash == '' or root_hash == root_hash_calc
    root_hash = root_hash_calc
    timestamp = int(time.time() * 1000)

    tree_head_signature = None
    for signingnode in signingnodes:
        try:
            tree_head_signature = \
              create_sth_signature(tree_size, timestamp,
                                   root_hash,
                                   "https://%s/" % signingnode["address"],
                                   key=own_key)
            break
        except requests.exceptions.HTTPError, e:
            print >>sys.stderr, e.response
            sys.stderr.flush()
    if tree_head_signature == None:
        print >>sys.stderr, "Could not contact any signing nodes"
        sys.exit(1)

    sth = {"tree_size": tree_size, "timestamp": timestamp,
           "sha256_root_hash": b64encode(root_hash),
           "tree_head_signature": b64encode(tree_head_signature)}

    check_sth_signature(ctbaseurl, sth, publickey=logpublickey)
    timing_point(timing, "build sth")

    print hexencode(root_hash), timestamp, tree_size
    sys.stdout.flush()

    write_file(sthfile, sth)

    if args.timing:
        print >>sys.stderr, "timing: merge_sth:", timing["deltatimes"]
        sys.stderr.flush()

def main():
    """
    Read file 'sth' to get current tree size, assuming zero if file not
    found.

    Read tree sizes from the backup.<secondary> files, put them in a
    list and sort it. Let new tree size equal list[backup-quorum]. Barf
    on a new tree size smaller than the currently published tree size.

    Decide on a timestamp, build an STH and write it to file 'sth'.
    """
    args, config, localconfig = parse_args()

    while True:
        merge_sth(args, config, localconfig)
        if args.interval is None:
            break
        print >>sys.stderr, "sleeping", args.interval, "seconds"
        time.sleep(args.interval)

if __name__ == '__main__':
    sys.exit(main())