pyct.py (7912B)
1 #!/usr/bin/env python 2 # 3 # This Source Code Form is subject to the terms of the Mozilla Public 4 # License, v. 2.0. If a copy of the MPL was not distributed with this 5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7 """ 8 Helper library for creating a Signed Certificate Timestamp given the 9 details of a signing key, when to sign, and the certificate data to 10 sign. See RFC 6962. 11 12 When run with an output file-like object and a path to a file containing 13 a specification, creates an SCT from the given information and writes it 14 to the output object. The specification is as follows: 15 16 timestamp:<YYYYMMDD> 17 [key:<key specification>] 18 [tamper] 19 [leafIndex:<leaf index>] 20 certificate: 21 <certificate specification> 22 23 Where: 24 [] indicates an optional field or component of a field 25 <> indicates a required component of a field 26 27 By default, the "default" key is used (logs are essentially identified 28 by key). Other keys known to pykey can be specified. 29 30 The certificate specification must come last. 31 """ 32 33 import binascii 34 import calendar 35 import datetime 36 import hashlib 37 from io import StringIO 38 from struct import pack 39 40 import pycert 41 import pykey 42 from pyasn1.codec.der import encoder 43 44 45 class InvalidKeyError(Exception): 46 """Helper exception to handle unknown key types.""" 47 48 def __init__(self, key): 49 self.key = key 50 51 def __str__(self): 52 return f'Invalid key: "{str(self.key)}"' 53 54 55 class UnknownSignedEntryType(Exception): 56 """Helper exception to handle unknown SignedEntry types.""" 57 58 def __init__(self, signedEntry): 59 self.signedEntry = signedEntry 60 61 def __str__(self): 62 return f'Unknown SignedEntry type: "{str(self.signedEntry)}"' 63 64 65 class SignedEntry: 66 """Base class for CT entries. Use PrecertEntry or 67 X509Entry.""" 68 69 70 class PrecertEntry(SignedEntry): 71 """Precertificate entry type for SCT.""" 72 73 def __init__(self, tbsCertificate, issuerKey): 74 self.tbsCertificate = tbsCertificate 75 self.issuerKey = issuerKey 76 77 78 class X509Entry(SignedEntry): 79 """x509 certificate entry type for SCT.""" 80 81 def __init__(self, certificate): 82 self.certificate = certificate 83 84 85 class SCT: 86 """SCT represents a Signed Certificate Timestamp.""" 87 88 def __init__(self, key, date, signedEntry, leafIndex=None): 89 self.key = key 90 self.timestamp = calendar.timegm(date.timetuple()) * 1000 91 self.signedEntry = signedEntry 92 self.tamper = False 93 self.leafIndex = leafIndex 94 95 def signAndEncode(self): 96 """Returns a signed and encoded representation of the 97 SCT as a string.""" 98 # The signature is over the following data: 99 # sct_version (one 0 byte) 100 # signature_type (one 0 byte) 101 # timestamp (8 bytes, milliseconds since the epoch) 102 # entry_type (two bytes (one 0 byte followed by one 0 byte for 103 # X509Entry or one 1 byte for PrecertEntry) 104 # signed_entry (bytes of X509Entry or PrecertEntry) 105 # extensions (2-byte-length-prefixed) 106 # A X509Entry is: 107 # certificate (3-byte-length-prefixed data) 108 # A PrecertEntry is: 109 # issuer_key_hash (32 bytes of SHA-256 hash of the issuing 110 # public key, as DER-encoded SPKI) 111 # tbs_certificate (3-byte-length-prefixed data) 112 timestamp = pack("!Q", self.timestamp) 113 114 if isinstance(self.signedEntry, X509Entry): 115 len_prefix = pack("!L", len(self.signedEntry.certificate))[1:] 116 entry_with_type = b"\0" + len_prefix + self.signedEntry.certificate 117 elif isinstance(self.signedEntry, PrecertEntry): 118 hasher = hashlib.sha256() 119 hasher.update( 120 encoder.encode(self.signedEntry.issuerKey.asSubjectPublicKeyInfo()) 121 ) 122 issuer_key_hash = hasher.digest() 123 len_prefix = pack("!L", len(self.signedEntry.tbsCertificate))[1:] 124 entry_with_type = ( 125 b"\1" + issuer_key_hash + len_prefix + self.signedEntry.tbsCertificate 126 ) 127 else: 128 raise UnknownSignedEntryType(self.signedEntry) 129 extensions = [] 130 if self.leafIndex: 131 # An extension consists of 1 byte to identify the extension type, 2 132 # big-endian bytes for the length of the extension data, and then 133 # the extension data. 134 # The type of leaf_index is 0, and its data consists of 5 bytes. 135 extensions = [b"\0\0\5" + self.leafIndex.to_bytes(5, byteorder="big")] 136 extensionsLength = sum(map(len, extensions)) 137 extensionsEncoded = extensionsLength.to_bytes(2, byteorder="big") + b"".join( 138 extensions 139 ) 140 data = b"\0\0" + timestamp + b"\0" + entry_with_type + extensionsEncoded 141 if isinstance(self.key, pykey.ECCKey): 142 signatureByte = b"\3" 143 elif isinstance(self.key, pykey.RSAKey): 144 signatureByte = b"\1" 145 else: 146 raise InvalidKeyError(self.key) 147 # sign returns a hex string like "'<hex bytes>'H", but we want 148 # bytes here 149 hexSignature = self.key.sign(data, pykey.HASH_SHA256) 150 signature = bytearray(binascii.unhexlify(hexSignature[1:-2])) 151 if self.tamper: 152 signature[-1] = ~signature[-1] & 0xFF 153 # The actual data returned is the following: 154 # sct_version (one 0 byte) 155 # id (32 bytes of SHA-256 hash of the signing key, as 156 # DER-encoded SPKI) 157 # timestamp (8 bytes, milliseconds since the epoch) 158 # extensions (2-byte-length-prefixed data) 159 # hash (one 4 byte representing sha256) 160 # signature (one byte - 1 for RSA and 3 for ECDSA) 161 # signature (2-byte-length-prefixed data) 162 hasher = hashlib.sha256() 163 hasher.update(encoder.encode(self.key.asSubjectPublicKeyInfo())) 164 key_id = hasher.digest() 165 signature_len_prefix = pack("!H", len(signature)) 166 return ( 167 b"\0" 168 + key_id 169 + timestamp 170 + extensionsEncoded 171 + b"\4" 172 + signatureByte 173 + signature_len_prefix 174 + signature 175 ) 176 177 @staticmethod 178 def fromSpecification(specStream): 179 key = pykey.keyFromSpecification("default") 180 certificateSpecification = StringIO() 181 readingCertificateSpecification = False 182 tamper = False 183 leafIndex = None 184 for line in specStream.readlines(): 185 lineStripped = line.strip() 186 if readingCertificateSpecification: 187 print(lineStripped, file=certificateSpecification) 188 elif lineStripped == "certificate:": 189 readingCertificateSpecification = True 190 elif lineStripped.startswith("key:"): 191 key = pykey.keyFromSpecification(lineStripped[len("key:") :]) 192 elif lineStripped.startswith("timestamp:"): 193 timestamp = datetime.datetime.strptime( 194 lineStripped[len("timestamp:") :], "%Y%m%d" 195 ) 196 elif lineStripped == "tamper": 197 tamper = True 198 elif lineStripped.startswith("leafIndex:"): 199 leafIndex = int(lineStripped[len("leafIndex:") :]) 200 else: 201 raise pycert.UnknownParameterTypeError(lineStripped) 202 certificateSpecification.seek(0) 203 certificate = pycert.Certificate(certificateSpecification).toDER() 204 sct = SCT(key, timestamp, X509Entry(certificate)) 205 sct.tamper = tamper 206 sct.leafIndex = leafIndex 207 return sct 208 209 210 # The build harness will call this function with an output 211 # file-like object and a path to a file containing an SCT 212 # specification. This will read the specification and output 213 # the SCT as bytes. 214 def main(output, inputPath): 215 with open(inputPath) as configStream: 216 output.write(SCT.fromSpecification(configStream).signAndEncode())