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

import sys
import argparse
import json
import errno
import shutil
import base64
from datetime import datetime, timedelta, tzinfo
import time
from certtools import get_sth, create_ssl_context, check_sth_signature, get_public_key_from_file, get_consistency_proof, verify_consistency_proof

NAGIOS_OK = 0
NAGIOS_WARN = 1
NAGIOS_CRIT = 2
NAGIOS_UNKNOWN = 3

DEFAULT_CUR_FILE = 'cur-sth.json'

parser = argparse.ArgumentParser(description="")
parser.add_argument('--cur-sth',
                    metavar='file',
                    default=DEFAULT_CUR_FILE,
                    help="File containing current STH (default=%s)" % DEFAULT_CUR_FILE)
parser.add_argument('baseurl', help="Base URL for CT log")
parser.add_argument('--publickey', default=None, metavar='file', help='Public key for the CT log')
parser.add_argument('--cafile', default=None, metavar='file', help='File containing the CA cert')
parser.add_argument('--allow-lag', action='store_true', help='Allow node to lag behind previous STH')
parser.add_argument('--quiet-ok', action='store_true', help="Don't print status if OK")

def print_sth(sth):
    if sth is None:
        print "NONE"
    else:
        print sth['timestamp']
        print sth['sha256_root_hash']
        print sth['tree_size']
        print sth['tree_head_signature']

def get_new_sth(baseurl):
    try:
        sth = get_sth(baseurl)
    except Exception, e:
        print e
        sys.exit(NAGIOS_UNKNOWN)
    return sth
        
def read_sth(fn):
    try:
        f = open(fn)
    except IOError, e:
        if e.errno == errno.ENOENT:
            return None
        raise e
    return json.loads(f.read())

def mv_file(fromfn, tofn):
    shutil.move(fromfn, tofn)

def write_file(fn, sth):
    tempname = fn + ".new"
    open(tempname, 'w').write(json.dumps(sth))
    mv_file(tempname, fn)

class UTC(tzinfo):
    def utcoffset(self, dt):
      return timedelta(hours=0)
    def dst(self, dt):
        return timedelta(0)

def check_age(sth):
    age = time.time() - sth["timestamp"]/1000
    sth_time = datetime.fromtimestamp(sth['timestamp'] / 1000, UTC()).strftime("%Y-%m-%d %H:%M:%S")
    roothash = b64_to_b16(sth['sha256_root_hash'])
    if age > 6 * 3600:
        print "CRITICAL: %s is older than 6h: %s UTC" % (roothash, sth_time)
        sys.exit(NAGIOS_CRIT)
    if age > 2 * 3600:
        print "WARNING: %s is older than 2h: %s UTC" % (roothash, sth_time)
        sys.exit(NAGIOS_WARN)
    return "%s UTC, %d minutes ago" % (sth_time, age/60)

def check_consistency(newsth, oldsth, baseurl):
    consistency_proof = [base64.decodestring(entry) for entry in get_consistency_proof(baseurl, oldsth["tree_size"], newsth["tree_size"])]
    (old_treehead, new_treehead) = verify_consistency_proof(consistency_proof, oldsth["tree_size"], newsth["tree_size"], base64.b64decode(oldsth["sha256_root_hash"]))
    assert old_treehead == base64.b64decode(oldsth["sha256_root_hash"])
    assert new_treehead == base64.b64decode(newsth["sha256_root_hash"])

def b64_to_b16(s):
    return base64.b16encode(base64.decodestring(s))

def main(args):
    if args.cur_sth is None:
        args.cur_sth = "cur-sth.json"

    create_ssl_context(cafile=args.cafile)

    logpublickey = get_public_key_from_file(args.publickey) if args.publickey else None

    newsth = get_new_sth(args.baseurl)
    check_sth_signature(args.baseurl, newsth, publickey=logpublickey)

    oldsth = read_sth(args.cur_sth)

    #print_sth(newsth)
    #print_sth(oldsth)

    if oldsth:
        if newsth["tree_size"] == oldsth["tree_size"]:
            if oldsth["sha256_root_hash"] != newsth["sha256_root_hash"]:
                print "CRITICAL: root hash is different even though tree size is the same.",
                print "tree size:", newsth["tree_size"],
                print "old hash:", b64_to_b16(oldsth["sha256_root_hash"])
                print "new hash:", b64_to_b16(newsth["sha256_root_hash"])
                sys.exit(NAGIOS_CRIT)
        elif newsth["tree_size"] < oldsth["tree_size"]:
            if not args.allow_lag:
                print "CRITICAL: new tree smaller than previous tree (%d < %d)" % \
                  (newsth["tree_size"], oldsth["tree_size"])
                sys.exit(NAGIOS_CRIT)

    if oldsth and oldsth["tree_size"] > 0 and oldsth["tree_size"] != newsth["tree_size"]:
        check_consistency(newsth, oldsth, args.baseurl)

    age = check_age(newsth)

    write_file(args.cur_sth, newsth)

    if not args.quiet_ok:
        print "OK: size: %d hash: %s, %s" % (newsth["tree_size"], b64_to_b16(newsth["sha256_root_hash"])[:8], age)
    sys.exit(NAGIOS_OK)

if __name__ == '__main__':
    main(parser.parse_args())