tor-browser

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

pytlsbinding.py (5141B)


      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 2-QWACs TLS certificate binding given the details
      9 of a signing certificate and a certificate to bind. See ETSI TS 119 411-5
     10 V2.1.1 Annex B.
     11 
     12 When run with an output file-like object and a path to a file containing
     13 a specification, creates a TLS certificate binding from the given information
     14 and writes it to the output object. The specification is as follows:
     15 
     16 signingCertificate:
     17 <certificate specification>
     18 :end
     19 certificateToBind:
     20 <certificate specification>
     21 :end
     22 
     23 Where:
     24  <> indicates a required component of a field
     25  ":end" indicates the end of a multi-line specification
     26 
     27 Currently only the algorithms RS256 (RSA PKCS#1v1.5 with SHA-256) and S256
     28 (SHA-256) are supported.
     29 """
     30 
     31 import base64
     32 import hashlib
     33 import json
     34 from io import StringIO
     35 
     36 import pycert
     37 import pykey
     38 
     39 
     40 def urlsafebase64(b):
     41    """Helper function that takes a bytes-like object and returns the
     42    urlsafebase64-encoded bytes without any trailing '='."""
     43    return base64.urlsafe_b64encode(b).decode().replace("=", "").encode("utf-8")
     44 
     45 
     46 class Header:
     47    """Class representing a 2-QWACs TLS certificate binding header."""
     48 
     49    def __init__(self, signingCertificate, certificateToBind):
     50        self.signingCertificate = signingCertificate
     51        self.certificateToBind = certificateToBind
     52 
     53    def __str__(self):
     54        signingCertificateBase64 = base64.standard_b64encode(
     55            self.signingCertificate.toDER()
     56        ).decode()
     57        certificateToBindDER = self.certificateToBind.toDER()
     58        certificateToBindBase64Urlsafe = urlsafebase64(certificateToBindDER)
     59        certificateToBindHash = urlsafebase64(
     60            hashlib.sha256(certificateToBindBase64Urlsafe).digest()
     61        ).decode()
     62        header = {
     63            "alg": "RS256",
     64            "cty": "TLS-Certificate-Binding-v1",
     65            "x5c": [signingCertificateBase64],
     66            "sigD": {
     67                "mId": "http://uri.etsi.org/19182/ObjectIdByURIHash",
     68                "pars": [""],
     69                "hashM": "S256",
     70                "hashV": [certificateToBindHash],
     71            },
     72        }
     73        return json.dumps(header)
     74 
     75 
     76 class TLSBinding:
     77    """Class representing a 2-QWACs TLS certificate binding."""
     78 
     79    def __init__(self, signingCertificate, certificateToBind):
     80        self.signingCertificate = signingCertificate
     81        self.certificateToBind = certificateToBind
     82 
     83    @staticmethod
     84    def fromSpecification(specStream):
     85        """Constructs a TLS certificate binding from a specification."""
     86        signingCertificateSpecification = StringIO()
     87        readingSigningCertificateSpecification = False
     88        certificateToBindSpecification = StringIO()
     89        readingCertificateToBindSpecification = False
     90        for line in specStream.readlines():
     91            lineStripped = line.strip()
     92            if readingSigningCertificateSpecification:
     93                if lineStripped == ":end":
     94                    readingSigningCertificateSpecification = False
     95                else:
     96                    print(lineStripped, file=signingCertificateSpecification)
     97            elif readingCertificateToBindSpecification:
     98                if lineStripped == ":end":
     99                    readingCertificateToBindSpecification = False
    100                else:
    101                    print(lineStripped, file=certificateToBindSpecification)
    102            elif lineStripped == "certificateToBind:":
    103                readingCertificateToBindSpecification = True
    104            elif lineStripped == "signingCertificate:":
    105                readingSigningCertificateSpecification = True
    106            else:
    107                raise pycert.UnknownParameterTypeError(lineStripped)
    108        signingCertificateSpecification.seek(0)
    109        signingCertificate = pycert.Certificate(signingCertificateSpecification)
    110        certificateToBindSpecification.seek(0)
    111        certificateToBind = pycert.Certificate(certificateToBindSpecification)
    112        return TLSBinding(signingCertificate, certificateToBind)
    113 
    114    def signAndEncode(self):
    115        """Returns a signed and encoded representation of the TLS certificate
    116        binding as bytes."""
    117        header = urlsafebase64(
    118            str(Header(self.signingCertificate, self.certificateToBind)).encode("utf-8")
    119        )
    120        signature = self.signingCertificate.subjectKey.sign(
    121            header + b".", pykey.HASH_SHA256
    122        )
    123        # signature will be of the form "'AABBCC...'H"
    124        return (
    125            header.decode()
    126            + ".."
    127            + urlsafebase64(bytes.fromhex(signature[1:-2])).decode()
    128        )
    129 
    130 
    131 # The build harness will call this function with an output
    132 # file-like object and a path to a file containing an SCT
    133 # specification. This will read the specification and output
    134 # the SCT as bytes.
    135 def main(output, inputPath):
    136    with open(inputPath) as configStream:
    137        output.write(TLSBinding.fromSpecification(configStream).signAndEncode())