blob: dee1cc29f8256e9f2650ca6f58376eb559a231bf [file]
# 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 plistlib
import shutil
import subprocess
import sys
import tempfile
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.
Returns:
The content of the property list file as a python object.
"""
return plistlib.readPlistFromString(subprocess.check_output([
'xcrun', 'plutil', '-convert', 'xml1', '-o', '-', plist_path]))
class Bundle(object):
"""Wraps a bundle."""
def __init__(self, bundle_path):
"""Initializes the Bundle object with data from bundle Info.plist file."""
self._path = bundle_path
self._data = LoadPlistFile(os.path.join(self._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, provisioning_profile_path):
"""Initializes the ProvisioningProfile with data from profile file."""
self._path = provisioning_profile_path
self._data = plistlib.readPlistFromString(subprocess.check_output([
'xcrun', 'security', 'cms', '-D', '-i', provisioning_profile_path]))
@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):
"""Wraps an Entitlement plist file."""
def __init__(self, entitlements_path):
"""Initializes Entitlements object from entitlement file."""
self._path = entitlements_path
self._data = LoadPlistFile(self._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 isinstance(data, str):
for key, substitution in substitutions.iteritems():
data = data.replace('$(%s)' % (key,), substitution)
return data
if isinstance(data, dict):
for key, value in data.iteritems():
data[key] = self._ExpandVariables(value, substitutions)
return data
if isinstance(data, list):
for i, value in enumerate(data):
data[i] = self._ExpandVariables(value, substitutions)
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):
plistlib.writePlist(self._data, target_path)
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(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.
return max(
valid_provisioning_profiles,
key=lambda p: len(p.application_identifier_pattern))
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')
if os.path.isfile(signature_file):
os.unlink(signature_file)
shutil.copy(binary, bundle.binary_path)
if args.preserve:
subprocess.check_call([
'xcrun', 'codesign', '--force', '--sign', args.identity,
'--deep', '--preserve-metadata=identifier,entitlements',
'--timestamp=none', bundle.path])
else:
entitlements = Entitlements(args.entitlements_path)
entitlements.LoadDefaults(provisioning_profile.entitlements)
entitlements.ExpandVariables({
'CFBundleIdentifier': bundle.identifier,
'AppIdentifierPrefix': '%s.' % (provisioning_profile.team_identifier,)
})
with tempfile.NamedTemporaryFile(suffix='.xcent') as temporary_file_path:
entitlements.WriteTo(temporary_file_path.name)
subprocess.check_call([
'xcrun', 'codesign', '--force', '--sign', args.identity,
'--entitlements', temporary_file_path.name, '--timestamp=none',
bundle.path])
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(args.path), args)
if __name__ == '__main__':
sys.exit(Main())