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