Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(644)

Side by Side Diff: build/config/ios/codesign.py

Issue 2060943002: [iOS/GN] Add code signing rules to ios_app_bundle target. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@ios-code-signing-gn
Patch Set: Fix code signing of "test" template Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | build/config/ios/rules.gni » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 import argparse
6 import fnmatch
7 import glob
8 import os
9 import shutil
10 import subprocess
11 import sys
12 import tempfile
13
14 import CoreFoundation
15
16
17 class CoreFoundationError(Exception):
18
Robert Sesek 2016/06/15 15:30:59 nit: Remove the blank lines between class name and
sdefresne 2016/06/15 16:34:41 Done.
19 """Wraps a Core Foundation error as a python exception."""
20
21 def __init__(self, fmt, *args):
22 super(Exception, self).__init__(fmt % args)
23
24
25 class InstallationError(Exception):
26
27 """Signals a local installation error that prevents code signing."""
28
29 def __init__(self, fmt, *args):
30 super(Exception, self).__init__(fmt % args)
31
32
33 def GetProvisioningProfilesDir():
34 """Returns the location of the installed mobile provisioning profiles.
35
36 Returns:
37 The path to the directory containing the installed mobile provisioning
38 profiles as a string.
39 """
40 return os.path.join(
41 os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles')
42
43
44 def LoadPlistFile(plist_path):
45 """Loads property list file at |plist_path|.
46
47 Args:
48 plist_path: path to the property list file to load, can either be any
49 format supported by Core Foundation (currently xml1, binary1).
50
51 Returns:
52 The content of the loaded property list, most likely a dictionary like
53 object (as a Core Foundation wrapped object).
54
55 Raises:
56 CoreFoundationError if Core Foundation returned an error while loading
57 the property list file.
58 """
59 with open(plist_path, 'rb') as plist_file:
60 plist_data = plist_file.read()
61 cfdata = CoreFoundation.CFDataCreate(None, plist_data, len(plist_data))
62 plist, plist_format, error = CoreFoundation.CFPropertyListCreateWithData(
63 None, cfdata, 0, None, None)
64 if error is not None:
65 raise CoreFoundationError('cannot load plist: %s', error)
66 return plist
67
68
69 class Bundle(object):
70
71 """Wraps a bundle."""
72
73 def __init__(self, path, data):
74 self._path = path
75 self._data = data
76
77 @staticmethod
78 def Load(bundle_path):
79 """Loads and wraps a bundle.
80
81 Args:
82 bundle_path: path to the bundle.
83
84 Returns:
85 A Bundle instance with data loaded from the bundle Info.plist property
86 list file.
87 """
88 return Bundle(
89 bundle_path, LoadPlistFile(os.path.join(bundle_path, 'Info.plist')))
90
91 @property
92 def path(self):
93 return self._path
94
95 @property
96 def identifier(self):
97 return self._data['CFBundleIdentifier']
98
99 @property
100 def binary_path(self):
101 return os.path.join(self._path, self._data['CFBundleExecutable'])
102
103
104 class ProvisioningProfile(object):
105
106 """Wraps a mobile provisioning profile file."""
107
108 def __init__(self, path, data):
109 self._path = path
110 self._data = data
111
112 @staticmethod
113 def Load(provisioning_profile_path):
114 """Loads and wraps a mobile provisioning profile file.
115
116 Args:
117 provisioning_profile_path: path to the mobile provisioning profile.
118
119 Returns:
120 A ProvisioningProfile instance with data loaded from the mobile
121 provisioning file.
122 """
123 with tempfile.NamedTemporaryFile() as temporary_file_path:
124 subprocess.check_call([
125 'security', 'cms', '-D',
126 '-i', provisioning_profile_path,
127 '-o', temporary_file_path.name])
128 return ProvisioningProfile(
129 provisioning_profile_path,
130 LoadPlistFile(temporary_file_path.name))
131
132 @property
133 def path(self):
134 return self._path
135
136 @property
137 def application_identifier_pattern(self):
138 return self._data.get('Entitlements', {}).get('application-identifier', '')
139
140 @property
141 def team_identifier(self):
142 return self._data.get('TeamIdentifier', [''])[0]
143
144 @property
145 def entitlements(self):
146 return self._data.get('Entitlements', {})
147
148 def ValidToSignBundle(self, bundle):
149 """Checks whether the provisioning profile can sign bundle_identifier.
150
151 Args:
152 bundle: the Bundle object that needs to be signed.
153
154 Returns:
155 True if the mobile provisioning profile can be used to sign a bundle
156 with the corresponding bundle_identifier, False otherwise.
157 """
158 return fnmatch.fnmatch(
159 '%s.%s' % (self.team_identifier, bundle.identifier),
160 self.application_identifier_pattern)
161
162 def Install(self, bundle):
163 """Copies mobile provisioning profile info the bundle."""
164 installation_path = os.path.join(bundle.path, 'embedded.mobileprovision')
165 shutil.copy2(self.path, installation_path)
166
167
168 class Entitlements(object):
Robert Sesek 2016/06/15 15:30:59 Document?
sdefresne 2016/06/15 16:34:41 Done.
169
170 def __init__(self, path, data):
171 self._path = path
172 self._data = data
173
174 @staticmethod
175 def Load(entitlements_path):
176 return Entitlements(
177 entitlements_path,
178 LoadPlistFile(entitlements_path))
179
180 @property
181 def path(self):
182 return self._path
183
184 def ExpandVariables(self, substitutions):
185 self._data = self._ExpandVariables(self._data, substitutions)
186
187 def _ExpandVariables(self, data, substitutions):
188 if hasattr(data, 'endswith'):
189 for key, substitution in substitutions.iteritems():
190 data = data.replace('$(%s)' % (key,), substitution)
191 return data
192
193 if hasattr(data, 'keys'):
194 copy = CoreFoundation.CFDictionaryCreateMutable(None, 0,
Robert Sesek 2016/06/15 15:30:59 It may be a little bit easier to use plistlib to l
sdefresne 2016/06/15 16:34:41 Changed the code to use plistlib.
195 CoreFoundation.kCFTypeDictionaryKeyCallBacks,
196 CoreFoundation.kCFTypeDictionaryValueCallBacks)
197 for key, value in data.iteritems():
198 copy[key] = self._ExpandVariables(value, substitutions)
199 return copy
200
201 if hasattr(data, 'append'):
202 copy = CoreFoundation.CFArrayCreateMutable(None, 0,
203 CoreFoundation.kCFTypeArrayCallBacks)
204 for value in data:
205 copy.append(self._ExpandVariables(value, substitutions))
206 return copy
207
208 return data
209
210 def LoadDefaults(self, defaults):
211 for key, value in defaults.iteritems():
212 if key not in self._data:
213 self._data[key] = value
214
215 def WriteTo(self, target_path):
216 cfdata, error = CoreFoundation.CFPropertyListCreateData(
217 None, self._data, CoreFoundation.kCFPropertyListXMLFormat_v1_0,
218 0, None)
219 if error is not None:
220 raise CoreFoundationError('cannot write property list as data: %s', error)
221 data = CoreFoundation.CFDataGetBytes(
222 cfdata,
223 CoreFoundation.CFRangeMake(0, CoreFoundation.CFDataGetLength(cfdata)),
224 None)
225 with open(target_path, 'wb') as target_file:
226 target_file.write(data)
227 target_file.flush()
228
229
230 def FindProvisioningProfile(bundle, provisioning_profile_short_name):
231 """Finds mobile provisioning profile to use to sign bundle.
232
233 Args:
234 bundle: the Bundle object to sign.
235 provisioning_profile_short_path: optional short name of the mobile
236 provisioning profile file to use to sign (will still be checked
237 to see if it can sign bundle).
238
239 Returns:
240 The ProvisioningProfile object that can be used to sign the Bundle
241 object.
242
243 Raises:
244 InstallationError if no mobile provisioning profile can be used to
245 sign the Bundle object.
246 """
247 provisioning_profiles_dir = GetProvisioningProfilesDir()
248
249 # First check if there is a mobile provisioning profile installed with
250 # the requested short name. If this is the case, restrict the search to
251 # that mobile provisioning profile, otherwise consider all the installed
252 # mobile provisioning profiles.
253 provisioning_profile_paths = []
254 if provisioning_profile_short_name:
255 provisioning_profile_path = os.path.join(
256 provisioning_profiles_dir,
257 provisioning_profile_short_name + '.mobileprovision')
258 if os.path.isfile(provisioning_profile_path):
259 provisioning_profile_paths.append(provisioning_profile_path)
260
261 if not provisioning_profile_paths:
262 provisioning_profile_paths = glob.glob(
263 os.path.join(provisioning_profiles_dir, '*.mobileprovision'))
264
265 # Iterate over all installed mobile provisioning profiles and filter those
266 # that can be used to sign the bundle.
267 valid_provisioning_profiles = []
268 for provisioning_profile_path in provisioning_profile_paths:
269 provisioning_profile = ProvisioningProfile.Load(provisioning_profile_path)
270 if provisioning_profile.ValidToSignBundle(bundle):
271 valid_provisioning_profiles.append(provisioning_profile)
272
273 if not valid_provisioning_profiles:
274 raise InstallationError(
275 'no mobile provisioning profile for "%s"',
276 bundle.identifier)
277
278 # Select the most specific mobile provisioning profile, i.e. the one with
279 # the longest application identifier pattern.
280 valid_provisioning_profiles.sort(
281 key=lambda p: len(p.application_identifier_pattern))
282 return valid_provisioning_profiles[0]
283
284
285 def CreateEntitlements(bundle, provisioning_profile, entitlements_path):
Robert Sesek 2016/06/15 15:30:59 Why have CreateEntitlements, Entitlements.Load, an
sdefresne 2016/06/15 16:34:41 Removed this method (it was converting a entitleme
286 """Creates entitlements using defaults from provisioning profile.
287
288 Args:
289 bundle: the Bundle object to sign.
290 provisition_profile: the ProvisioningProfile object used to sign.
291 entitlements_path: path to the template to use to generate the bundle
292 entitlements file, needs to be a property list file.
293 """
294 entitlements = Entitlements.Load(entitlements_path)
295 entitlements.ExpandVariables({
296 'CFBundleIdentifier': bundle.identifier,
297 'AppIdentifierPrefix': '%s.' % (provisioning_profile.team_identifier,)
298 })
299 entitlements.LoadDefaults(provisioning_profile.entitlements)
300 return entitlements
301
302
303 def CodeSignBundle(binary, bundle, args):
304 """Cryptographically signs bundle.
305
306 Args:
307 bundle: the Bundle object to sign.
308 args: a dictionary with configuration settings for the code signature,
309 need to define 'entitlements_path', 'provisioning_profile_short_name',
310 'deep_signature' and 'identify' keys.
311 """
312 provisioning_profile = FindProvisioningProfile(
313 bundle, args.provisioning_profile_short_name)
314 provisioning_profile.Install(bundle)
315
316 signature_file = os.path.join(bundle.path, "_CodeSignature", "CodeResources")
Robert Sesek 2016/06/15 15:30:59 nit: Switch to single quotes here for consistency
sdefresne 2016/06/15 16:34:41 Done.
317 if os.path.isfile(signature_file):
318 os.unlink(signature_file)
319
320 shutil.copy(binary, bundle.binary_path)
321
322 command = ['codesign', '--force', '--sign', args.identity, '--timestamp=none']
Robert Sesek 2016/06/15 15:30:59 Should invoke codesign through xcrun.
sdefresne 2016/06/15 16:34:41 Done.
323 if args.preserve:
324 command.extend(['--deep', '--preserve-metadata=identifier,entitlements'])
325 command.append(bundle.path)
326
327 subprocess.check_call(command)
328 else:
329 entitlements = CreateEntitlements(
330 bundle, provisioning_profile, args.entitlements_path)
331 with tempfile.NamedTemporaryFile(suffix='.xcent') as temporary_file_path:
332 entitlements.WriteTo(temporary_file_path.name)
333 command.extend(['--entitlements', temporary_file_path.name])
334 command.append(bundle.path)
335 subprocess.check_call(command)
336
337
338 def Main():
339 parser = argparse.ArgumentParser('codesign iOS bundles')
340 parser.add_argument(
341 'path', help='path to the iOS bundle to codesign')
342 parser.add_argument(
343 '--binary', '-b', required=True,
344 help='path to the iOS bundle binary')
345 parser.add_argument(
346 '--provisioning-profile', '-p', dest='provisioning_profile_short_name',
347 help='short name of the mobile provisioning profile to use ('
348 'if undefined, will autodetect the mobile provisioning '
349 'to use)')
350 parser.add_argument(
351 '--identity', '-i', required=True,
352 help='identity to use to codesign')
353 group = parser.add_mutually_exclusive_group(required=True)
354 group.add_argument(
355 '--entitlements', '-e', dest='entitlements_path',
356 help='path to the entitlements file to use')
357 group.add_argument(
358 '--deep', '-d', action='store_true', default=False, dest='preserve',
359 help='deep signature (default: %(default)s)')
360 args = parser.parse_args()
361
362 CodeSignBundle(args.binary, Bundle.Load(args.path), args)
363
364
365 if __name__ == '__main__':
366 sys.exit(Main())
OLDNEW
« no previous file with comments | « no previous file | build/config/ios/rules.gni » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698