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())