#!/usr/bin/env python
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Helper library
for creating a
2-QWACs TLS certificate binding given the details
of a signing certificate
and a certificate to bind. See ETSI TS
119 411-
5
V2.
1.
1 Annex B.
When run
with an output file-like object
and a path to a file containing
a specification, creates a TLS certificate binding
from the given information
and writes it to the output object. The specification
is as follows:
signingCertificate:
<certificate specification>
:end
certificateToBind:
<certificate specification>
:end
Where:
<> indicates a required component of a field
":end" indicates the end of a multi-line specification
Currently only the algorithms RS256 (RSA PKCS
#1v1.5 with SHA-256) and S256
(SHA-
256) are supported.
"""
import base64
import hashlib
import json
from io
import StringIO
import pycert
import pykey
def urlsafebase64(b):
"""Helper function that takes a bytes-like object and returns the
urlsafebase64-encoded bytes without any trailing
'='.
"""
return base64.urlsafe_b64encode(b).decode().replace(
"=",
"").encode(
"utf-8")
class Header:
"""Class representing a 2-QWACs TLS certificate binding header."""
def __init__(self, signingCertificate, certificateToBind):
self.signingCertificate = signingCertificate
self.certificateToBind = certificateToBind
def __str__(self):
signingCertificateBase64 = base64.standard_b64encode(
self.signingCertificate.toDER()
).decode()
certificateToBindDER = self.certificateToBind.toDER()
certificateToBindBase64Urlsafe = urlsafebase64(certificateToBindDER)
certificateToBindHash = urlsafebase64(
hashlib.sha256(certificateToBindBase64Urlsafe).digest()
).decode()
header = {
"alg":
"RS256",
"cty":
"TLS-Certificate-Binding-v1",
"x5c": [signingCertificateBase64],
"sigD": {
"mId":
"http://uri.etsi.org/19182/ObjectIdByURIHash",
"pars": [
""],
"hashM":
"S256",
"hashV": [certificateToBindHash],
},
}
return json.dumps(header)
class TLSBinding:
"""Class representing a 2-QWACs TLS certificate binding."""
def __init__(self, signingCertificate, certificateToBind):
self.signingCertificate = signingCertificate
self.certificateToBind = certificateToBind
@staticmethod
def fromSpecification(specStream):
"""Constructs a TLS certificate binding from a specification."""
signingCertificateSpecification = StringIO()
readingSigningCertificateSpecification =
False
certificateToBindSpecification = StringIO()
readingCertificateToBindSpecification =
False
for line
in specStream.readlines():
lineStripped = line.strip()
if readingSigningCertificateSpecification:
if lineStripped ==
":end":
readingSigningCertificateSpecification =
False
else:
print(lineStripped, file=signingCertificateSpecification)
elif readingCertificateToBindSpecification:
if lineStripped ==
":end":
readingCertificateToBindSpecification =
False
else:
print(lineStripped, file=certificateToBindSpecification)
elif lineStripped ==
"certificateToBind:":
readingCertificateToBindSpecification =
True
elif lineStripped ==
"signingCertificate:":
readingSigningCertificateSpecification =
True
else:
raise pycert.UnknownParameterTypeError(lineStripped)
signingCertificateSpecification.seek(
0)
signingCertificate = pycert.Certificate(signingCertificateSpecification)
certificateToBindSpecification.seek(
0)
certificateToBind = pycert.Certificate(certificateToBindSpecification)
return TLSBinding(signingCertificate, certificateToBind)
def signAndEncode(self):
"""Returns a signed and encoded representation of the TLS certificate
binding
as bytes.
"""
header = urlsafebase64(
str(Header(self.signingCertificate, self.certificateToBind)).encode(
"utf-8")
)
signature = self.signingCertificate.subjectKey.sign(
header + b
".", pykey.HASH_SHA256
)
# signature will be of the form "'AABBCC...'H"
return (
header.decode()
+
".."
+ urlsafebase64(bytes.fromhex(signature[
1:-
2])).decode()
)
# The build harness will call this function with an output
# file-like object and a path to a file containing an SCT
# specification. This will read the specification and output
# the SCT as bytes.
def main(output, inputPath):
with open(inputPath)
as configStream:
output.write(TLSBinding.fromSpecification(configStream).signAndEncode())