Chromium Code Reviews| Index: build/config/ios/codesign.py |
| diff --git a/build/config/ios/codesign.py b/build/config/ios/codesign.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..e2358965798ac2ba3c5346f8caf217d02e63420f |
| --- /dev/null |
| +++ b/build/config/ios/codesign.py |
| @@ -0,0 +1,366 @@ |
| +# Copyright 2016 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +import argparse |
| +import fnmatch |
| +import glob |
| +import os |
| +import shutil |
| +import subprocess |
| +import sys |
| +import tempfile |
| + |
| +import CoreFoundation |
| + |
| + |
| +class CoreFoundationError(Exception): |
| + |
|
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.
|
| + """Wraps a Core Foundation error as a python exception.""" |
| + |
| + def __init__(self, fmt, *args): |
| + super(Exception, self).__init__(fmt % args) |
| + |
| + |
| +class InstallationError(Exception): |
| + |
| + """Signals a local installation error that prevents code signing.""" |
| + |
| + def __init__(self, fmt, *args): |
| + super(Exception, self).__init__(fmt % args) |
| + |
| + |
| +def GetProvisioningProfilesDir(): |
| + """Returns the location of the installed mobile provisioning profiles. |
| + |
| + Returns: |
| + The path to the directory containing the installed mobile provisioning |
| + profiles as a string. |
| + """ |
| + return os.path.join( |
| + os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') |
| + |
| + |
| +def LoadPlistFile(plist_path): |
| + """Loads property list file at |plist_path|. |
| + |
| + Args: |
| + plist_path: path to the property list file to load, can either be any |
| + format supported by Core Foundation (currently xml1, binary1). |
| + |
| + Returns: |
| + The content of the loaded property list, most likely a dictionary like |
| + object (as a Core Foundation wrapped object). |
| + |
| + Raises: |
| + CoreFoundationError if Core Foundation returned an error while loading |
| + the property list file. |
| + """ |
| + with open(plist_path, 'rb') as plist_file: |
| + plist_data = plist_file.read() |
| + cfdata = CoreFoundation.CFDataCreate(None, plist_data, len(plist_data)) |
| + plist, plist_format, error = CoreFoundation.CFPropertyListCreateWithData( |
| + None, cfdata, 0, None, None) |
| + if error is not None: |
| + raise CoreFoundationError('cannot load plist: %s', error) |
| + return plist |
| + |
| + |
| +class Bundle(object): |
| + |
| + """Wraps a bundle.""" |
| + |
| + def __init__(self, path, data): |
| + self._path = path |
| + self._data = data |
| + |
| + @staticmethod |
| + def Load(bundle_path): |
| + """Loads and wraps a bundle. |
| + |
| + Args: |
| + bundle_path: path to the bundle. |
| + |
| + Returns: |
| + A Bundle instance with data loaded from the bundle Info.plist property |
| + list file. |
| + """ |
| + return Bundle( |
| + bundle_path, LoadPlistFile(os.path.join(bundle_path, 'Info.plist'))) |
| + |
| + @property |
| + def path(self): |
| + return self._path |
| + |
| + @property |
| + def identifier(self): |
| + return self._data['CFBundleIdentifier'] |
| + |
| + @property |
| + def binary_path(self): |
| + return os.path.join(self._path, self._data['CFBundleExecutable']) |
| + |
| + |
| +class ProvisioningProfile(object): |
| + |
| + """Wraps a mobile provisioning profile file.""" |
| + |
| + def __init__(self, path, data): |
| + self._path = path |
| + self._data = data |
| + |
| + @staticmethod |
| + def Load(provisioning_profile_path): |
| + """Loads and wraps a mobile provisioning profile file. |
| + |
| + Args: |
| + provisioning_profile_path: path to the mobile provisioning profile. |
| + |
| + Returns: |
| + A ProvisioningProfile instance with data loaded from the mobile |
| + provisioning file. |
| + """ |
| + with tempfile.NamedTemporaryFile() as temporary_file_path: |
| + subprocess.check_call([ |
| + 'security', 'cms', '-D', |
| + '-i', provisioning_profile_path, |
| + '-o', temporary_file_path.name]) |
| + return ProvisioningProfile( |
| + provisioning_profile_path, |
| + LoadPlistFile(temporary_file_path.name)) |
| + |
| + @property |
| + def path(self): |
| + return self._path |
| + |
| + @property |
| + def application_identifier_pattern(self): |
| + return self._data.get('Entitlements', {}).get('application-identifier', '') |
| + |
| + @property |
| + def team_identifier(self): |
| + return self._data.get('TeamIdentifier', [''])[0] |
| + |
| + @property |
| + def entitlements(self): |
| + return self._data.get('Entitlements', {}) |
| + |
| + def ValidToSignBundle(self, bundle): |
| + """Checks whether the provisioning profile can sign bundle_identifier. |
| + |
| + Args: |
| + bundle: the Bundle object that needs to be signed. |
| + |
| + Returns: |
| + True if the mobile provisioning profile can be used to sign a bundle |
| + with the corresponding bundle_identifier, False otherwise. |
| + """ |
| + return fnmatch.fnmatch( |
| + '%s.%s' % (self.team_identifier, bundle.identifier), |
| + self.application_identifier_pattern) |
| + |
| + def Install(self, bundle): |
| + """Copies mobile provisioning profile info the bundle.""" |
| + installation_path = os.path.join(bundle.path, 'embedded.mobileprovision') |
| + shutil.copy2(self.path, installation_path) |
| + |
| + |
| +class Entitlements(object): |
|
Robert Sesek
2016/06/15 15:30:59
Document?
sdefresne
2016/06/15 16:34:41
Done.
|
| + |
| + def __init__(self, path, data): |
| + self._path = path |
| + self._data = data |
| + |
| + @staticmethod |
| + def Load(entitlements_path): |
| + return Entitlements( |
| + entitlements_path, |
| + LoadPlistFile(entitlements_path)) |
| + |
| + @property |
| + def path(self): |
| + return self._path |
| + |
| + def ExpandVariables(self, substitutions): |
| + self._data = self._ExpandVariables(self._data, substitutions) |
| + |
| + def _ExpandVariables(self, data, substitutions): |
| + if hasattr(data, 'endswith'): |
| + for key, substitution in substitutions.iteritems(): |
| + data = data.replace('$(%s)' % (key,), substitution) |
| + return data |
| + |
| + if hasattr(data, 'keys'): |
| + 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.
|
| + CoreFoundation.kCFTypeDictionaryKeyCallBacks, |
| + CoreFoundation.kCFTypeDictionaryValueCallBacks) |
| + for key, value in data.iteritems(): |
| + copy[key] = self._ExpandVariables(value, substitutions) |
| + return copy |
| + |
| + if hasattr(data, 'append'): |
| + copy = CoreFoundation.CFArrayCreateMutable(None, 0, |
| + CoreFoundation.kCFTypeArrayCallBacks) |
| + for value in data: |
| + copy.append(self._ExpandVariables(value, substitutions)) |
| + return copy |
| + |
| + return data |
| + |
| + def LoadDefaults(self, defaults): |
| + for key, value in defaults.iteritems(): |
| + if key not in self._data: |
| + self._data[key] = value |
| + |
| + def WriteTo(self, target_path): |
| + cfdata, error = CoreFoundation.CFPropertyListCreateData( |
| + None, self._data, CoreFoundation.kCFPropertyListXMLFormat_v1_0, |
| + 0, None) |
| + if error is not None: |
| + raise CoreFoundationError('cannot write property list as data: %s', error) |
| + data = CoreFoundation.CFDataGetBytes( |
| + cfdata, |
| + CoreFoundation.CFRangeMake(0, CoreFoundation.CFDataGetLength(cfdata)), |
| + None) |
| + with open(target_path, 'wb') as target_file: |
| + target_file.write(data) |
| + target_file.flush() |
| + |
| + |
| +def FindProvisioningProfile(bundle, provisioning_profile_short_name): |
| + """Finds mobile provisioning profile to use to sign bundle. |
| + |
| + Args: |
| + bundle: the Bundle object to sign. |
| + provisioning_profile_short_path: optional short name of the mobile |
| + provisioning profile file to use to sign (will still be checked |
| + to see if it can sign bundle). |
| + |
| + Returns: |
| + The ProvisioningProfile object that can be used to sign the Bundle |
| + object. |
| + |
| + Raises: |
| + InstallationError if no mobile provisioning profile can be used to |
| + sign the Bundle object. |
| + """ |
| + provisioning_profiles_dir = GetProvisioningProfilesDir() |
| + |
| + # First check if there is a mobile provisioning profile installed with |
| + # the requested short name. If this is the case, restrict the search to |
| + # that mobile provisioning profile, otherwise consider all the installed |
| + # mobile provisioning profiles. |
| + provisioning_profile_paths = [] |
| + if provisioning_profile_short_name: |
| + provisioning_profile_path = os.path.join( |
| + provisioning_profiles_dir, |
| + provisioning_profile_short_name + '.mobileprovision') |
| + if os.path.isfile(provisioning_profile_path): |
| + provisioning_profile_paths.append(provisioning_profile_path) |
| + |
| + if not provisioning_profile_paths: |
| + provisioning_profile_paths = glob.glob( |
| + os.path.join(provisioning_profiles_dir, '*.mobileprovision')) |
| + |
| + # Iterate over all installed mobile provisioning profiles and filter those |
| + # that can be used to sign the bundle. |
| + valid_provisioning_profiles = [] |
| + for provisioning_profile_path in provisioning_profile_paths: |
| + provisioning_profile = ProvisioningProfile.Load(provisioning_profile_path) |
| + if provisioning_profile.ValidToSignBundle(bundle): |
| + valid_provisioning_profiles.append(provisioning_profile) |
| + |
| + if not valid_provisioning_profiles: |
| + raise InstallationError( |
| + 'no mobile provisioning profile for "%s"', |
| + bundle.identifier) |
| + |
| + # Select the most specific mobile provisioning profile, i.e. the one with |
| + # the longest application identifier pattern. |
| + valid_provisioning_profiles.sort( |
| + key=lambda p: len(p.application_identifier_pattern)) |
| + return valid_provisioning_profiles[0] |
| + |
| + |
| +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
|
| + """Creates entitlements using defaults from provisioning profile. |
| + |
| + Args: |
| + bundle: the Bundle object to sign. |
| + provisition_profile: the ProvisioningProfile object used to sign. |
| + entitlements_path: path to the template to use to generate the bundle |
| + entitlements file, needs to be a property list file. |
| + """ |
| + entitlements = Entitlements.Load(entitlements_path) |
| + entitlements.ExpandVariables({ |
| + 'CFBundleIdentifier': bundle.identifier, |
| + 'AppIdentifierPrefix': '%s.' % (provisioning_profile.team_identifier,) |
| + }) |
| + entitlements.LoadDefaults(provisioning_profile.entitlements) |
| + return entitlements |
| + |
| + |
| +def CodeSignBundle(binary, bundle, args): |
| + """Cryptographically signs bundle. |
| + |
| + Args: |
| + bundle: the Bundle object to sign. |
| + args: a dictionary with configuration settings for the code signature, |
| + need to define 'entitlements_path', 'provisioning_profile_short_name', |
| + 'deep_signature' and 'identify' keys. |
| + """ |
| + provisioning_profile = FindProvisioningProfile( |
| + bundle, args.provisioning_profile_short_name) |
| + provisioning_profile.Install(bundle) |
| + |
| + 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.
|
| + if os.path.isfile(signature_file): |
| + os.unlink(signature_file) |
| + |
| + shutil.copy(binary, bundle.binary_path) |
| + |
| + 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.
|
| + if args.preserve: |
| + command.extend(['--deep', '--preserve-metadata=identifier,entitlements']) |
| + command.append(bundle.path) |
| + |
| + subprocess.check_call(command) |
| + else: |
| + entitlements = CreateEntitlements( |
| + bundle, provisioning_profile, args.entitlements_path) |
| + with tempfile.NamedTemporaryFile(suffix='.xcent') as temporary_file_path: |
| + entitlements.WriteTo(temporary_file_path.name) |
| + command.extend(['--entitlements', temporary_file_path.name]) |
| + command.append(bundle.path) |
| + subprocess.check_call(command) |
| + |
| + |
| +def Main(): |
| + parser = argparse.ArgumentParser('codesign iOS bundles') |
| + parser.add_argument( |
| + 'path', help='path to the iOS bundle to codesign') |
| + parser.add_argument( |
| + '--binary', '-b', required=True, |
| + help='path to the iOS bundle binary') |
| + parser.add_argument( |
| + '--provisioning-profile', '-p', dest='provisioning_profile_short_name', |
| + help='short name of the mobile provisioning profile to use (' |
| + 'if undefined, will autodetect the mobile provisioning ' |
| + 'to use)') |
| + parser.add_argument( |
| + '--identity', '-i', required=True, |
| + help='identity to use to codesign') |
| + group = parser.add_mutually_exclusive_group(required=True) |
| + group.add_argument( |
| + '--entitlements', '-e', dest='entitlements_path', |
| + help='path to the entitlements file to use') |
| + group.add_argument( |
| + '--deep', '-d', action='store_true', default=False, dest='preserve', |
| + help='deep signature (default: %(default)s)') |
| + args = parser.parse_args() |
| + |
| + CodeSignBundle(args.binary, Bundle.Load(args.path), args) |
| + |
| + |
| +if __name__ == '__main__': |
| + sys.exit(Main()) |