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