103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
#!/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 Signed Certificate Timestamp given the
|
|
details of a signing key, when to sign, and the certificate data to
|
|
sign. Currently only supports precert_entry types. See RFC 6962.
|
|
"""
|
|
|
|
import binascii
|
|
import calendar
|
|
import hashlib
|
|
from struct import pack
|
|
|
|
import pykey
|
|
from pyasn1.codec.der import encoder
|
|
|
|
|
|
class InvalidKeyError(Exception):
|
|
"""Helper exception to handle unknown key types."""
|
|
|
|
def __init__(self, key):
|
|
self.key = key
|
|
|
|
def __str__(self):
|
|
return 'Invalid key: "%s"' % str(self.key)
|
|
|
|
|
|
class SCT(object):
|
|
"""SCT represents a Signed Certificate Timestamp."""
|
|
|
|
def __init__(self, key, date, tbsCertificate, issuerKey):
|
|
self.key = key
|
|
self.timestamp = calendar.timegm(date.timetuple()) * 1000
|
|
self.tbsCertificate = tbsCertificate
|
|
self.issuerKey = issuerKey
|
|
|
|
def signAndEncode(self):
|
|
"""Returns a signed and encoded representation of the SCT as a
|
|
string."""
|
|
# The signature is over the following data:
|
|
# sct_version (one 0 byte)
|
|
# signature_type (one 0 byte)
|
|
# timestamp (8 bytes, milliseconds since the epoch)
|
|
# entry_type (two bytes [0, 1] - currently only precert_entry is
|
|
# supported)
|
|
# signed_entry (bytes of PreCert)
|
|
# extensions (2-byte-length-prefixed, currently empty (so two 0
|
|
# bytes))
|
|
# A PreCert is:
|
|
# issuer_key_hash (32 bytes of SHA-256 hash of the issuing
|
|
# public key, as DER-encoded SPKI)
|
|
# tbs_certificate (3-byte-length-prefixed data)
|
|
timestamp = pack("!Q", self.timestamp)
|
|
hasher = hashlib.sha256()
|
|
hasher.update(encoder.encode(self.issuerKey.asSubjectPublicKeyInfo()))
|
|
issuer_key_hash = hasher.digest()
|
|
len_prefix = pack("!L", len(self.tbsCertificate))[1:]
|
|
data = (
|
|
b"\0\0"
|
|
+ timestamp
|
|
+ b"\0\1"
|
|
+ issuer_key_hash
|
|
+ len_prefix
|
|
+ self.tbsCertificate
|
|
+ b"\0\0"
|
|
)
|
|
if isinstance(self.key, pykey.ECCKey):
|
|
signatureByte = b"\3"
|
|
elif isinstance(self.key, pykey.RSAKey):
|
|
signatureByte = b"\1"
|
|
else:
|
|
raise InvalidKeyError(self.key)
|
|
# sign returns a hex string like "'<hex bytes>'H", but we want
|
|
# bytes here
|
|
hexSignature = self.key.sign(data, pykey.HASH_SHA256)
|
|
signature = binascii.unhexlify(hexSignature[1:-2])
|
|
# The actual data returned is the following:
|
|
# sct_version (one 0 byte)
|
|
# id (32 bytes of SHA-256 hash of the signing key, as
|
|
# DER-encoded SPKI)
|
|
# timestamp (8 bytes, milliseconds since the epoch)
|
|
# extensions (2-byte-length-prefixed data, currently
|
|
# empty)
|
|
# hash (one 4 byte representing sha256)
|
|
# signature (one byte - 1 for RSA and 3 for ECDSA)
|
|
# signature (2-byte-length-prefixed data)
|
|
hasher = hashlib.sha256()
|
|
hasher.update(encoder.encode(self.key.asSubjectPublicKeyInfo()))
|
|
key_id = hasher.digest()
|
|
signature_len_prefix = pack("!H", len(signature))
|
|
return (
|
|
b"\0"
|
|
+ key_id
|
|
+ timestamp
|
|
+ b"\0\0\4"
|
|
+ signatureByte
|
|
+ signature_len_prefix
|
|
+ signature
|
|
)
|