OLD | NEW |
1 # Copyright 2016 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import argparse | 5 import argparse |
6 import fnmatch | 6 import fnmatch |
7 import glob | 7 import glob |
8 import os | 8 import os |
9 import plistlib | 9 import plistlib |
10 import shutil | 10 import shutil |
11 import subprocess | 11 import subprocess |
12 import sys | 12 import sys |
13 import tempfile | 13 import tempfile |
14 | 14 |
15 | 15 |
16 class InstallationError(Exception): | |
17 """Signals a local installation error that prevents code signing.""" | |
18 | |
19 def __init__(self, fmt, *args): | |
20 super(Exception, self).__init__(fmt % args) | |
21 | |
22 | |
23 def GetProvisioningProfilesDir(): | 16 def GetProvisioningProfilesDir(): |
24 """Returns the location of the installed mobile provisioning profiles. | 17 """Returns the location of the installed mobile provisioning profiles. |
25 | 18 |
26 Returns: | 19 Returns: |
27 The path to the directory containing the installed mobile provisioning | 20 The path to the directory containing the installed mobile provisioning |
28 profiles as a string. | 21 profiles as a string. |
29 """ | 22 """ |
30 return os.path.join( | 23 return os.path.join( |
31 os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') | 24 os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') |
32 | 25 |
(...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
144 | 137 |
145 def LoadDefaults(self, defaults): | 138 def LoadDefaults(self, defaults): |
146 for key, value in defaults.iteritems(): | 139 for key, value in defaults.iteritems(): |
147 if key not in self._data: | 140 if key not in self._data: |
148 self._data[key] = value | 141 self._data[key] = value |
149 | 142 |
150 def WriteTo(self, target_path): | 143 def WriteTo(self, target_path): |
151 plistlib.writePlist(self._data, target_path) | 144 plistlib.writePlist(self._data, target_path) |
152 | 145 |
153 | 146 |
154 def FindProvisioningProfile(bundle_identifier, provisioning_profile_short_name): | 147 def FindProvisioningProfile(bundle_identifier, required): |
155 """Finds mobile provisioning profile to use to sign bundle. | 148 """Finds mobile provisioning profile to use to sign bundle. |
156 | 149 |
157 Args: | 150 Args: |
158 bundle_identifier: the identifier of the bundle to sign. | 151 bundle_identifier: the identifier of the bundle to sign. |
159 provisioning_profile_short_path: optional short name of the mobile | |
160 provisioning profile file to use to sign (will still be checked | |
161 to see if it can sign bundle). | |
162 | 152 |
163 Returns: | 153 Returns: |
164 The ProvisioningProfile object that can be used to sign the Bundle | 154 The ProvisioningProfile object that can be used to sign the Bundle |
165 object or None if no matching provisioning profile was found. | 155 object or None if no matching provisioning profile was found. |
166 """ | 156 """ |
167 provisioning_profiles_dir = GetProvisioningProfilesDir() | 157 provisioning_profile_paths = glob.glob( |
168 | 158 os.path.join(GetProvisioningProfilesDir(), '*.mobileprovision')) |
169 # First check if there is a mobile provisioning profile installed with | |
170 # the requested short name. If this is the case, restrict the search to | |
171 # that mobile provisioning profile, otherwise consider all the installed | |
172 # mobile provisioning profiles. | |
173 provisioning_profile_paths = [] | |
174 if provisioning_profile_short_name: | |
175 provisioning_profile_path = os.path.join( | |
176 provisioning_profiles_dir, | |
177 provisioning_profile_short_name + '.mobileprovision') | |
178 if os.path.isfile(provisioning_profile_path): | |
179 provisioning_profile_paths.append(provisioning_profile_path) | |
180 | |
181 if not provisioning_profile_paths: | |
182 provisioning_profile_paths = glob.glob( | |
183 os.path.join(provisioning_profiles_dir, '*.mobileprovision')) | |
184 | 159 |
185 # Iterate over all installed mobile provisioning profiles and filter those | 160 # Iterate over all installed mobile provisioning profiles and filter those |
186 # that can be used to sign the bundle. | 161 # that can be used to sign the bundle. |
187 valid_provisioning_profiles = [] | 162 valid_provisioning_profiles = [] |
188 for provisioning_profile_path in provisioning_profile_paths: | 163 for provisioning_profile_path in provisioning_profile_paths: |
189 provisioning_profile = ProvisioningProfile(provisioning_profile_path) | 164 provisioning_profile = ProvisioningProfile(provisioning_profile_path) |
190 if provisioning_profile.ValidToSignBundle(bundle_identifier): | 165 if provisioning_profile.ValidToSignBundle(bundle_identifier): |
191 valid_provisioning_profiles.append(provisioning_profile) | 166 valid_provisioning_profiles.append(provisioning_profile) |
192 | 167 |
193 if not valid_provisioning_profiles: | 168 if not valid_provisioning_profiles: |
| 169 if required: |
| 170 sys.stderr.write( |
| 171 'No mobile provisioning profile found for "%s".\n' % |
| 172 bundle_identifier) |
| 173 sys.exit(1) |
194 return None | 174 return None |
195 | 175 |
196 # Select the most specific mobile provisioning profile, i.e. the one with | 176 # Select the most specific mobile provisioning profile, i.e. the one with |
197 # the longest application identifier pattern. | 177 # the longest application identifier pattern. |
198 return max( | 178 return max( |
199 valid_provisioning_profiles, | 179 valid_provisioning_profiles, |
200 key=lambda p: len(p.application_identifier_pattern)) | 180 key=lambda p: len(p.application_identifier_pattern)) |
201 | 181 |
202 | 182 |
203 def InstallFramework(framework_path, bundle, args): | 183 def CodeSignBundle(bundle_path, identity, extra_args): |
| 184 process = subprocess.Popen(['xcrun', 'codesign', '--force', '--sign', |
| 185 identity, '--timestamp=none'] + list(extra_args) + [bundle_path], |
| 186 stderr=subprocess.PIPE) |
| 187 _, stderr = process.communicate() |
| 188 if process.returncode: |
| 189 sys.stderr.write(stderr) |
| 190 sys.exit(process.returncode) |
| 191 for line in stderr.splitlines(): |
| 192 if line.endswith(': replacing existing signature'): |
| 193 # Ignore warning about replacing existing signature as this should only |
| 194 # happen when re-signing system frameworks (and then it is expected). |
| 195 continue |
| 196 sys.stderr.write(line) |
| 197 sys.stderr.write('\n') |
| 198 |
| 199 |
| 200 def InstallSystemFramework(framework_path, bundle_path, args): |
204 """Install framework from |framework_path| to |bundle| and code-re-sign it.""" | 201 """Install framework from |framework_path| to |bundle| and code-re-sign it.""" |
205 installed_framework_path = os.path.join( | 202 installed_framework_path = os.path.join( |
206 bundle.path, 'Frameworks', os.path.basename(framework_path)) | 203 bundle_path, 'Frameworks', os.path.basename(framework_path)) |
207 | 204 |
208 if os.path.exists(installed_framework_path): | 205 if os.path.exists(installed_framework_path): |
209 shutil.rmtree(installed_framework_path) | 206 shutil.rmtree(installed_framework_path) |
210 | 207 |
211 shutil.copytree(framework_path, installed_framework_path) | 208 shutil.copytree(framework_path, installed_framework_path) |
212 | 209 CodeSignBundle(installed_framework_path, args.identity, |
213 framework_bundle = Bundle(installed_framework_path) | 210 ['--deep', '--preserve-metadata=identifier,entitlements']) |
214 CodeSignBundle(framework_bundle.binary_path, framework_bundle, args, True) | |
215 | 211 |
216 | 212 |
217 def GenerateEntitlements(path, provisioning_profile, bundle_identifier): | 213 def GenerateEntitlements(path, provisioning_profile, bundle_identifier): |
218 """Generates an entitlements file. | 214 """Generates an entitlements file. |
219 | 215 |
220 Args: | 216 Args: |
221 path: path to the entitlements template file | 217 path: path to the entitlements template file |
222 provisioning_profile: ProvisioningProfile object to use, may be None | 218 provisioning_profile: ProvisioningProfile object to use, may be None |
223 bundle_identifier: identifier of the bundle to sign. | 219 bundle_identifier: identifier of the bundle to sign. |
224 """ | 220 """ |
225 entitlements = Entitlements(path) | 221 entitlements = Entitlements(path) |
226 if provisioning_profile: | 222 if provisioning_profile: |
227 entitlements.LoadDefaults(provisioning_profile.entitlements) | 223 entitlements.LoadDefaults(provisioning_profile.entitlements) |
228 app_identifier_prefix = provisioning_profile.team_identifier + '.' | 224 app_identifier_prefix = provisioning_profile.team_identifier + '.' |
229 else: | 225 else: |
230 app_identifier_prefix = '*.' | 226 app_identifier_prefix = '*.' |
231 entitlements.ExpandVariables({ | 227 entitlements.ExpandVariables({ |
232 'CFBundleIdentifier': bundle_identifier, | 228 'CFBundleIdentifier': bundle_identifier, |
233 'AppIdentifierPrefix': app_identifier_prefix, | 229 'AppIdentifierPrefix': app_identifier_prefix, |
234 }) | 230 }) |
235 return entitlements | 231 return entitlements |
236 | 232 |
237 | 233 |
238 def CodeSignBundle(binary, bundle, args, preserve=False): | 234 class Action(object): |
239 """Cryptographically signs bundle. | |
240 | 235 |
241 Args: | 236 """Class implementing one action supported by the script.""" |
242 bundle: the Bundle object to sign. | |
243 args: a dictionary with configuration settings for the code signature, | |
244 need to define 'entitlements_path', 'provisioning_profile_short_name', | |
245 'deep_signature' and 'identify' keys. | |
246 """ | |
247 provisioning_profile = FindProvisioningProfile( | |
248 bundle.identifier, args.provisioning_profile_short_name) | |
249 if provisioning_profile: | |
250 provisioning_profile.Install(bundle) | |
251 else: | |
252 sys.stderr.write( | |
253 'Warning: no mobile provisioning profile found for "%s", some features ' | |
254 'may not be functioning properly.\n' % bundle.identifier) | |
255 | 237 |
256 if preserve: | 238 @classmethod |
257 process = subprocess.Popen([ | 239 def Register(cls, subparsers): |
258 'xcrun', 'codesign', '--force', | 240 parser = subparsers.add_parser(cls.name, help=cls.help) |
259 '--sign', args.identity if args.identity else '-', | 241 parser.set_defaults(func=cls._Execute) |
260 '--deep', '--preserve-metadata=identifier,entitlements', | 242 cls._Register(parser) |
261 '--timestamp=none', bundle.path], stderr=subprocess.PIPE) | 243 |
262 _, stderr = process.communicate() | 244 |
263 if process.returncode: | 245 class CodeSignBundleAction(Action): |
264 sys.stderr.write(stderr) | 246 |
265 sys.exit(process.returncode) | 247 """Class implementing the code-sign-bundle action.""" |
266 for line in stderr.splitlines(): | 248 |
267 # Ignore expected warning as we are replacing the signature on purpose. | 249 name = 'code-sign-bundle' |
268 if not line.endswith(': replacing existing signature'): | 250 help = 'perform code signature for a bundle' |
269 sys.stderr.write(line + '\n') | 251 |
270 else: | 252 @staticmethod |
271 signature_file = os.path.join( | 253 def _Register(parser): |
272 bundle.path, '_CodeSignature', 'CodeResources') | 254 parser.add_argument( |
| 255 '--entitlements', '-e', dest='entitlements_path', |
| 256 help='path to the entitlements file to use') |
| 257 parser.add_argument( |
| 258 'path', help='path to the iOS bundle to codesign') |
| 259 parser.add_argument( |
| 260 '--identity', '-i', required=True, |
| 261 help='identity to use to codesign') |
| 262 parser.add_argument( |
| 263 '--binary', '-b', required=True, |
| 264 help='path to the iOS bundle binary') |
| 265 parser.add_argument( |
| 266 '--framework', '-F', action='append', default=[], dest="frameworks", |
| 267 help='install and resign system framework') |
| 268 |
| 269 @staticmethod |
| 270 def _Execute(args): |
| 271 if not args.identity: |
| 272 args.identity = '-' |
| 273 |
| 274 bundle = Bundle(args.path) |
| 275 |
| 276 # Find mobile provisioning profile and embeds it into the bundle (if a code |
| 277 # signing identify has been provided, fails if no valid mobile provisioning |
| 278 # is found). |
| 279 provisioning_profile_required = args.identity != '-' |
| 280 provisioning_profile = FindProvisioningProfile( |
| 281 bundle.identifier, provisioning_profile_required) |
| 282 if provisioning_profile: |
| 283 provisioning_profile.Install(bundle) |
| 284 |
| 285 # Delete existing code signature. |
| 286 signature_file = os.path.join(args.path, '_CodeSignature', 'CodeResources') |
273 if os.path.isfile(signature_file): | 287 if os.path.isfile(signature_file): |
274 os.unlink(signature_file) | 288 os.unlink(signature_file) |
275 | 289 |
| 290 # Install system frameworks if requested. |
| 291 for framework_path in args.frameworks: |
| 292 InstallSystemFramework(framework_path, args.path, args) |
| 293 |
| 294 # Copy main binary into bundle. |
276 if os.path.isfile(bundle.binary_path): | 295 if os.path.isfile(bundle.binary_path): |
277 os.unlink(bundle.binary_path) | 296 os.unlink(bundle.binary_path) |
278 shutil.copy(binary, bundle.binary_path) | 297 shutil.copy(args.binary, bundle.binary_path) |
279 | 298 |
280 codesign_command = [ | 299 # Embeds entitlements into the code signature (if code signing identify has |
281 'xcrun', 'codesign', '--force', '--sign', | 300 # been provided). |
282 args.identity if args.identity else '-', | 301 codesign_extra_args = [] |
283 '--timestamp=none', bundle.path, | 302 if provisioning_profile: |
284 ] | 303 temporary_entitlements_file = tempfile.NamedTemporaryFile(suffix='.xcent') |
| 304 codesign_extra_args.extend( |
| 305 ['--entitlements', temporary_entitlements_file.name]) |
285 | 306 |
286 with tempfile.NamedTemporaryFile(suffix='.xcent') as temporary_file_path: | 307 entitlements = GenerateEntitlements( |
287 if provisioning_profile and args.identity: | 308 args.entitlements_path, provisioning_profile, bundle.identifier) |
288 entitlements = GenerateEntitlements( | 309 entitlements.WriteTo(temporary_entitlements_file.name) |
289 args.entitlements_path, provisioning_profile, bundle.identifier) | 310 |
290 entitlements.WriteTo(temporary_file_path.name) | 311 CodeSignBundle(bundle.path, args.identity, codesign_extra_args) |
291 codesign_command.extend(['--entitlements', temporary_file_path.name]) | |
292 subprocess.check_call(codesign_command) | |
293 | 312 |
294 | 313 |
295 def MainCodeSignBundle(args): | 314 class GenerateEntitlementsAction(Action): |
296 """Adapter to call CodeSignBundle from Main.""" | |
297 bundle = Bundle(args.path) | |
298 for framework in args.frameworks: | |
299 InstallFramework(framework, bundle, args) | |
300 CodeSignBundle(args.binary, bundle, args) | |
301 | 315 |
| 316 """Class implementing the generate-entitlements action.""" |
302 | 317 |
303 def MainGenerateEntitlements(args): | 318 name = 'generate-entitlements' |
304 """Adapter to call GenerateEntitlements from Main.""" | 319 help = 'generate entitlements file' |
305 info_plist = LoadPlistFile(args.info_plist) | |
306 bundle_identifier = info_plist['CFBundleIdentifier'] | |
307 provisioning_profile = FindProvisioningProfile( | |
308 bundle_identifier, args.provisioning_profile_short_name) | |
309 | 320 |
310 entitlements = GenerateEntitlements( | 321 @staticmethod |
311 args.entitlements_path, provisioning_profile, bundle_identifier) | 322 def _Register(parser): |
312 entitlements.WriteTo(args.path) | 323 parser.add_argument( |
| 324 '--entitlements', '-e', dest='entitlements_path', |
| 325 help='path to the entitlements file to use') |
| 326 parser.add_argument( |
| 327 'path', help='path to the entitlements file to generate') |
| 328 parser.add_argument( |
| 329 '--info-plist', '-p', required=True, |
| 330 help='path to the bundle Info.plist') |
| 331 |
| 332 @staticmethod |
| 333 def _Execute(args): |
| 334 info_plist = LoadPlistFile(args.info_plist) |
| 335 bundle_identifier = info_plist['CFBundleIdentifier'] |
| 336 provisioning_profile = FindProvisioningProfile(bundle_identifier, False) |
| 337 entitlements = GenerateEntitlements( |
| 338 args.entitlements_path, provisioning_profile, bundle_identifier) |
| 339 entitlements.WriteTo(args.path) |
313 | 340 |
314 | 341 |
315 def Main(): | 342 def Main(): |
316 parser = argparse.ArgumentParser('codesign iOS bundles') | 343 parser = argparse.ArgumentParser('codesign iOS bundles') |
317 parser.add_argument( | |
318 '--provisioning-profile', '-p', dest='provisioning_profile_short_name', | |
319 help='short name of the mobile provisioning profile to use (' | |
320 'if undefined, will autodetect the mobile provisioning ' | |
321 'to use)') | |
322 parser.add_argument( | |
323 '--entitlements', '-e', dest='entitlements_path', | |
324 help='path to the entitlements file to use') | |
325 subparsers = parser.add_subparsers() | 344 subparsers = parser.add_subparsers() |
326 | 345 |
327 code_sign_bundle_parser = subparsers.add_parser( | 346 for action in [ CodeSignBundleAction, GenerateEntitlementsAction ]: |
328 'code-sign-bundle', | 347 action.Register(subparsers) |
329 help='code sign a bundle') | |
330 code_sign_bundle_parser.add_argument( | |
331 'path', help='path to the iOS bundle to codesign') | |
332 code_sign_bundle_parser.add_argument( | |
333 '--identity', '-i', required=True, | |
334 help='identity to use to codesign') | |
335 code_sign_bundle_parser.add_argument( | |
336 '--binary', '-b', required=True, | |
337 help='path to the iOS bundle binary') | |
338 code_sign_bundle_parser.add_argument( | |
339 '--framework', '-F', action='append', default=[], dest="frameworks", | |
340 help='install and resign system framework') | |
341 code_sign_bundle_parser.set_defaults(func=MainCodeSignBundle) | |
342 | |
343 generate_entitlements_parser = subparsers.add_parser( | |
344 'generate-entitlements', | |
345 help='generate entitlements file') | |
346 generate_entitlements_parser.add_argument( | |
347 'path', help='path to the entitlements file to generate') | |
348 generate_entitlements_parser.add_argument( | |
349 '--info-plist', '-p', required=True, | |
350 help='path to the bundle Info.plist') | |
351 generate_entitlements_parser.set_defaults( | |
352 func=MainGenerateEntitlements) | |
353 | 348 |
354 args = parser.parse_args() | 349 args = parser.parse_args() |
355 args.func(args) | 350 args.func(args) |
356 | 351 |
357 | 352 |
358 if __name__ == '__main__': | 353 if __name__ == '__main__': |
359 sys.exit(Main()) | 354 sys.exit(Main()) |
OLD | NEW |