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