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/.
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
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:
17 [key:<key specification>]
20 <certificate specification>
23 [] indicates an optional field or component of a field
24 <> indicates a required component of a field
26 By default, the "default" key is used (logs are essentially identified
27 by key). Other keys known to pykey can be specified.
29 The certificate specification must come last.
36 from io
import StringIO
37 from struct
import pack
41 from pyasn1
.codec
.der
import encoder
44 class InvalidKeyError(Exception):
45 """Helper exception to handle unknown key types."""
47 def __init__(self
, key
):
51 return 'Invalid key: "%s"' % str(self
.key
)
54 class UnknownSignedEntryType(Exception):
55 """Helper exception to handle unknown SignedEntry types."""
57 def __init__(self
, signedEntry
):
58 self
.signedEntry
= signedEntry
61 return 'Unknown SignedEntry type: "%s"' % str(self
.signedEntry
)
64 class SignedEntry(object):
65 """Base class for CT entries. Use PrecertEntry or
69 class PrecertEntry(SignedEntry
):
70 """Precertificate entry type for SCT."""
72 def __init__(self
, tbsCertificate
, issuerKey
):
73 self
.tbsCertificate
= tbsCertificate
74 self
.issuerKey
= issuerKey
77 class X509Entry(SignedEntry
):
78 """x509 certificate entry type for SCT."""
80 def __init__(self
, certificate
):
81 self
.certificate
= certificate
85 """SCT represents a Signed Certificate Timestamp."""
87 def __init__(self
, key
, date
, signedEntry
):
89 self
.timestamp
= calendar
.timegm(date
.timetuple()) * 1000
90 self
.signedEntry
= signedEntry
93 def signAndEncode(self
):
94 """Returns a signed and encoded representation of the
96 # The signature is over the following data:
97 # sct_version (one 0 byte)
98 # signature_type (one 0 byte)
99 # timestamp (8 bytes, milliseconds since the epoch)
100 # entry_type (two bytes (one 0 byte followed by one 0 byte for
101 # X509Entry or one 1 byte for PrecertEntry)
102 # signed_entry (bytes of X509Entry or PrecertEntry)
103 # extensions (2-byte-length-prefixed, currently empty (so two 0
106 # certificate (3-byte-length-prefixed data)
108 # issuer_key_hash (32 bytes of SHA-256 hash of the issuing
109 # public key, as DER-encoded SPKI)
110 # tbs_certificate (3-byte-length-prefixed data)
111 timestamp
= pack("!Q", self
.timestamp
)
113 if isinstance(self
.signedEntry
, X509Entry
):
114 len_prefix
= pack("!L", len(self
.signedEntry
.certificate
))[1:]
115 entry_with_type
= b
"\0" + len_prefix
+ self
.signedEntry
.certificate
116 elif isinstance(self
.signedEntry
, PrecertEntry
):
117 hasher
= hashlib
.sha256()
119 encoder
.encode(self
.signedEntry
.issuerKey
.asSubjectPublicKeyInfo())
121 issuer_key_hash
= hasher
.digest()
122 len_prefix
= pack("!L", len(self
.signedEntry
.tbsCertificate
))[1:]
124 b
"\1" + issuer_key_hash
+ len_prefix
+ self
.signedEntry
.tbsCertificate
127 raise UnknownSignedEntryType(self
.signedEntry
)
128 data
= b
"\0\0" + timestamp
+ b
"\0" + entry_with_type
+ b
"\0\0"
129 if isinstance(self
.key
, pykey
.ECCKey
):
130 signatureByte
= b
"\3"
131 elif isinstance(self
.key
, pykey
.RSAKey
):
132 signatureByte
= b
"\1"
134 raise InvalidKeyError(self
.key
)
135 # sign returns a hex string like "'<hex bytes>'H", but we want
137 hexSignature
= self
.key
.sign(data
, pykey
.HASH_SHA256
)
138 signature
= bytearray(binascii
.unhexlify(hexSignature
[1:-2]))
140 signature
[-1] = ~signature
[-1] & 0xFF
141 # The actual data returned is the following:
142 # sct_version (one 0 byte)
143 # id (32 bytes of SHA-256 hash of the signing key, as
145 # timestamp (8 bytes, milliseconds since the epoch)
146 # extensions (2-byte-length-prefixed data, currently
148 # hash (one 4 byte representing sha256)
149 # signature (one byte - 1 for RSA and 3 for ECDSA)
150 # signature (2-byte-length-prefixed data)
151 hasher
= hashlib
.sha256()
152 hasher
.update(encoder
.encode(self
.key
.asSubjectPublicKeyInfo()))
153 key_id
= hasher
.digest()
154 signature_len_prefix
= pack("!H", len(signature
))
161 + signature_len_prefix
166 def fromSpecification(specStream
):
167 key
= pykey
.keyFromSpecification("default")
168 certificateSpecification
= StringIO()
169 readingCertificateSpecification
= False
171 for line
in specStream
.readlines():
173 if readingCertificateSpecification
:
174 print(line
, file=certificateSpecification
)
175 elif line
== "certificate:":
176 readingCertificateSpecification
= True
177 elif line
.startswith("key:"):
178 key
= pykey
.keyFromSpecification(line
[len("key:") :])
179 elif line
.startswith("timestamp:"):
180 timestamp
= datetime
.datetime
.strptime(
181 line
[len("timestamp:") :], "%Y%m%d"
183 elif line
== "tamper":
186 raise pycert
.UnknownParameterTypeError(line
)
187 certificateSpecification
.seek(0)
188 certificate
= pycert
.Certificate(certificateSpecification
).toDER()
189 sct
= SCT(key
, timestamp
, X509Entry(certificate
))
194 # The build harness will call this function with an output
195 # file-like object and a path to a file containing an SCT
196 # specification. This will read the specification and output
198 def main(output
, inputPath
):
199 with
open(inputPath
) as configStream
:
200 output
.write(SCT
.fromSpecification(configStream
).signAndEncode())