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 plistlib |
| 10 import shutil |
| 11 import subprocess |
| 12 import sys |
| 13 import tempfile |
| 14 |
| 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(): |
| 24 """Returns the location of the installed mobile provisioning profiles. |
| 25 |
| 26 Returns: |
| 27 The path to the directory containing the installed mobile provisioning |
| 28 profiles as a string. |
| 29 """ |
| 30 return os.path.join( |
| 31 os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') |
| 32 |
| 33 |
| 34 def LoadPlistFile(plist_path): |
| 35 """Loads property list file at |plist_path|. |
| 36 |
| 37 Args: |
| 38 plist_path: path to the property list file to load. |
| 39 |
| 40 Returns: |
| 41 The content of the property list file as a python object. |
| 42 """ |
| 43 return plistlib.readPlistFromString(subprocess.check_output([ |
| 44 'xcrun', 'plutil', '-convert', 'xml1', '-o', '-', plist_path])) |
| 45 |
| 46 |
| 47 class Bundle(object): |
| 48 """Wraps a bundle.""" |
| 49 |
| 50 def __init__(self, bundle_path): |
| 51 """Initializes the Bundle object with data from bundle Info.plist file.""" |
| 52 self._path = bundle_path |
| 53 self._data = LoadPlistFile(os.path.join(self._path, 'Info.plist')) |
| 54 |
| 55 @property |
| 56 def path(self): |
| 57 return self._path |
| 58 |
| 59 @property |
| 60 def identifier(self): |
| 61 return self._data['CFBundleIdentifier'] |
| 62 |
| 63 @property |
| 64 def binary_path(self): |
| 65 return os.path.join(self._path, self._data['CFBundleExecutable']) |
| 66 |
| 67 |
| 68 class ProvisioningProfile(object): |
| 69 """Wraps a mobile provisioning profile file.""" |
| 70 |
| 71 def __init__(self, provisioning_profile_path): |
| 72 """Initializes the ProvisioningProfile with data from profile file.""" |
| 73 self._path = provisioning_profile_path |
| 74 self._data = plistlib.readPlistFromString(subprocess.check_output([ |
| 75 'xcrun', 'security', 'cms', '-D', '-i', provisioning_profile_path])) |
| 76 |
| 77 @property |
| 78 def path(self): |
| 79 return self._path |
| 80 |
| 81 @property |
| 82 def application_identifier_pattern(self): |
| 83 return self._data.get('Entitlements', {}).get('application-identifier', '') |
| 84 |
| 85 @property |
| 86 def team_identifier(self): |
| 87 return self._data.get('TeamIdentifier', [''])[0] |
| 88 |
| 89 @property |
| 90 def entitlements(self): |
| 91 return self._data.get('Entitlements', {}) |
| 92 |
| 93 def ValidToSignBundle(self, bundle): |
| 94 """Checks whether the provisioning profile can sign bundle_identifier. |
| 95 |
| 96 Args: |
| 97 bundle: the Bundle object that needs to be signed. |
| 98 |
| 99 Returns: |
| 100 True if the mobile provisioning profile can be used to sign a bundle |
| 101 with the corresponding bundle_identifier, False otherwise. |
| 102 """ |
| 103 return fnmatch.fnmatch( |
| 104 '%s.%s' % (self.team_identifier, bundle.identifier), |
| 105 self.application_identifier_pattern) |
| 106 |
| 107 def Install(self, bundle): |
| 108 """Copies mobile provisioning profile info the bundle.""" |
| 109 installation_path = os.path.join(bundle.path, 'embedded.mobileprovision') |
| 110 shutil.copy2(self.path, installation_path) |
| 111 |
| 112 |
| 113 class Entitlements(object): |
| 114 """Wraps an Entitlement plist file.""" |
| 115 |
| 116 def __init__(self, entitlements_path): |
| 117 """Initializes Entitlements object from entitlement file.""" |
| 118 self._path = entitlements_path |
| 119 self._data = LoadPlistFile(self._path) |
| 120 |
| 121 @property |
| 122 def path(self): |
| 123 return self._path |
| 124 |
| 125 def ExpandVariables(self, substitutions): |
| 126 self._data = self._ExpandVariables(self._data, substitutions) |
| 127 |
| 128 def _ExpandVariables(self, data, substitutions): |
| 129 if isinstance(data, str): |
| 130 for key, substitution in substitutions.iteritems(): |
| 131 data = data.replace('$(%s)' % (key,), substitution) |
| 132 return data |
| 133 |
| 134 if isinstance(data, dict): |
| 135 for key, value in data.iteritems(): |
| 136 data[key] = self._ExpandVariables(value, substitutions) |
| 137 return data |
| 138 |
| 139 if isinstance(data, list): |
| 140 for i, value in enumerate(data): |
| 141 data[i] = self._ExpandVariables(value, substitutions) |
| 142 |
| 143 return data |
| 144 |
| 145 def LoadDefaults(self, defaults): |
| 146 for key, value in defaults.iteritems(): |
| 147 if key not in self._data: |
| 148 self._data[key] = value |
| 149 |
| 150 def WriteTo(self, target_path): |
| 151 plistlib.writePlist(self._data, target_path) |
| 152 |
| 153 |
| 154 def FindProvisioningProfile(bundle, provisioning_profile_short_name): |
| 155 """Finds mobile provisioning profile to use to sign bundle. |
| 156 |
| 157 Args: |
| 158 bundle: the Bundle object 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 |
| 163 Returns: |
| 164 The ProvisioningProfile object that can be used to sign the Bundle |
| 165 object. |
| 166 |
| 167 Raises: |
| 168 InstallationError if no mobile provisioning profile can be used to |
| 169 sign the Bundle object. |
| 170 """ |
| 171 provisioning_profiles_dir = GetProvisioningProfilesDir() |
| 172 |
| 173 # First check if there is a mobile provisioning profile installed with |
| 174 # the requested short name. If this is the case, restrict the search to |
| 175 # that mobile provisioning profile, otherwise consider all the installed |
| 176 # mobile provisioning profiles. |
| 177 provisioning_profile_paths = [] |
| 178 if provisioning_profile_short_name: |
| 179 provisioning_profile_path = os.path.join( |
| 180 provisioning_profiles_dir, |
| 181 provisioning_profile_short_name + '.mobileprovision') |
| 182 if os.path.isfile(provisioning_profile_path): |
| 183 provisioning_profile_paths.append(provisioning_profile_path) |
| 184 |
| 185 if not provisioning_profile_paths: |
| 186 provisioning_profile_paths = glob.glob( |
| 187 os.path.join(provisioning_profiles_dir, '*.mobileprovision')) |
| 188 |
| 189 # Iterate over all installed mobile provisioning profiles and filter those |
| 190 # that can be used to sign the bundle. |
| 191 valid_provisioning_profiles = [] |
| 192 for provisioning_profile_path in provisioning_profile_paths: |
| 193 provisioning_profile = ProvisioningProfile(provisioning_profile_path) |
| 194 if provisioning_profile.ValidToSignBundle(bundle): |
| 195 valid_provisioning_profiles.append(provisioning_profile) |
| 196 |
| 197 if not valid_provisioning_profiles: |
| 198 raise InstallationError( |
| 199 'no mobile provisioning profile for "%s"', |
| 200 bundle.identifier) |
| 201 |
| 202 # Select the most specific mobile provisioning profile, i.e. the one with |
| 203 # the longest application identifier pattern. |
| 204 valid_provisioning_profiles.sort( |
| 205 key=lambda p: len(p.application_identifier_pattern)) |
| 206 return valid_provisioning_profiles[0] |
| 207 |
| 208 |
| 209 def CodeSignBundle(binary, bundle, args): |
| 210 """Cryptographically signs bundle. |
| 211 |
| 212 Args: |
| 213 bundle: the Bundle object to sign. |
| 214 args: a dictionary with configuration settings for the code signature, |
| 215 need to define 'entitlements_path', 'provisioning_profile_short_name', |
| 216 'deep_signature' and 'identify' keys. |
| 217 """ |
| 218 provisioning_profile = FindProvisioningProfile( |
| 219 bundle, args.provisioning_profile_short_name) |
| 220 provisioning_profile.Install(bundle) |
| 221 |
| 222 signature_file = os.path.join(bundle.path, '_CodeSignature', 'CodeResources') |
| 223 if os.path.isfile(signature_file): |
| 224 os.unlink(signature_file) |
| 225 |
| 226 shutil.copy(binary, bundle.binary_path) |
| 227 |
| 228 if args.preserve: |
| 229 subprocess.check_call([ |
| 230 'xcrun', 'codesign', '--force', '--sign', args.identity, |
| 231 '--deep', '--preserve-metadata=identifier,entitlements', |
| 232 '--timestamp=none', bundle.path]) |
| 233 else: |
| 234 entitlements = Entitlements(args.entitlements_path) |
| 235 entitlements.LoadDefaults(provisioning_profile.entitlements) |
| 236 entitlements.ExpandVariables({ |
| 237 'CFBundleIdentifier': bundle.identifier, |
| 238 'AppIdentifierPrefix': '%s.' % (provisioning_profile.team_identifier,) |
| 239 }) |
| 240 |
| 241 with tempfile.NamedTemporaryFile(suffix='.xcent') as temporary_file_path: |
| 242 entitlements.WriteTo(temporary_file_path.name) |
| 243 subprocess.check_call([ |
| 244 'xcrun', 'codesign', '--force', '--sign', args.identity, |
| 245 '--entitlements', temporary_file_path.name, '--timestamp=none', |
| 246 bundle.path]) |
| 247 |
| 248 |
| 249 def Main(): |
| 250 parser = argparse.ArgumentParser('codesign iOS bundles') |
| 251 parser.add_argument( |
| 252 'path', help='path to the iOS bundle to codesign') |
| 253 parser.add_argument( |
| 254 '--binary', '-b', required=True, |
| 255 help='path to the iOS bundle binary') |
| 256 parser.add_argument( |
| 257 '--provisioning-profile', '-p', dest='provisioning_profile_short_name', |
| 258 help='short name of the mobile provisioning profile to use (' |
| 259 'if undefined, will autodetect the mobile provisioning ' |
| 260 'to use)') |
| 261 parser.add_argument( |
| 262 '--identity', '-i', required=True, |
| 263 help='identity to use to codesign') |
| 264 group = parser.add_mutually_exclusive_group(required=True) |
| 265 group.add_argument( |
| 266 '--entitlements', '-e', dest='entitlements_path', |
| 267 help='path to the entitlements file to use') |
| 268 group.add_argument( |
| 269 '--deep', '-d', action='store_true', default=False, dest='preserve', |
| 270 help='deep signature (default: %(default)s)') |
| 271 args = parser.parse_args() |
| 272 |
| 273 CodeSignBundle(args.binary, Bundle(args.path), args) |
| 274 |
| 275 |
| 276 if __name__ == '__main__': |
| 277 sys.exit(Main()) |
OLD | NEW |