tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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())