Skip to content

Commit 3a8a09e

Browse files
committed
Adding a small Python utility to submit certificate chains.
1 parent b9f607b commit 3a8a09e

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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

Comments
 (0)