diff options
Diffstat (limited to 'security/manager/ssl/tests/unit/test_signed_apps/gentestfiles/sign_b2g_app.py')
-rw-r--r-- | security/manager/ssl/tests/unit/test_signed_apps/gentestfiles/sign_b2g_app.py | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/security/manager/ssl/tests/unit/test_signed_apps/gentestfiles/sign_b2g_app.py b/security/manager/ssl/tests/unit/test_signed_apps/gentestfiles/sign_b2g_app.py new file mode 100644 index 000000000..89b385a9b --- /dev/null +++ b/security/manager/ssl/tests/unit/test_signed_apps/gentestfiles/sign_b2g_app.py @@ -0,0 +1,174 @@ +import argparse +from base64 import b64encode +from hashlib import sha1 +import sys +import zipfile +import ctypes + +import nss_ctypes + +# Change the limits in JarSignatureVerification.cpp when you change the limits +# here. +max_entry_uncompressed_len = 100 * 1024 * 1024 +max_total_uncompressed_len = 500 * 1024 * 1024 +max_entry_count = 100 * 1000 +max_entry_filename_len = 1024 +max_mf_len = max_entry_count * 50 +max_sf_len = 1024 + + + +def nss_load_cert(nss_db_dir, nss_password, cert_nickname): + nss_ctypes.NSS_Init(nss_db_dir) + try: + wincx = nss_ctypes.SetPasswordContext(nss_password) + cert = nss_ctypes.PK11_FindCertFromNickname(cert_nickname, wincx) + return (wincx, cert) + except: + nss_ctypes.NSS_Shutdown() + raise + +def nss_create_detached_signature(cert, dataToSign, wincx): + certdb = nss_ctypes.CERT_GetDefaultCertDB() + p7 = nss_ctypes.SEC_PKCS7CreateSignedData(cert, + nss_ctypes.certUsageObjectSigner, + certdb, + nss_ctypes.SEC_OID_SHA1, + sha1(dataToSign).digest(), + wincx ) + try: + nss_ctypes.SEC_PKCS7AddSigningTime(p7) + nss_ctypes.SEC_PKCS7IncludeCertChain(p7, wincx) + return nss_ctypes.SEC_PKCS7Encode(p7, None, wincx) + finally: + nss_ctypes.SEC_PKCS7DestroyContentInfo(p7) + +# We receive a ids_json string for the toBeSigned app +def sign_zip(in_zipfile_name, out_zipfile_name, cert, wincx, ids_json): + mf_entries = [] + seen_entries = set() + + total_uncompressed_len = 0 + entry_count = 0 + with zipfile.ZipFile(out_zipfile_name, 'w') as out_zip: + with zipfile.ZipFile(in_zipfile_name, 'r') as in_zip: + for entry_info in in_zip.infolist(): + name = entry_info.filename + + # Check for reserved and/or insane (potentially malicious) names + if name.endswith("/"): + pass + # Do nothing; we don't copy directory entries since they are just a + # waste of space. + elif name.lower().startswith("meta-inf/"): + # META-INF/* is reserved for our use + raise ValueError("META-INF entries are not allowed: %s" % (name)) + elif len(name) > max_entry_filename_len: + raise ValueError("Entry's filename is too long: %s" % (name)) + # TODO: elif name has invalid characters... + elif name in seen_entries: + # It is possible for a zipfile to have duplicate entries (with the exact + # same filenames). Python's zipfile module accepts them, but our zip + # reader in Gecko cannot do anything useful with them, and there's no + # sane reason for duplicate entries to exist, so reject them. + raise ValueError("Duplicate entry in input file: %s" % (name)) + else: + entry_count += 1 + if entry_count > max_entry_count: + raise ValueError("Too many entries in input archive") + + seen_entries.add(name) + + # Read in the input entry, but be careful to avoid going over the + # various limits we have, to minimize the likelihood that we'll run + # out of memory. Note that we can't use the length from entry_info + # because that might not be accurate if the input zip file is + # maliciously crafted to contain misleading metadata. + with in_zip.open(name, 'r') as entry_file: + contents = entry_file.read(max_entry_uncompressed_len + 1) + if len(contents) > max_entry_uncompressed_len: + raise ValueError("Entry is too large: %s" % (name)) + total_uncompressed_len += len(contents) + if total_uncompressed_len > max_total_uncompressed_len: + raise ValueError("Input archive is too large") + + # Copy the entry, using the same compression as used in the input file + out_zip.writestr(entry_info, contents) + + # Add the entry to the manifest we're building + mf_entries.append('Name: %s\nSHA1-Digest: %s\n' + % (name, b64encode(sha1(contents).digest()))) + if (ids_json): + mf_entries.append('Name: %s\nSHA1-Digest: %s\n' + % ("META-INF/ids.json", b64encode(sha1(ids_json).digest()))) + + mf_contents = 'Manifest-Version: 1.0\n\n' + '\n'.join(mf_entries) + if len(mf_contents) > max_mf_len: + raise ValueError("Generated MANIFEST.MF is too large: %d" % (len(mf_contents))) + + sf_contents = ('Signature-Version: 1.0\nSHA1-Digest-Manifest: %s\n' + % (b64encode(sha1(mf_contents).digest()))) + if len(sf_contents) > max_sf_len: + raise ValueError("Generated SIGNATURE.SF is too large: %d" + % (len(mf_contents))) + + p7 = nss_create_detached_signature(cert, sf_contents, wincx) + + # write the signature, SF, and MF + out_zip.writestr("META-INF/A.RSA", p7, zipfile.ZIP_DEFLATED) + out_zip.writestr("META-INF/A.SF", sf_contents, zipfile.ZIP_DEFLATED) + out_zip.writestr("META-INF/MANIFEST.MF", mf_contents, zipfile.ZIP_DEFLATED) + if (ids_json): + out_zip.writestr("META-INF/ids.json", ids_json, zipfile.ZIP_DEFLATED) + +def main(): + parser = argparse.ArgumentParser(description='Sign a B2G app.') + parser.add_argument('-d', action='store', + required=True, help='NSS database directory') + parser.add_argument('-f', action='store', + type=argparse.FileType('rb'), + required=True, help='password file') + parser.add_argument('-k', action='store', + required=True, help="nickname of signing cert.") + parser.add_argument('-i', action='store', type=argparse.FileType('rb'), + required=True, help="input JAR file (unsigned)") + parser.add_argument('-o', action='store', type=argparse.FileType('wb'), + required=True, help="output JAR file (signed)") + parser.add_argument('-I', '--ids-file', action='store', type=argparse.FileType('rb'), + help="Path to the ids.json file", dest='I') + parser.add_argument('-S', '--storeId', action='store', + help="Store Id for the package", dest='S') + parser.add_argument('-V', '--storeVersion', action='store', type=int, + help="Package Version", dest='V') + args = parser.parse_args() + + # Sadly nested groups and neccesarily inclusive groups (http://bugs.python.org/issue11588) + # are not implemented. Note that this means the automatic help is slighty incorrect + if not((not args.I and args.V and args.S) or (args.I and not args.V and not args.S)): + raise ValueError("Either -I or -S and -V must be specified") + + if (args.I): + ids_contents = args.I.read(max_entry_uncompressed_len+1) + else: + ids_contents = '''{ + "id": "%(id)s", + "version": %(version)d +} +''' % {"id": args.S, "version": args.V} + if len(ids_contents) > max_entry_uncompressed_len: + raise ValueError("Entry is too large: %s" % (name)) + + db_dir = args.d + password = args.f.readline().strip() + cert_nickname = args.k + + (wincx, cert) = nss_load_cert(db_dir, password, cert_nickname) + try: + sign_zip(args.i, args.o, cert, wincx, ids_contents) + return 0 + finally: + nss_ctypes.CERT_DestroyCertificate(cert) + nss_ctypes.NSS_Shutdown() + +if __name__ == "__main__": + sys.exit(main()) |