summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/tests/unit/test_signed_apps/gentestfiles/sign_b2g_app.py
blob: 89b385a9b04b57b43eba583ad305ed95c5f7b550 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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())