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 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.
12 from base64
import b64encode
13 from cbor2
import dumps
14 from cbor2
.types
import CBORTag
15 from hashlib
import sha1
, sha256
17 from io
import StringIO
24 # These libraries moved to security/manager/tools/ in bug 1699294.
25 sys
.path
.insert(0, os
.path
.join(os
.path
.dirname(__file__
), "..", "..", "..", "tools"))
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
)
51 # protected : serialized_map,
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
65 protected
= {ALG
: algorithm
, KID
: signingCertificate
}
66 protectedEncoded
= dumps(protected
)
68 # context : "Signature"
69 # body_protected : bodyProtected
70 # sign_protected : protectedEncoded
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
]
82 # protected : serialized_map,
85 # signatures : [+ COSE_Signature]
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
97 protected
= {KID
: intermediates
}
98 protectedEncoded
= dumps(protected
)
100 for algorithm
, signingKey
, signingCertificate
in signatures
:
101 coseSignatures
.append(
103 payload
, algorithm
, signingKey
, signingCertificate
, protectedEncoded
106 tagged
= CBORTag(COSE_Sign
, [protectedEncoded
, {}, None, coseSignatures
])
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."""
117 for path
, _dirs
, files
in os
.walk(directory
):
119 fullPath
= os
.path
.join(path
, f
)
120 internalPath
= re
.sub(r
"^/", "", fullPath
.replace(directory
, ""))
121 paths
.append((fullPath
, internalPath
))
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
142 certSpecification
= (
143 "issuer:%s\n" % issuerName
147 + "subjectKey:%s\n" % keyName
150 certSpecification
+= "extension:keyUsage:digitalSignature"
152 certSpecification
+= (
153 "extension:basicConstraints:cA,\n"
154 + "extension:keyUsage:cRLSign,keyCertSign"
157 certSpecification
+= "\nissuerKey:%s" % issuerKey
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
172 if coseAlgorithm
== "ES256":
173 keyName
= "secp256r1"
175 elif coseAlgorithm
== "ES384":
176 keyName
= "secp384r1"
178 elif coseAlgorithm
== "ES512":
179 keyName
= "secp521r1" # COSE uses the hash algorithm; this is the curve
182 raise UnknownCOSEAlgorithmError(coseAlgorithm
)
183 key
= pykey
.ECCKey(keyName
)
184 # The subject must differ to avoid errors when importing into NSS later.
186 "xpcshell signed app test signer " + keyName
,
193 return (algId
, key
, ee
.toDER())
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
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.
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
)
250 "META-INF/cose.manifest", manifestHashes
, coseManifest
, mfEntries
253 coseIssuerName
= issuerName
255 issuerKey
= "default"
256 coseIssuerName
= "xpcshell signed app test issuer"
257 intermediate
= getCert(
265 intermediate
= intermediate
.toDER()
266 intermediates
.append(intermediate
)
268 coseAlgorithmToSignatureParams(
274 for coseAlgorithm
in coseAlgorithms
276 coseSignatureBytes
= coseSig(coseManifest
, intermediates
, signatures
)
277 outZip
.writestr("META-INF/cose.sig", coseSignatureBytes
)
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" % (
295 hashFunc(six
.ensure_binary(sfContents
)).hexdigest(),
297 cmsSpecification
+= (
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
306 cmsSpecification
+= "\nvalidity:%s" % certValidity
307 cmsSpecificationStream
= StringIO()
308 print(cmsSpecification
, file=cmsSpecificationStream
)
309 cmsSpecificationStream
.seek(0)
310 cms
= pycms
.CMS(cmsSpecificationStream
)
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."""
323 class UnknownHashAlgorithmError(Error
):
324 """Helper exception type to handle unknown hash algorithms."""
326 def __init__(self
, name
):
327 super(UnknownHashAlgorithmError
, self
).__init
__()
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
__()
342 return "Unknown COSE algorithm %s" % repr(self
.name
)
345 def hashNameToFunctionAndIdentifier(name
):
347 return (sha1
, "SHA1")
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.")
364 default
="xpcshell signed apps test root",
366 parser
.add_argument("-r", "--root", action
="store", help="Root name", default
="")
371 help="Root key name",
377 help="Certificate validity; YYYYMMDD-YYYYMMDD or duration in days",
384 help="Hash algorithms to use in manifest",
391 help="Hash algorithms to use in signature file",
398 help="Append a COSE signature with the given "
399 + "algorithms (out of ES256, ES384, and ES512)",
407 help="Pad the header sections of the manifests "
408 + "with X MB of repetitive data",
410 group
= parser
.add_mutually_exclusive_group()
415 help="Hash algorithms to use in PKCS#7 signature",
420 "--empty-signerInfos",
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")
435 parsed
.cert_validity
,
436 [hashNameToFunctionAndIdentifier(h
) for h
in parsed
.manifest_hash
],
437 [hashNameToFunctionAndIdentifier(h
) for h
in parsed
.signature_hash
],
440 parsed
.empty_signerInfos
,
441 int(parsed
.pad_headers
),