From 94e6d102779db64b3cd875d693ec1ad9bed22257 Mon Sep 17 00:00:00 2001 From: rsleevi Date: Thu, 4 Sep 2014 19:05:24 -0700 Subject: Add a script for generating testing CRLSets This allows the test CRLSets to be kept in sync whenever test certificates are updated. BUG=403640 Review URL: https://codereview.chromium.org/515043002 Cr-Commit-Position: refs/heads/master@{#293401} --- net/data/ssl/scripts/crlsetutil.py | 200 +++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100755 net/data/ssl/scripts/crlsetutil.py (limited to 'net/data') diff --git a/net/data/ssl/scripts/crlsetutil.py b/net/data/ssl/scripts/crlsetutil.py new file mode 100755 index 0000000..49a1a86 --- /dev/null +++ b/net/data/ssl/scripts/crlsetutil.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# Copyright (c) 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +""" +This utility takes a JSON input that describes a CRLSet and produces a +CRLSet from it. + +The input is taken on stdin and is a dict with the following keys: + - BlockedBySPKI: An array of strings, where each string is a filename + containing a PEM certificate, from which an SPKI will be extracted. + - BlockedByHash: A dict of string to an array of ints, where the string is + a filename containing a PEM format certificate, and the ints are the + serial numbers. The listed serial numbers will be blocked when issued by + the given certificate. + +For example: + +{ + "BlockedBySPKI": ["/tmp/blocked-certificate"], + "BlockedByHash": { + "/tmp/intermediate-certificate": [1, 2, 3] + } +} +""" + +import hashlib +import json +import optparse +import struct +import sys + + +def _pem_cert_to_binary(pem_filename): + """Decodes the first PEM-encoded certificate in a given file into binary + + Args: + pem_filename: A filename that contains a PEM-encoded certificate. It may + contain additional data (keys, textual representation) which will be + ignored + + Returns: + A byte array containing the decoded certificate data + """ + base64 = "" + started = False + + with open(pem_filename, 'r') as pem_file: + for line in pem_file: + if not started: + if line.startswith('-----BEGIN CERTIFICATE'): + started = True + else: + if line.startswith('-----END CERTIFICATE'): + break + base64 += line[:-1].strip() + + return base64.decode('base64') + + +def _parse_asn1_element(der_bytes): + """Parses a DER-encoded tag/Length/Value into its component parts + + Args: + der_bytes: A DER-encoded ASN.1 data type + + Returns: + A tuple of the ASN.1 tag value, the length of the ASN.1 header that was + read, the sequence of bytes for the value, and then any data from der_bytes + that was not part of the tag/Length/Value. + """ + tag = ord(der_bytes[0]) + length = ord(der_bytes[1]) + header_length = 2 + + if length & 0x80: + num_length_bytes = length & 0x7f + length = 0 + for i in xrange(2, 2 + num_length_bytes): + length <<= 8 + length += ord(der_bytes[i]) + header_length = 2 + num_length_bytes + + contents = der_bytes[:header_length + length] + rest = der_bytes[header_length + length:] + + return (tag, header_length, contents, rest) + + +class ASN1Iterator(object): + """Iterator that parses and iterates through a ASN.1 DER structure""" + + def __init__(self, contents): + self._tag = 0 + self._header_length = 0 + self._rest = None + self._contents = contents + self.step_into() + + def step_into(self): + """Begins processing the inner contents of the next ASN.1 element""" + (self._tag, self._header_length, self._contents, self._rest) = ( + _parse_asn1_element(self._contents[self._header_length:])) + + def step_over(self): + """Skips/ignores the next ASN.1 element""" + (self._tag, self._header_length, self._contents, self._rest) = ( + _parse_asn1_element(self._rest)) + + def tag(self): + """Returns the ASN.1 tag of the current element""" + return self._tag + + def contents(self): + """Returns the raw data of the current element""" + return self._contents + + +def _der_cert_to_spki(der_bytes): + """Returns the subjectPublicKeyInfo of a DER-encoded certificate + + Args: + der_bytes: A DER-encoded certificate (RFC 5280) + + Returns: + A byte array containing the subjectPublicKeyInfo + """ + iterator = ASN1Iterator(der_bytes) + iterator.step_into() # enter certificate structure + iterator.step_into() # enter TBSCertificate + iterator.step_over() # over version + iterator.step_over() # over serial + iterator.step_over() # over signature algorithm + iterator.step_over() # over issuer name + iterator.step_over() # over validity + iterator.step_over() # over subject name + return iterator.contents() + + +def pem_cert_file_to_spki_hash(pem_filename): + """Gets the SHA-256 hash of the subjectPublicKeyInfo of a cert in a file + + Args: + pem_filename: A file containing a PEM-encoded certificate. + + Returns: + The SHA-256 hash of the first certificate in the file, as a byte sequence + """ + return hashlib.sha256( + _der_cert_to_spki(_pem_cert_to_binary(pem_filename))).digest() + + +def main(): + parser = optparse.OptionParser(description=sys.modules[__name__].__doc__) + parser.add_option('-o', '--output', + help='Specifies the output file. The default is stdout.') + options, _ = parser.parse_args() + outfile = sys.stdout + if options.output and options.output != '-': + outfile = open(options.output, 'wb') + + config = json.load(sys.stdin) + blocked_spkis = [ + pem_cert_file_to_spki_hash(pem_file).encode('base64').strip() + for pem_file in config.get('BlockedBySPKI', [])] + parents = { + pem_cert_file_to_spki_hash(pem_file): serials + for pem_file, serials in config.get('BlockedByHash', {}).iteritems() + } + header_json = { + 'Version': 0, + 'ContentType': 'CRLSet', + 'Sequence': 0, + 'DeltaFrom': 0, + 'NumParents': len(parents), + 'BlockedSPKIs': blocked_spkis, + } + header = json.dumps(header_json) + outfile.write(struct.pack('>= 8 + + outfile.write(struct.pack('