diff options
| author | Linus Nordberg <linus@nordberg.se> | 2021-06-03 16:07:17 +0200 | 
|---|---|---|
| committer | Linus Nordberg <linus@nordberg.se> | 2021-06-03 16:07:17 +0200 | 
| commit | 58ae1f824cf6df237849bd6666b5615808f47468 (patch) | |
| tree | 8d3e607b854d7594fd0e7ed7ab1331b6ac1364ff | |
| parent | b1c954687ca049ac7e34034f98ba9fd8c15258e7 (diff) | |
get the logic for --bootstrap-log right
| -rwxr-xr-x | siglog-witness.py | 150 | 
1 files changed, 93 insertions, 57 deletions
| diff --git a/siglog-witness.py b/siglog-witness.py index f3475d2..06a2c9c 100755 --- a/siglog-witness.py +++ b/siglog-witness.py @@ -31,10 +31,11 @@ SIGKEY_FILE_DEFAULT = CONFIG_DIR_DEFAULT + 'signing_key'  CONFIG_FILE = CONFIG_DIR_DEFAULT + 'siglog-witness.conf'  ERR_TREEHEAD_SIGNATURE_INVALID = 2 -ERR_CONSISTENCYPROOF_FETCH     = 4 -ERR_CONSISTENCYPROOF_INVALID   = 5 -ERR_TREEHEAD_FETCH             = 6 -ERR_LOGKEYFILE                 = 7 +ERR_TREEHEAD_READ              = 3 +ERR_TREEHEAD_FETCH             = 4 +ERR_CONSISTENCYPROOF_FETCH     = 5 +ERR_CONSISTENCYPROOF_INVALID   = 6 +ERR_LOGKEY                     = 7  ERR_LOGKEY_FORMAT              = 8  ERR_SIGKEYFILE_TYPE            = 9  ERR_SIGKEYFILE_MODE            = 10 @@ -170,30 +171,42 @@ def make_base_dir_maybe():      except FileNotFoundError:          os.makedirs(dirname, mode=0o700) -def read_tree_head(): -    filename = os.path.expanduser(g_args.base_dir) + 'signed_tree_head' +def read_tree_head(filename):      try:          with open(filename, mode='r') as f:              return TreeHead(f.read())      except FileNotFoundError:          return None +def read_tree_head_and_verify(log_verification_key): +    fn = os.path.expanduser(g_args.base_dir) + 'signed_tree_head' +    tree_head = read_tree_head(fn) +    if not tree_head: +        return None, (ERR_TREEHEAD_READ, +                      "ERROR: unable to read file {}".format(fn)) + +    if not tree_head.signature_valid(log_verification_key): +        return None, (ERR_TREEHEAD_SIGNATURE_INVALID, +                      "ERROR: signature of stored tree head invalid") + +    return tree_head, None +  def store_tree_head(tree_head):      dirname = os.path.expanduser(g_args.base_dir)      with open(dirname + 'signed_tree_head', mode='w+b') as f:          f.write(tree_head.text()) -def fetch_tree_head(log_verification_key): +def fetch_tree_head_and_verify(log_verification_key):      req = requests.get(g_args.base_url + 'st/v0/get-tree-head-to-sign')      if req.status_code != 200: -        return None, (ERR_TREEHEAD_FETCH, "ERROR: unable to fetch new tree head: {}".format(req.status_code)) +        return None, (ERR_TREEHEAD_FETCH, +                      "ERROR: unable to fetch new tree head: {}".format(req.status_code))      tree_head = TreeHead(req.content.decode()) -      if not tree_head.signature_valid(log_verification_key): -        return None, (ERR_TREEHEAD_SIGNATURE_INVALID, "ERROR: signature of fetched tree head not valid") +        return None, (ERR_TREEHEAD_SIGNATURE_INVALID, +                      "ERROR: signature of fetched tree head invalid") -    assert(tree_head is not None)      return tree_head, None  def fetch_consistency_proof(first, second): @@ -201,9 +214,9 @@ def fetch_consistency_proof(first, second):      post_data += 'new_size={}\n'.format(second)      req = requests.post(g_args.base_url + 'st/v0/get-consistency-proof', post_data)      if req.status_code != 200: -        print("ERROR: st/v0/get-consistency-proof({}) => {}".format(post_data, req)) -        return None -    return ConsistencyProof(req.content.decode()) +        return None, (ERR_CONSISTENCYPROOF_FETCH, +                      "ERROR: unable to fetch consistency proof: {}".format(req.status_code)) +    return ConsistencyProof(req.content.decode()), None  def numbits(n):      p = 0 @@ -253,9 +266,9 @@ def consistency_proof_valid(first, second, proof):      return sn == 0 and fr == first.root_hash() and sr == second.root_hash() -def sign_and_send_sig(signing_key, sth): +def sign_send_store_tree_head(signing_key, tree_head):      hash = sha256(signing_key.verify_key.encode()) -    signature = signing_key.sign(sth.serialise()).signature +    signature = signing_key.sign(tree_head.serialise()).signature      post_data = 'signature={}\n'.format(hexlify(signature).decode('ascii'))      post_data += 'key_hash={}\n'.format(hash.hexdigest()) @@ -266,14 +279,18 @@ def sign_and_send_sig(signing_key, sth):                  "ERROR: Unable to post signature to log: {} => {}: {}". format(req.url,                                                                                 req.status_code,                                                                                 req.text)) +    # Store only when all else is done. Next invocation will treat a +    # stored tree head as having been verified. +    store_tree_head(tree_head)  def ensure_log_verification_key():      if not g_args.log_verification_key: -        return None, (ERR_LOGKEYFILE, "ERROR: missing log verification key") +        return None, (ERR_LOGKEY, "ERROR: missing log verification key")      try:          log_verification_key = nacl.signing.VerifyKey(g_args.log_verification_key, encoder=nacl.encoding.HexEncoder)      except: -        return None, (ERR_LOGKEY_FORMAT, "ERROR: invalid log verification key: {}".format(g_args.log_verification_key)) +        return None, (ERR_LOGKEY_FORMAT, +                      "ERROR: invalid log verification key: {}".format(g_args.log_verification_key))      assert(log_verification_key is not None)      return log_verification_key, None @@ -292,21 +309,28 @@ def ensure_sigkey(fn):      s = os.stat(fn, follow_symlinks=False)      if not S_ISREG(s.st_mode): -        return None, (ERR_SIGKEYFILE_TYPE, "ERROR: Signing key file {} must be a regular file".format(fn)) +        return None, (ERR_SIGKEYFILE_TYPE, +                      "ERROR: Signing key file {} must be a regular file".format(fn))      if S_IMODE(s.st_mode) & 0o077 != 0: -        return None, (ERR_SIGKEYFILE_MODE, "ERROR: Signing key file {} permissions too lax: {:04o}".format(fn, S_IMODE(s.st_mode))) +        return None, (ERR_SIGKEYFILE_MODE, +                      "ERROR: Signing key file {} permissions too lax: {:04o}".format(fn, S_IMODE(s.st_mode)))      with open(fn, 'r') as f:          try:              signing_key = nacl.signing.SigningKey(f.readline().strip(), nacl.encoding.HexEncoder)          except: -            return None, (ERR_SIGKEY_FORMAT, "ERROR: Invalid signing key in {}".format(fn)) +            return None, (ERR_SIGKEY_FORMAT, +                          "ERROR: Invalid signing key in {}".format(fn))      assert(signing_key is not None)      return signing_key, None +def user_confirm(prompt): +    if input(prompt + ' y/n> ').lower()[0] == 'y': +        return True +    return False +  def main(args): -    msg = None      global g_args      g_args = Parser()      parse_config(CONFIG_FILE) @@ -326,43 +350,55 @@ def main(args):      signing_key, err = ensure_sigkey(g_args.sigkey_file)      if err: return err -    new, err = fetch_tree_head(log_verification_key) +    new = None                  # FIXME rename new -> new_tree_head +    cur, err = read_tree_head_and_verify(log_verification_key) # FIXME rename cur -> cur_tree_head +    if err: +        new, err2 = fetch_tree_head_and_verify(log_verification_key) +        if err2: return err2 + +        if not g_args.bootstrap_log: +            return err + +        print("\nWARNING: We have only seen one single tree head from the\n" +              "log {},\n" +              "representing a tree of size {}. We are therefore unable to\n" +              "verify that the tree it represents is really a superset of an\n" +              "earlier version of the tree in this log.\n" +              "\nWe are effectively signing this tree head blindly.\n".format(g_args.base_url, +                                                                              new.tree_size())) +        if user_confirm("Really sign head for tree of size {} and upload " +                        "the signature?".format(new.tree_size())): +            err3 = sign_send_store_tree_head(signing_key, new) +            if err3: return err3 + +        return 0, None + +    new, err = fetch_tree_head_and_verify(log_verification_key)      if err: return err -    # FIXME: if we're bootstrapping, ignore potential tree head on disk -    cur = read_tree_head() -    if not cur: -        print("INFO: No current tree head found in {}".format(g_args.base_dir)) -    else: -        if not cur.signature_valid(log_verification_key): -            return ERR_TREEHEAD_SIGNATURE_INVALID, "ERROR: signature of current tree head invalid" -        if new.tree_size() <= cur.tree_size(): -            msg = "INFO: Fetched tree already verified, size {}".format(cur.tree_size()) -        else: -            proof = fetch_consistency_proof(cur.tree_size(), new.tree_size()) -            if not proof: -                return ERR_CONSISTENCYPROOF_FETCH, "ERROR: unable to fetch consistency proof" -            if consistency_proof_valid(cur, new, proof): -                consistency_verified = True -            else: -                errmsg = "ERROR: failing consistency proof check for {}->{}".format(cur.tree_size(), new.tree_size()) -                errmsg += "DEBUG: {}:{}->{}:{}\n  {}".format(cur.tree_size(), -                                                             cur.root_hash(), -                                                             new.tree_size(), -                                                             new.root_hash(), -                                                             proof.path()) -                return ERR_CONSISTENCYPROOF_INVALID, errmsg - -    if g_args.bootstrap_log: -        # TODO maybe require user confirmation -        ignore_consistency = True - -    store_tree_head(new) -    if consistency_verified or ignore_consistency: -        err = sign_and_send_sig(signing_key, new) -        if err: return err - -    return 0, msg +    if not cur.signature_valid(log_verification_key): +        return ERR_TREEHEAD_SIGNATURE_INVALID, "ERROR: signature of current tree head invalid" + +    if new.tree_size() <= cur.tree_size(): +        return 0, "INFO: Fetched head of tree of size {} already seen".format(cur.tree_size()) + +    proof, err = fetch_consistency_proof(cur.tree_size(), new.tree_size()) +    if err: return err + +    if not consistency_proof_valid(cur, new, proof): +        errmsg = "ERROR: failing consistency proof check for {}->{}\n".format(cur.tree_size(), +                                                                              new.tree_size()) +        errmsg += "DEBUG: {}:{}->{}:{}\n  {}".format(cur.tree_size(), +                                                     cur.root_hash(), +                                                     new.tree_size(), +                                                     new.root_hash(), +                                                     proof.path()) +        return ERR_CONSISTENCYPROOF_INVALID, errmsg + +    err = sign_send_store_tree_head(signing_key, new) +    if err: return err + +    return 0, None  if __name__ == '__main__':      status = main(sys.argv) | 
