Bug 1940967 - Vendor glean_parser v16.2.0 r=TravisLong,mach-reviewers,ahal
[gecko.git] / security / manager / ssl / tests / unit / sign_app.py
blob012a3721740a111ee26bc25484475f8188477e67
1 #!/usr/bin/env python3
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/.
7 """
8 Given a directory of files, packages them up and signs the
9 resulting zip file. Mainly for creating test inputs to the
10 nsIX509CertDB.openSignedAppFileAsync API.
11 """
12 from base64 import b64encode
13 from cbor2 import dumps
14 from cbor2.types import CBORTag
15 from hashlib import sha1, sha256
16 import argparse
17 from io import StringIO
18 import os
19 import re
20 import six
21 import sys
22 import zipfile
24 # These libraries moved to security/manager/tools/ in bug 1699294.
25 sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "tools"))
26 import pycert
27 import pycms
28 import pykey
30 ES256 = -7
31 ES384 = -35
32 ES512 = -36
33 KID = 4
34 ALG = 1
35 COSE_Sign = 98
38 def coseAlgorithmToPykeyHash(algorithm):
39 """Helper function that takes one of (ES256, ES384, ES512)
40 and returns the corresponding pykey.HASH_* identifier."""
41 if algorithm == ES256:
42 return pykey.HASH_SHA256
43 if algorithm == ES384:
44 return pykey.HASH_SHA384
45 if algorithm == ES512:
46 return pykey.HASH_SHA512
47 raise UnknownCOSEAlgorithmError(algorithm)
50 # COSE_Signature = [
51 # protected : serialized_map,
52 # unprotected : {},
53 # signature : bstr
54 # ]
57 def coseSignature(payload, algorithm, signingKey, signingCertificate, bodyProtected):
58 """Returns a COSE_Signature structure.
59 payload is a string representing the data to be signed
60 algorithm is one of (ES256, ES384, ES512)
61 signingKey is a pykey.ECKey to sign the data with
62 signingCertificate is a byte string
63 bodyProtected is the serialized byte string of the protected body header
64 """
65 protected = {ALG: algorithm, KID: signingCertificate}
66 protectedEncoded = dumps(protected)
67 # Sig_structure = [
68 # context : "Signature"
69 # body_protected : bodyProtected
70 # sign_protected : protectedEncoded
71 # external_aad : nil
72 # payload : bstr
73 # ]
74 sigStructure = ["Signature", bodyProtected, protectedEncoded, None, payload]
75 sigStructureEncoded = dumps(sigStructure)
76 pykeyHash = coseAlgorithmToPykeyHash(algorithm)
77 signature = signingKey.signRaw(sigStructureEncoded, pykeyHash)
78 return [protectedEncoded, {}, signature]
81 # COSE_Sign = [
82 # protected : serialized_map,
83 # unprotected : {},
84 # payload : nil,
85 # signatures : [+ COSE_Signature]
86 # ]
89 def coseSig(payload, intermediates, signatures):
90 """Returns the entire (tagged) COSE_Sign structure.
91 payload is a string representing the data to be signed
92 intermediates is an array of byte strings
93 signatures is an array of (algorithm, signingKey,
94 signingCertificate) triplets to be passed to
95 coseSignature
96 """
97 protected = {KID: intermediates}
98 protectedEncoded = dumps(protected)
99 coseSignatures = []
100 for algorithm, signingKey, signingCertificate in signatures:
101 coseSignatures.append(
102 coseSignature(
103 payload, algorithm, signingKey, signingCertificate, protectedEncoded
106 tagged = CBORTag(COSE_Sign, [protectedEncoded, {}, None, coseSignatures])
107 return dumps(tagged)
110 def walkDirectory(directory):
111 """Given a relative path to a directory, enumerates the
112 files in the tree rooted at that location. Returns a list
113 of pairs of paths to those files. The first in each pair
114 is the full path to the file. The second in each pair is
115 the path to the file relative to the directory itself."""
116 paths = []
117 for path, _dirs, files in os.walk(directory):
118 for f in files:
119 fullPath = os.path.join(path, f)
120 internalPath = re.sub(r"^/", "", fullPath.replace(directory, ""))
121 paths.append((fullPath, internalPath))
122 return paths
125 def addManifestEntry(filename, hashes, contents, entries):
126 """Helper function to fill out a manifest entry.
127 Takes the filename, a list of (hash function, hash function name)
128 pairs to use, the contents of the file, and the current list
129 of manifest entries."""
130 entry = "Name: %s\n" % filename
131 for hashFunc, name in hashes:
132 base64hash = b64encode(hashFunc(contents).digest()).decode("ascii")
133 entry += "%s-Digest: %s\n" % (name, base64hash)
134 entries.append(entry)
137 def getCert(subject, keyName, issuerName, ee, issuerKey="", validity=""):
138 """Helper function to create an X509 cert from a specification.
139 Takes the subject, the subject key name to use, the issuer name,
140 a bool whether this is an EE cert or not, and optionally an issuer key
141 name."""
142 certSpecification = (
143 "issuer:%s\n" % issuerName
144 + "subject:"
145 + subject
146 + "\n"
147 + "subjectKey:%s\n" % keyName
149 if ee:
150 certSpecification += "extension:keyUsage:digitalSignature"
151 else:
152 certSpecification += (
153 "extension:basicConstraints:cA,\n"
154 + "extension:keyUsage:cRLSign,keyCertSign"
156 if issuerKey:
157 certSpecification += "\nissuerKey:%s" % issuerKey
158 if validity:
159 certSpecification += "\nvalidity:%s" % validity
160 certSpecificationStream = StringIO()
161 print(certSpecification, file=certSpecificationStream)
162 certSpecificationStream.seek(0)
163 return pycert.Certificate(certSpecificationStream)
166 def coseAlgorithmToSignatureParams(coseAlgorithm, issuerName, issuerKey, certValidity):
167 """Given a COSE algorithm ('ES256', 'ES384', 'ES512'), an issuer
168 name, the name of the issuer's key, and a validity period, returns a
169 (algorithm id, pykey.ECCKey, encoded certificate) triplet for use
170 with coseSig.
172 if coseAlgorithm == "ES256":
173 keyName = "secp256r1"
174 algId = ES256
175 elif coseAlgorithm == "ES384":
176 keyName = "secp384r1"
177 algId = ES384
178 elif coseAlgorithm == "ES512":
179 keyName = "secp521r1" # COSE uses the hash algorithm; this is the curve
180 algId = ES512
181 else:
182 raise UnknownCOSEAlgorithmError(coseAlgorithm)
183 key = pykey.ECCKey(keyName)
184 # The subject must differ to avoid errors when importing into NSS later.
185 ee = getCert(
186 "xpcshell signed app test signer " + keyName,
187 keyName,
188 issuerName,
189 True,
190 issuerKey,
191 certValidity,
193 return (algId, key, ee.toDER())
196 def signZip(
197 appDirectory,
198 outputFile,
199 issuerName,
200 rootName,
201 rootKey,
202 certValidity,
203 manifestHashes,
204 signatureHashes,
205 pkcs7Hashes,
206 coseAlgorithms,
207 emptySignerInfos,
208 headerPaddingFactor,
210 """Given a directory containing the files to package up, an output
211 filename to write to, the name of the issuer of the signing
212 certificate, the name of trust anchor, the name of the trust
213 anchor's key, a list of hash algorithms to use in the manifest file,
214 a similar list for the signature file, a similar list for the pkcs#7
215 signature, a list of COSE signature algorithms to include, whether
216 the pkcs#7 signer info should be kept empty, and how many MB to pad
217 the manifests by (to test handling large manifest files), packages
218 up the files in the directory and creates the output as
219 appropriate."""
220 # The header of each manifest starts with the magic string
221 # 'Manifest-Version: 1.0' and ends with a blank line. There can be
222 # essentially anything after the first line before the blank line.
223 mfEntries = ["Manifest-Version: 1.0"]
224 if headerPaddingFactor > 0:
225 # In this format, each line can only be 72 bytes long. We make
226 # our padding 50 bytes per line (49 of content and one newline)
227 # so the math is easy.
228 singleLinePadding = "a" * 49
229 # 1000000 / 50 = 20000
230 allPadding = [singleLinePadding] * (headerPaddingFactor * 20000)
231 mfEntries.extend(allPadding)
232 # Append the blank line.
233 mfEntries.append("")
235 issuerKey = rootKey
236 with zipfile.ZipFile(outputFile, "w", zipfile.ZIP_DEFLATED) as outZip:
237 for fullPath, internalPath in walkDirectory(appDirectory):
238 with open(fullPath, "rb") as inputFile:
239 contents = inputFile.read()
240 outZip.writestr(internalPath, contents)
242 # Add the entry to the manifest we're building
243 addManifestEntry(internalPath, manifestHashes, contents, mfEntries)
245 if len(coseAlgorithms) > 0:
246 coseManifest = "\n".join(mfEntries)
247 outZip.writestr("META-INF/cose.manifest", coseManifest)
248 coseManifest = six.ensure_binary(coseManifest)
249 addManifestEntry(
250 "META-INF/cose.manifest", manifestHashes, coseManifest, mfEntries
252 intermediates = []
253 coseIssuerName = issuerName
254 if rootName:
255 issuerKey = "default"
256 coseIssuerName = "xpcshell signed app test issuer"
257 intermediate = getCert(
258 coseIssuerName,
259 issuerKey,
260 rootName,
261 False,
262 rootKey,
263 certValidity,
265 intermediate = intermediate.toDER()
266 intermediates.append(intermediate)
267 signatures = [
268 coseAlgorithmToSignatureParams(
269 coseAlgorithm,
270 coseIssuerName,
271 issuerKey,
272 certValidity,
274 for coseAlgorithm in coseAlgorithms
276 coseSignatureBytes = coseSig(coseManifest, intermediates, signatures)
277 outZip.writestr("META-INF/cose.sig", coseSignatureBytes)
278 addManifestEntry(
279 "META-INF/cose.sig", manifestHashes, coseSignatureBytes, mfEntries
282 if len(pkcs7Hashes) != 0 or emptySignerInfos:
283 mfContents = "\n".join(mfEntries)
284 sfContents = "Signature-Version: 1.0\n"
285 for hashFunc, name in signatureHashes:
286 hashed = hashFunc(six.ensure_binary(mfContents)).digest()
287 base64hash = b64encode(hashed).decode("ascii")
288 sfContents += "%s-Digest-Manifest: %s\n" % (name, base64hash)
290 cmsSpecification = ""
291 for name in pkcs7Hashes:
292 hashFunc, _ = hashNameToFunctionAndIdentifier(name)
293 cmsSpecification += "%s:%s\n" % (
294 name,
295 hashFunc(six.ensure_binary(sfContents)).hexdigest(),
297 cmsSpecification += (
298 "signer:\n"
299 + "issuer:%s\n" % issuerName
300 + "subject:xpcshell signed app test signer\n"
301 + "extension:keyUsage:digitalSignature"
303 if issuerKey != "default":
304 cmsSpecification += "\nissuerKey:%s" % issuerKey
305 if certValidity:
306 cmsSpecification += "\nvalidity:%s" % certValidity
307 cmsSpecificationStream = StringIO()
308 print(cmsSpecification, file=cmsSpecificationStream)
309 cmsSpecificationStream.seek(0)
310 cms = pycms.CMS(cmsSpecificationStream)
311 p7 = cms.toDER()
312 outZip.writestr("META-INF/A.RSA", p7)
313 outZip.writestr("META-INF/A.SF", sfContents)
314 outZip.writestr("META-INF/MANIFEST.MF", mfContents)
317 class Error(Exception):
318 """Base class for exceptions in this module."""
320 pass
323 class UnknownHashAlgorithmError(Error):
324 """Helper exception type to handle unknown hash algorithms."""
326 def __init__(self, name):
327 super(UnknownHashAlgorithmError, self).__init__()
328 self.name = name
330 def __str__(self):
331 return "Unknown hash algorithm %s" % repr(self.name)
334 class UnknownCOSEAlgorithmError(Error):
335 """Helper exception type to handle unknown COSE algorithms."""
337 def __init__(self, name):
338 super(UnknownCOSEAlgorithmError, self).__init__()
339 self.name = name
341 def __str__(self):
342 return "Unknown COSE algorithm %s" % repr(self.name)
345 def hashNameToFunctionAndIdentifier(name):
346 if name == "sha1":
347 return (sha1, "SHA1")
348 if name == "sha256":
349 return (sha256, "SHA256")
350 raise UnknownHashAlgorithmError(name)
353 def main(outputFile, appPath, *args):
354 """Main entrypoint. Given an already-opened file-like
355 object, a path to the app directory to sign, and some
356 optional arguments, signs the contents of the directory and
357 writes the resulting package to the 'file'."""
358 parser = argparse.ArgumentParser(description="Sign an app.")
359 parser.add_argument(
360 "-i",
361 "--issuer",
362 action="store",
363 help="Issuer name",
364 default="xpcshell signed apps test root",
366 parser.add_argument("-r", "--root", action="store", help="Root name", default="")
367 parser.add_argument(
368 "-k",
369 "--root-key",
370 action="store",
371 help="Root key name",
372 default="default",
374 parser.add_argument(
375 "--cert-validity",
376 action="store",
377 help="Certificate validity; YYYYMMDD-YYYYMMDD or duration in days",
378 default="",
380 parser.add_argument(
381 "-m",
382 "--manifest-hash",
383 action="append",
384 help="Hash algorithms to use in manifest",
385 default=[],
387 parser.add_argument(
388 "-s",
389 "--signature-hash",
390 action="append",
391 help="Hash algorithms to use in signature file",
392 default=[],
394 parser.add_argument(
395 "-c",
396 "--cose-sign",
397 action="append",
398 help="Append a COSE signature with the given "
399 + "algorithms (out of ES256, ES384, and ES512)",
400 default=[],
402 parser.add_argument(
403 "-z",
404 "--pad-headers",
405 action="store",
406 default=0,
407 help="Pad the header sections of the manifests "
408 + "with X MB of repetitive data",
410 group = parser.add_mutually_exclusive_group()
411 group.add_argument(
412 "-p",
413 "--pkcs7-hash",
414 action="append",
415 help="Hash algorithms to use in PKCS#7 signature",
416 default=[],
418 group.add_argument(
419 "-e",
420 "--empty-signerInfos",
421 action="store_true",
422 help="Emit pkcs#7 SignedData with empty signerInfos",
424 parsed = parser.parse_args(args)
425 if len(parsed.manifest_hash) == 0:
426 parsed.manifest_hash.append("sha256")
427 if len(parsed.signature_hash) == 0:
428 parsed.signature_hash.append("sha256")
429 signZip(
430 appPath,
431 outputFile,
432 parsed.issuer,
433 parsed.root,
434 parsed.root_key,
435 parsed.cert_validity,
436 [hashNameToFunctionAndIdentifier(h) for h in parsed.manifest_hash],
437 [hashNameToFunctionAndIdentifier(h) for h in parsed.signature_hash],
438 parsed.pkcs7_hash,
439 parsed.cose_sign,
440 parsed.empty_signerInfos,
441 int(parsed.pad_headers),