|
| 1 | +#!/usr/bin/env python |
| 2 | +"""Submits a chain to a list of logs.""" |
| 3 | + |
| 4 | +import base64 |
| 5 | +import hashlib |
| 6 | +import sys |
| 7 | + |
| 8 | +import json |
| 9 | +import logging |
| 10 | +import gflags |
| 11 | + |
| 12 | +from ct.client import log_client |
| 13 | +from ct.crypto import cert |
| 14 | +from ct.crypto import error |
| 15 | +from ct.crypto import pem |
| 16 | +from ct.crypto import verify |
| 17 | +from ct.proto import client_pb2 |
| 18 | +from ct.serialization import tls_message |
| 19 | + |
| 20 | +FLAGS = gflags.FLAGS |
| 21 | + |
| 22 | +gflags.DEFINE_string("log_list", None, "File containing the list of logs " |
| 23 | + "to submit to (see certificate-transparency.org/known-logs" |
| 24 | + " for the format description).") |
| 25 | +gflags.DEFINE_string("chain", None, "Certificate chain to submit (PEM).") |
| 26 | +gflags.DEFINE_string("log_scheme", "http", "Log scheme (http/https)") |
| 27 | +gflags.DEFINE_string("output", None, "output file for sct_list") |
| 28 | +gflags.MarkFlagAsRequired("log_list") |
| 29 | +gflags.MarkFlagAsRequired("chain") |
| 30 | +gflags.MarkFlagAsRequired("output") |
| 31 | + |
| 32 | +def _read_ct_log_list(log_list_file): |
| 33 | + """Parses the log list JSON, returns a log url to key map.""" |
| 34 | + try: |
| 35 | + log_list_json = json.loads(log_list_file) |
| 36 | + log_url_to_key = {} |
| 37 | + for log_info in log_list_json['logs']: |
| 38 | + log_url_to_key[FLAGS.log_scheme + '://' + log_info['url']] = ( |
| 39 | + base64.decodestring(log_info['key'])) |
| 40 | + return log_url_to_key |
| 41 | + except (OSError, IOError) as io_exception: |
| 42 | + raise Exception('Could not read log list file %s: %s' % |
| 43 | + (log_list_file, io_exception)) |
| 44 | + |
| 45 | +def _submit_to_single_log(log_url, full_chain): |
| 46 | + """Submits the chain to a single log specified by log_url.""" |
| 47 | + ct_client = log_client.LogClient(log_url, connection_timeout=10) |
| 48 | + res = None |
| 49 | + try: |
| 50 | + res = ct_client.add_chain(full_chain) |
| 51 | + except log_client.HTTPError as err: |
| 52 | + logging.info('Skipping log %s because of error: %s\n', log_url, err) |
| 53 | + return res |
| 54 | + |
| 55 | +def _map_log_id_to_verifier(log_list): |
| 56 | + """Returns a map from log id to verifier object from the log_list.""" |
| 57 | + log_id_to_verifier = {} |
| 58 | + for log_key in log_list.values(): |
| 59 | + key_id = hashlib.sha256(log_key).digest() |
| 60 | + key_info = client_pb2.KeyInfo() |
| 61 | + key_info.type = client_pb2.KeyInfo.ECDSA |
| 62 | + key_info.pem_key = pem.to_pem(log_key, 'PUBLIC KEY') |
| 63 | + log_id_to_verifier[key_id] = verify.LogVerifier(key_info) |
| 64 | + return log_id_to_verifier |
| 65 | + |
| 66 | +def _submit_to_all_logs(log_list, certs_chain): |
| 67 | + """Submits the chain to all logs in log_list and validates SCTs.""" |
| 68 | + log_id_to_verifier = _map_log_id_to_verifier(log_list) |
| 69 | + |
| 70 | + chain_der = [c.to_der() for c in certs_chain] |
| 71 | + raw_scts_for_cert = [] |
| 72 | + for log_url in log_list.keys(): |
| 73 | + res = _submit_to_single_log(log_url, chain_der) |
| 74 | + if res: |
| 75 | + raw_scts_for_cert.append(res) |
| 76 | + else: |
| 77 | + logging.info("No SCT from log %s", log_url) |
| 78 | + |
| 79 | + validated_scts = [] |
| 80 | + for raw_sct in raw_scts_for_cert: |
| 81 | + key_id = raw_sct.id.key_id |
| 82 | + try: |
| 83 | + log_id_to_verifier[key_id].verify_sct(raw_sct, certs_chain[0]) |
| 84 | + validated_scts.append(raw_sct) |
| 85 | + except error.SignatureError as err: |
| 86 | + logging.warning( |
| 87 | + 'Discarding SCT from log_id %s which does not validate: %s', |
| 88 | + key_id.encode('hex'), err) |
| 89 | + except KeyError as err: |
| 90 | + logging.warning('Could not find CT log validator for log_id %s. ' |
| 91 | + 'The log key for this log is probably misconfigured.', |
| 92 | + key_id.encode('hex')) |
| 93 | + |
| 94 | + scts_for_cert = [tls_message.encode(proto_sct) |
| 95 | + for proto_sct in validated_scts |
| 96 | + if proto_sct] |
| 97 | + sct_list = client_pb2.SignedCertificateTimestampList() |
| 98 | + sct_list.sct_list.extend(scts_for_cert) |
| 99 | + return tls_message.encode(sct_list) |
| 100 | + |
| 101 | +def run(): |
| 102 | + """Submits the chain specified in the flags to all logs.""" |
| 103 | + logging.getLogger().setLevel(logging.INFO) |
| 104 | + logging.info("Starting up.") |
| 105 | + with open(FLAGS.log_list) as log_list_file: |
| 106 | + log_url_to_key = _read_ct_log_list(log_list_file.read()) |
| 107 | + |
| 108 | + certs_chain = [c for c in cert.certs_from_pem_file(FLAGS.chain)] |
| 109 | + logging.info("Chain is of length %d", len(certs_chain)) |
| 110 | + |
| 111 | + sct_list = _submit_to_all_logs(log_url_to_key, certs_chain) |
| 112 | + with open(FLAGS.output, 'wb') as sct_list_file: |
| 113 | + sct_list_file.write(sct_list) |
| 114 | + |
| 115 | +if __name__ == "__main__": |
| 116 | + sys.argv = FLAGS(sys.argv) |
| 117 | + run() |
0 commit comments