Commit 86dea68f authored by Joshua Pawlicki's avatar Joshua Pawlicki Committed by Commit Bot

Refactor chrome/installer/mac/signing in preparation for updater reuse.

The main thing here is to move get_parts into the parts.py.

Bug: 926234
Change-Id: Icf7d9129281ef45438d1dfa27c992c011d0306bb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2016972
Auto-Submit: Joshua Pawlicki <waffles@chromium.org>
Commit-Queue: Robert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Cr-Commit-Position: refs/heads/master@{#737374}
parent 3316bcc5
...@@ -113,6 +113,7 @@ group("mac_signing_tests") { ...@@ -113,6 +113,7 @@ group("mac_signing_tests") {
"signing/model_test.py", "signing/model_test.py",
"signing/modification_test.py", "signing/modification_test.py",
"signing/notarize_test.py", "signing/notarize_test.py",
"signing/parts_test.py",
"signing/pipeline_test.py", "signing/pipeline_test.py",
"signing/run_mac_signing_tests.py", "signing/run_mac_signing_tests.py",
"signing/signing_test.py", "signing/signing_test.py",
......
...@@ -10,6 +10,7 @@ mac_signing_sources = [ ...@@ -10,6 +10,7 @@ mac_signing_sources = [
"signing/model.py", "signing/model.py",
"signing/modification.py", "signing/modification.py",
"signing/notarize.py", "signing/notarize.py",
"signing/parts.py",
"signing/pipeline.py", "signing/pipeline.py",
"signing/signing.py", "signing/signing.py",
] ]
...@@ -10,7 +10,7 @@ be modified or renamed to support side-by-side channel installs. ...@@ -10,7 +10,7 @@ be modified or renamed to support side-by-side channel installs.
import os.path import os.path
from . import commands, signing from . import commands, parts
_CF_BUNDLE_EXE = 'CFBundleExecutable' _CF_BUNDLE_EXE = 'CFBundleExecutable'
_CF_BUNDLE_ID = 'CFBundleIdentifier' _CF_BUNDLE_ID = 'CFBundleIdentifier'
...@@ -145,7 +145,7 @@ def _process_entitlements(paths, dist, config): ...@@ -145,7 +145,7 @@ def _process_entitlements(paths, dist, config):
entitlements_names = [ entitlements_names = [
part.entitlements part.entitlements
for part in signing.get_parts(config).values() for part in parts.get_parts(config).values()
if part.entitlements if part.entitlements
] ]
for entitlements_name in entitlements_names: for entitlements_name in entitlements_names:
......
# Copyright 2020 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.
"""
The parts module defines the various binary pieces of the Chrome application
bundle that need to be signed, as well as providing utilities to sign them.
"""
import os.path
from . import commands, signing
from .model import CodeSignOptions, CodeSignedProduct, VerifyOptions
_PROVISIONPROFILE_EXT = '.provisionprofile'
_PROVISIONPROFILE_DEST = 'embedded.provisionprofile'
def get_parts(config):
"""Returns all the |model.CodeSignedProduct| objects to be signed for a
Chrome application bundle.
Args:
config: The |config.CodeSignConfig|.
Returns:
A dictionary of |model.CodeSignedProduct|. The keys are short
identifiers that have no bearing on the actual signing operations.
"""
# Inner parts of the bundle do not have the identifier customized with
# the channel's identifier fragment.
if hasattr(config, 'base_config'):
uncustomized_bundle_id = config.base_config.base_bundle_id
else:
uncustomized_bundle_id = config.base_bundle_id
# Specify the components of HARDENED_RUNTIME that are also available on
# older macOS versions.
full_hardened_runtime_options = (
CodeSignOptions.HARDENED_RUNTIME + CodeSignOptions.RESTRICT +
CodeSignOptions.LIBRARY_VALIDATION + CodeSignOptions.KILL)
verify_options = VerifyOptions.DEEP + VerifyOptions.STRICT
parts = {
'app':
CodeSignedProduct(
'{.app_product}.app'.format(config),
config.base_bundle_id,
options=full_hardened_runtime_options,
requirements=config.codesign_requirements_outer_app,
identifier_requirement=False,
entitlements='app-entitlements.plist',
verify_options=verify_options),
'framework':
CodeSignedProduct(
# The framework is a dylib, so options= flags are meaningless.
config.framework_dir,
'{}.framework'.format(uncustomized_bundle_id),
verify_options=verify_options),
'notification-xpc':
CodeSignedProduct(
'{.framework_dir}/XPCServices/AlertNotificationService.xpc'
.format(config),
'{}.framework.AlertNotificationService'.format(
config.base_bundle_id),
options=full_hardened_runtime_options,
verify_options=verify_options),
'crashpad':
CodeSignedProduct(
'{.framework_dir}/Helpers/chrome_crashpad_handler'.format(
config),
'chrome_crashpad_handler',
options=full_hardened_runtime_options,
verify_options=verify_options),
'helper-app':
CodeSignedProduct(
'{0.framework_dir}/Helpers/{0.product} Helper.app'.format(
config),
'{}.helper'.format(uncustomized_bundle_id),
options=full_hardened_runtime_options,
verify_options=verify_options),
'helper-renderer-app':
CodeSignedProduct(
'{0.framework_dir}/Helpers/{0.product} Helper (Renderer).app'
.format(config),
'{}.helper.renderer'.format(uncustomized_bundle_id),
# Do not use |full_hardened_runtime_options| because library
# validation is incompatible with the JIT entitlement.
options=CodeSignOptions.RESTRICT + CodeSignOptions.KILL +
CodeSignOptions.HARDENED_RUNTIME,
entitlements='helper-renderer-entitlements.plist',
verify_options=verify_options),
'helper-gpu-app':
CodeSignedProduct(
'{0.framework_dir}/Helpers/{0.product} Helper (GPU).app'.format(
config),
'{}.helper'.format(uncustomized_bundle_id),
# Do not use |full_hardened_runtime_options| because library
# validation is incompatible with more permissive code signing
# entitlements.
options=CodeSignOptions.RESTRICT + CodeSignOptions.KILL +
CodeSignOptions.HARDENED_RUNTIME,
entitlements='helper-gpu-entitlements.plist',
verify_options=verify_options),
'helper-plugin-app':
CodeSignedProduct(
'{0.framework_dir}/Helpers/{0.product} Helper (Plugin).app'
.format(config),
'{}.helper.plugin'.format(uncustomized_bundle_id),
# Do not use |full_hardened_runtime_options| because library
# validation is incompatible with the disable-library-validation
# entitlement.
options=CodeSignOptions.RESTRICT + CodeSignOptions.KILL +
CodeSignOptions.HARDENED_RUNTIME,
entitlements='helper-plugin-entitlements.plist',
verify_options=verify_options),
'app-mode-app':
CodeSignedProduct(
'{.framework_dir}/Helpers/app_mode_loader'.format(config),
'app_mode_loader',
options=full_hardened_runtime_options,
verify_options=verify_options),
}
dylibs = (
'libEGL.dylib',
'libGLESv2.dylib',
'libswiftshader_libEGL.dylib',
'libswiftshader_libGLESv2.dylib',
)
for library in dylibs:
library_basename = os.path.basename(library)
parts[library_basename] = CodeSignedProduct(
'{.framework_dir}/Libraries/{library}'.format(
config, library=library),
library_basename.replace('.dylib', ''),
verify_options=verify_options)
return parts
def get_installer_tools(config):
"""Returns all the |model.CodeSignedProduct| objects to be signed for
creating the installer tools package.
Args:
config: The |config.CodeSignConfig|.
Returns:
A dictionary of |model.CodeSignedProduct|. The keys are short
identifiers that have no bearing on the actual signing operations.
"""
tools = {}
binaries = (
'goobsdiff',
'goobspatch',
'liblzma_decompress.dylib',
'xz',
'xzdec',
)
for binary in binaries:
options = (
CodeSignOptions.HARDENED_RUNTIME + CodeSignOptions.RESTRICT +
CodeSignOptions.LIBRARY_VALIDATION + CodeSignOptions.KILL)
tools[binary] = CodeSignedProduct(
'{.packaging_dir}/{binary}'.format(config, binary=binary),
binary.replace('.dylib', ''),
options=options if not binary.endswith('dylib') else None,
verify_options=VerifyOptions.DEEP + VerifyOptions.STRICT)
return tools
def sign_chrome(paths, config, sign_framework=False):
"""Code signs the Chrome application bundle and all of its internal nested
code parts.
Args:
paths: A |model.Paths| object.
config: The |model.CodeSignConfig| object. The |app_product| binary and
nested binaries must exist in |paths.work|.
sign_framework: True if the inner framework is to be signed in addition
to the outer application. False if only the outer application is to
be signed.
"""
parts = get_parts(config)
_sanity_check_version_keys(paths, parts)
if sign_framework:
# To sign an .app bundle that contains nested code, the nested
# components themselves must be signed. Each of these components is
# signed below. Note that unless a framework has multiple versions
# (which is discouraged), signing the entire framework is equivalent to
# signing the Current version.
# https://developer.apple.com/library/content/technotes/tn2206/_index.html#//apple_ref/doc/uid/DTS40007919-CH1-TNTAG13
for name, part in parts.items():
if name in ('app', 'framework'):
continue
signing.sign_part(paths, config, part)
# Sign the framework bundle.
signing.sign_part(paths, config, parts['framework'])
provisioning_profile_basename = config.provisioning_profile_basename
if provisioning_profile_basename:
commands.copy_files(
os.path.join(
paths.packaging_dir(config),
provisioning_profile_basename + _PROVISIONPROFILE_EXT),
os.path.join(paths.work, parts['app'].path, 'Contents',
_PROVISIONPROFILE_DEST))
# Sign the outer app bundle.
signing.sign_part(paths, config, parts['app'])
# Verify all the parts.
for part in parts.values():
signing.verify_part(paths, part)
# Display the code signature.
signing.validate_app(paths, config, parts['app'])
def _sanity_check_version_keys(paths, parts):
"""Verifies that the various version keys in Info.plists match.
Args:
paths: A |model.Paths| object.
parts: The dictionary returned from get_parts().
"""
app_plist_path = os.path.join(paths.work, parts['app'].path, 'Contents',
'Info.plist')
framework_plist_path = os.path.join(paths.work, parts['framework'].path,
'Resources', 'Info.plist')
with commands.PlistContext(
app_plist_path) as app_plist, commands.PlistContext(
framework_plist_path) as framework_plist:
if not 'KSVersion' in app_plist:
assert 'com.google.Chrome' not in app_plist['CFBundleIdentifier']
return
ks_version = app_plist['KSVersion']
cf_version = framework_plist['CFBundleShortVersionString']
if cf_version != ks_version:
raise ValueError(
'CFBundleVersion ({}) does not mach KSVersion ({})'.format(
cf_version, ks_version))
# Copyright 2020 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 unittest
from . import model, parts, signing, test_common, test_config
mock = test_common.import_mock()
class TestGetParts(unittest.TestCase):
def test_get_parts_no_base(self):
config = test_config.TestConfig()
all_parts = parts.get_parts(config)
self.assertEqual('test.signing.bundle_id', all_parts['app'].identifier)
self.assertEqual('test.signing.bundle_id.framework',
all_parts['framework'].identifier)
self.assertEqual(
'test.signing.bundle_id.framework.AlertNotificationService',
all_parts['notification-xpc'].identifier)
self.assertEqual('test.signing.bundle_id.helper',
all_parts['helper-app'].identifier)
def test_get_parts_no_customize(self):
config = model.Distribution(channel='dev').to_config(
test_config.TestConfig())
all_parts = parts.get_parts(config)
self.assertEqual('test.signing.bundle_id', all_parts['app'].identifier)
self.assertEqual('test.signing.bundle_id.framework',
all_parts['framework'].identifier)
self.assertEqual(
'test.signing.bundle_id.framework.AlertNotificationService',
all_parts['notification-xpc'].identifier)
self.assertEqual('test.signing.bundle_id.helper',
all_parts['helper-app'].identifier)
def test_get_parts_customize(self):
config = model.Distribution(
channel='canary',
channel_customize=True).to_config(test_config.TestConfig())
all_parts = parts.get_parts(config)
self.assertEqual('test.signing.bundle_id.canary',
all_parts['app'].identifier)
self.assertEqual('test.signing.bundle_id.framework',
all_parts['framework'].identifier)
self.assertEqual(
'test.signing.bundle_id.canary.framework.AlertNotificationService',
all_parts['notification-xpc'].identifier)
self.assertEqual('test.signing.bundle_id.helper',
all_parts['helper-app'].identifier)
def test_part_options(self):
all_parts = parts.get_parts(test_config.TestConfig())
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(all_parts['app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(all_parts['helper-app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT + model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(all_parts['helper-renderer-app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT + model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(all_parts['helper-gpu-app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT + model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(all_parts['helper-plugin-app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(all_parts['crashpad'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(all_parts['notification-xpc'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(all_parts['app-mode-app'].options))
def _get_plist_read(other_version):
def _plist_read(*args):
path = args[0]
first_slash = path.find('/')
path = path[first_slash + 1:]
plists = {
'App Product.app/Contents/Info.plist': {
'KSVersion': '99.0.9999.99'
},
'App Product.app/Contents/Frameworks/Product Framework.framework/Resources/Info.plist':
{
'CFBundleShortVersionString': other_version
}
}
return plists[path]
return _plist_read
@mock.patch.multiple('signing.signing',
**{m: mock.DEFAULT for m in ('sign_part', 'verify_part')})
@mock.patch.multiple('signing.commands', **{
m: mock.DEFAULT
for m in ('copy_files', 'move_file', 'make_dir', 'run_command')
})
class TestSignChrome(unittest.TestCase):
def setUp(self):
self.paths = model.Paths('$I', '$O', '$W')
@mock.patch('signing.parts._sanity_check_version_keys')
def test_sign_chrome(self, *args, **kwargs):
manager = mock.Mock()
for kwarg in kwargs:
manager.attach_mock(kwargs[kwarg], kwarg)
dist = model.Distribution()
config = dist.to_config(test_config.TestConfig())
parts.sign_chrome(self.paths, config, sign_framework=True)
# No files should be moved.
self.assertEqual(0, kwargs['move_file'].call_count)
# Test that the provisioning profile is copied.
self.assertEqual(kwargs['copy_files'].mock_calls, [
mock.call.copy_files(
'$I/Product Packaging/provisiontest.provisionprofile',
'$W/App Product.app/Contents/embedded.provisionprofile')
])
# Ensure that all the parts are signed.
signed_paths = [
call[1][2].path for call in kwargs['sign_part'].mock_calls
]
self.assertEqual(
set([p.path for p in parts.get_parts(config).values()]),
set(signed_paths))
# Make sure that the framework and the app are the last two parts that
# are signed.
self.assertEqual(signed_paths[-2:], [
'App Product.app/Contents/Frameworks/Product Framework.framework',
'App Product.app'
])
self.assertEqual(kwargs['run_command'].mock_calls, [
mock.call.run_command([
'codesign', '--display', '--requirements', '-', '--verbose=5',
'$W/App Product.app'
]),
mock.call.run_command(
['spctl', '--assess', '-vv', '$W/App Product.app']),
])
@mock.patch('signing.parts._sanity_check_version_keys')
def test_sign_chrome_no_assess(self, *args, **kwargs):
dist = model.Distribution()
class Config(test_config.TestConfig):
@property
def run_spctl_assess(self):
return False
config = dist.to_config(Config())
parts.sign_chrome(self.paths, config, sign_framework=True)
self.assertEqual(kwargs['run_command'].mock_calls, [
mock.call.run_command([
'codesign', '--display', '--requirements', '-', '--verbose=5',
'$W/App Product.app'
]),
])
@mock.patch('signing.parts._sanity_check_version_keys')
def test_sign_chrome_no_provisioning(self, *args, **kwargs):
dist = model.Distribution()
class Config(test_config.TestConfig):
@property
def provisioning_profile_basename(self):
return None
config = dist.to_config(Config())
parts.sign_chrome(self.paths, config, sign_framework=True)
self.assertEqual(0, kwargs['copy_files'].call_count)
@mock.patch('signing.parts._sanity_check_version_keys')
def test_sign_chrome_no_framework(self, *args, **kwargs):
manager = mock.Mock()
for kwarg in kwargs:
manager.attach_mock(kwargs[kwarg], kwarg)
dist = model.Distribution()
config = dist.to_config(test_config.TestConfig())
parts.sign_chrome(self.paths, config, sign_framework=False)
# No files should be moved.
self.assertEqual(0, kwargs['move_file'].call_count)
# Test that the provisioning profile is copied.
self.assertEqual(kwargs['copy_files'].mock_calls, [
mock.call.copy_files(
'$I/Product Packaging/provisiontest.provisionprofile',
'$W/App Product.app/Contents/embedded.provisionprofile')
])
# Ensure that only the app is signed.
signed_paths = [
call[1][2].path for call in kwargs['sign_part'].mock_calls
]
self.assertEqual(signed_paths, ['App Product.app'])
self.assertEqual(kwargs['run_command'].mock_calls, [
mock.call.run_command([
'codesign', '--display', '--requirements', '-', '--verbose=5',
'$W/App Product.app'
]),
mock.call.run_command(
['spctl', '--assess', '-vv', '$W/App Product.app']),
])
@mock.patch(
'signing.commands.plistlib.readPlist',
side_effect=_get_plist_read('99.0.9999.99'))
def test_sanity_check_ok(self, read_plist, **kwargs):
config = model.Distribution().to_config(test_config.TestConfig())
parts.sign_chrome(self.paths, config, sign_framework=True)
@mock.patch(
'signing.commands.plistlib.readPlist',
side_effect=_get_plist_read('55.0.5555.55'))
def test_sanity_check_bad(self, read_plist, **kwargs):
config = model.Distribution().to_config(test_config.TestConfig())
self.assertRaises(
ValueError,
lambda: parts.sign_chrome(self.paths, config, sign_framework=True))
...@@ -11,7 +11,7 @@ The pipeline module orchestrates the entire signing process, which includes: ...@@ -11,7 +11,7 @@ The pipeline module orchestrates the entire signing process, which includes:
import os.path import os.path
from . import commands, model, modification, notarize, signing from . import commands, model, modification, notarize, parts, signing
def _customize_and_sign_chrome(paths, dist_config, dest_dir, signed_frameworks): def _customize_and_sign_chrome(paths, dist_config, dest_dir, signed_frameworks):
...@@ -74,13 +74,13 @@ def _customize_and_sign_chrome(paths, dist_config, dest_dir, signed_frameworks): ...@@ -74,13 +74,13 @@ def _customize_and_sign_chrome(paths, dist_config, dest_dir, signed_frameworks):
actual_framework_change_count, actual_framework_change_count,
signed_framework_change_count)) signed_framework_change_count))
signing.sign_chrome(paths, dist_config, sign_framework=False) parts.sign_chrome(paths, dist_config, sign_framework=False)
else: else:
unsigned_framework_path = os.path.join(paths.work, unsigned_framework_path = os.path.join(paths.work,
'modified_unsigned_framework') 'modified_unsigned_framework')
commands.copy_dir_overwrite_and_count_changes( commands.copy_dir_overwrite_and_count_changes(
work_dir_framework_path, unsigned_framework_path, dry_run=False) work_dir_framework_path, unsigned_framework_path, dry_run=False)
signing.sign_chrome(paths, dist_config, sign_framework=True) parts.sign_chrome(paths, dist_config, sign_framework=True)
actual_framework_change_count = commands.copy_dir_overwrite_and_count_changes( actual_framework_change_count = commands.copy_dir_overwrite_and_count_changes(
work_dir_framework_path, unsigned_framework_path, dry_run=True) work_dir_framework_path, unsigned_framework_path, dry_run=True)
if signed_frameworks is not None: if signed_frameworks is not None:
...@@ -101,11 +101,10 @@ def _staple_chrome(paths, dist_config): ...@@ -101,11 +101,10 @@ def _staple_chrome(paths, dist_config):
paths: A |model.Paths| object. paths: A |model.Paths| object.
dist_config: A |config.CodeSignConfig| for the customized product. dist_config: A |config.CodeSignConfig| for the customized product.
""" """
parts = signing.get_parts(dist_config)
# Only staple the signed, bundled executables. # Only staple the signed, bundled executables.
part_paths = [ part_paths = [
part.path part.path
for part in parts.values() for part in parts.get_parts(dist_config).values()
# TODO(https://crbug.com/979725): Reinstate .xpc bundle stapling once # TODO(https://crbug.com/979725): Reinstate .xpc bundle stapling once
# the signing environment is on a macOS release that supports # the signing environment is on a macOS release that supports
# Xcode 10.2 or newer. # Xcode 10.2 or newer.
...@@ -382,7 +381,7 @@ def _package_installer_tools(paths, config): ...@@ -382,7 +381,7 @@ def _package_installer_tools(paths, config):
""" """
DIFF_TOOLS = 'diff_tools' DIFF_TOOLS = 'diff_tools'
tools_to_sign = signing.get_installer_tools(config) tools_to_sign = parts.get_installer_tools(config)
chrome_tools = ( chrome_tools = (
'keystone_install.sh',) if config.is_chrome_branded() else () 'keystone_install.sh',) if config.is_chrome_branded() else ()
other_tools = ( other_tools = (
...@@ -430,7 +429,7 @@ def _intermediate_work_dir_name(dist_config): ...@@ -430,7 +429,7 @@ def _intermediate_work_dir_name(dist_config):
return dist_config.packaging_basename return dist_config.packaging_basename
def sign_all(orig_paths, def sign_chrome(orig_paths,
config, config,
disable_packaging=False, disable_packaging=False,
do_notarization=True, do_notarization=True,
......
...@@ -55,9 +55,9 @@ def _get_adjacent_item(l, o): ...@@ -55,9 +55,9 @@ def _get_adjacent_item(l, o):
'copy_dir_overwrite_and_count_changes', 'run_command', 'copy_dir_overwrite_and_count_changes', 'run_command',
'make_dir', 'shutil', 'write_file', 'set_executable') 'make_dir', 'shutil', 'write_file', 'set_executable')
}) })
@mock.patch.multiple( @mock.patch.multiple('signing.signing',
'signing.signing', **{m: mock.DEFAULT for m in ('sign_part', 'verify_part')})
**{m: mock.DEFAULT for m in ('sign_part', 'sign_chrome', 'verify_part')}) @mock.patch.multiple('signing.parts', **{'sign_chrome': mock.DEFAULT})
@mock.patch('signing.commands.tempfile.mkdtemp', _get_work_dir) @mock.patch('signing.commands.tempfile.mkdtemp', _get_work_dir)
class TestPipelineHelpers(unittest.TestCase): class TestPipelineHelpers(unittest.TestCase):
...@@ -203,8 +203,7 @@ class TestPipelineHelpers(unittest.TestCase): ...@@ -203,8 +203,7 @@ class TestPipelineHelpers(unittest.TestCase):
'$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework', '$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework',
'$W/modified_unsigned_framework', '$W/modified_unsigned_framework',
dry_run=False), dry_run=False),
mock.call.sign_chrome( mock.call.sign_chrome(paths, channel_dist_config, sign_framework=True),
paths, channel_dist_config, sign_framework=True),
mock.call.copy_dir_overwrite_and_count_changes( mock.call.copy_dir_overwrite_and_count_changes(
'$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework', '$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework',
'$W/modified_unsigned_framework', '$W/modified_unsigned_framework',
...@@ -716,7 +715,7 @@ class TestSignAll(unittest.TestCase): ...@@ -716,7 +715,7 @@ class TestSignAll(unittest.TestCase):
] ]
config = Config() config = Config()
pipeline.sign_all(self.paths, config) pipeline.sign_chrome(self.paths, config)
self.assertEqual(1, kwargs['_package_installer_tools'].call_count) self.assertEqual(1, kwargs['_package_installer_tools'].call_count)
...@@ -779,7 +778,7 @@ class TestSignAll(unittest.TestCase): ...@@ -779,7 +778,7 @@ class TestSignAll(unittest.TestCase):
] ]
config = Config() config = Config()
pipeline.sign_all(self.paths, config) pipeline.sign_chrome(self.paths, config)
self.assertEqual(1, kwargs['_package_installer_tools'].call_count) self.assertEqual(1, kwargs['_package_installer_tools'].call_count)
...@@ -845,7 +844,7 @@ class TestSignAll(unittest.TestCase): ...@@ -845,7 +844,7 @@ class TestSignAll(unittest.TestCase):
] ]
config = Config() config = Config()
pipeline.sign_all(self.paths, config) pipeline.sign_chrome(self.paths, config)
self.assertEqual(1, kwargs['_package_installer_tools'].call_count) self.assertEqual(1, kwargs['_package_installer_tools'].call_count)
...@@ -900,7 +899,7 @@ class TestSignAll(unittest.TestCase): ...@@ -900,7 +899,7 @@ class TestSignAll(unittest.TestCase):
kwargs['wait_for_results'].return_value = iter([app_uuid]) kwargs['wait_for_results'].return_value = iter([app_uuid])
config = test_config.TestConfig() config = test_config.TestConfig()
pipeline.sign_all(self.paths, config, disable_packaging=True) pipeline.sign_chrome(self.paths, config, disable_packaging=True)
manager.assert_has_calls([ manager.assert_has_calls([
# First customize the distribution and sign it. # First customize the distribution and sign it.
...@@ -936,7 +935,7 @@ class TestSignAll(unittest.TestCase): ...@@ -936,7 +935,7 @@ class TestSignAll(unittest.TestCase):
manager.attach_mock(kwargs[attr], attr) manager.attach_mock(kwargs[attr], attr)
config = test_config.TestConfig() config = test_config.TestConfig()
pipeline.sign_all(self.paths, config, do_notarization=False) pipeline.sign_chrome(self.paths, config, do_notarization=False)
self.assertEqual(1, kwargs['_package_installer_tools'].call_count) self.assertEqual(1, kwargs['_package_installer_tools'].call_count)
...@@ -960,7 +959,7 @@ class TestSignAll(unittest.TestCase): ...@@ -960,7 +959,7 @@ class TestSignAll(unittest.TestCase):
manager.attach_mock(kwargs[attr], attr) manager.attach_mock(kwargs[attr], attr)
config = test_config.TestConfig() config = test_config.TestConfig()
pipeline.sign_all( pipeline.sign_chrome(
self.paths, config, disable_packaging=True, do_notarization=False) self.paths, config, disable_packaging=True, do_notarization=False)
manager.assert_has_calls([ manager.assert_has_calls([
...@@ -1006,7 +1005,7 @@ class TestSignAll(unittest.TestCase): ...@@ -1006,7 +1005,7 @@ class TestSignAll(unittest.TestCase):
] ]
config = Config() config = Config()
pipeline.sign_all(self.paths, config, do_notarization=False) pipeline.sign_chrome(self.paths, config, do_notarization=False)
self.assertEqual(1, kwargs['_package_installer_tools'].call_count) self.assertEqual(1, kwargs['_package_installer_tools'].call_count)
self.assertEqual(4, kwargs['_customize_and_sign_chrome'].call_count) self.assertEqual(4, kwargs['_customize_and_sign_chrome'].call_count)
......
...@@ -6,169 +6,9 @@ The signing module defines the various binary pieces of the Chrome application ...@@ -6,169 +6,9 @@ The signing module defines the various binary pieces of the Chrome application
bundle that need to be signed, as well as providing utilities to sign them. bundle that need to be signed, as well as providing utilities to sign them.
""" """
import copy
import os.path import os.path
from . import commands from . import commands
from .model import CodeSignOptions, CodeSignedProduct, VerifyOptions
_PROVISIONPROFILE_EXT = '.provisionprofile'
_PROVISIONPROFILE_DEST = 'embedded.provisionprofile'
def get_parts(config):
"""Returns all the |model.CodeSignedProduct| objects to be signed for a
Chrome application bundle.
Args:
config: The |config.CodeSignConfig|.
Returns:
A dictionary of |model.CodeSignedProduct|. The keys are short
identifiers that have no bearing on the actual signing operations.
"""
# Inner parts of the bundle do not have the identifier customized with
# the channel's identifier fragment.
if hasattr(config, 'base_config'):
uncustomized_bundle_id = config.base_config.base_bundle_id
else:
uncustomized_bundle_id = config.base_bundle_id
# Specify the components of HARDENED_RUNTIME that are also available on
# older macOS versions.
full_hardened_runtime_options = (
CodeSignOptions.HARDENED_RUNTIME + CodeSignOptions.RESTRICT +
CodeSignOptions.LIBRARY_VALIDATION + CodeSignOptions.KILL)
verify_options = VerifyOptions.DEEP + VerifyOptions.STRICT
parts = {
'app':
CodeSignedProduct(
'{.app_product}.app'.format(config),
config.base_bundle_id,
options=full_hardened_runtime_options,
requirements=config.codesign_requirements_outer_app,
identifier_requirement=False,
entitlements='app-entitlements.plist',
verify_options=verify_options),
'framework':
CodeSignedProduct(
# The framework is a dylib, so options= flags are meaningless.
config.framework_dir,
'{}.framework'.format(uncustomized_bundle_id),
verify_options=verify_options),
'notification-xpc':
CodeSignedProduct(
'{.framework_dir}/XPCServices/AlertNotificationService.xpc'
.format(config),
'{}.framework.AlertNotificationService'.format(
config.base_bundle_id),
options=full_hardened_runtime_options,
verify_options=verify_options),
'crashpad':
CodeSignedProduct(
'{.framework_dir}/Helpers/chrome_crashpad_handler'.format(
config),
'chrome_crashpad_handler',
options=full_hardened_runtime_options,
verify_options=verify_options),
'helper-app':
CodeSignedProduct(
'{0.framework_dir}/Helpers/{0.product} Helper.app'.format(
config),
'{}.helper'.format(uncustomized_bundle_id),
options=full_hardened_runtime_options,
verify_options=verify_options),
'helper-renderer-app':
CodeSignedProduct(
'{0.framework_dir}/Helpers/{0.product} Helper (Renderer).app'
.format(config),
'{}.helper.renderer'.format(uncustomized_bundle_id),
# Do not use |full_hardened_runtime_options| because library
# validation is incompatible with the JIT entitlement.
options=CodeSignOptions.RESTRICT + CodeSignOptions.KILL +
CodeSignOptions.HARDENED_RUNTIME,
entitlements='helper-renderer-entitlements.plist',
verify_options=verify_options),
'helper-gpu-app':
CodeSignedProduct(
'{0.framework_dir}/Helpers/{0.product} Helper (GPU).app'
.format(config),
'{}.helper'.format(uncustomized_bundle_id),
# Do not use |full_hardened_runtime_options| because library
# validation is incompatible with more permissive code signing
# entitlements.
options=CodeSignOptions.RESTRICT + CodeSignOptions.KILL +
CodeSignOptions.HARDENED_RUNTIME,
entitlements='helper-gpu-entitlements.plist',
verify_options=verify_options),
'helper-plugin-app':
CodeSignedProduct(
'{0.framework_dir}/Helpers/{0.product} Helper (Plugin).app'
.format(config),
'{}.helper.plugin'.format(uncustomized_bundle_id),
# Do not use |full_hardened_runtime_options| because library
# validation is incompatible with the disable-library-validation
# entitlement.
options=CodeSignOptions.RESTRICT + CodeSignOptions.KILL +
CodeSignOptions.HARDENED_RUNTIME,
entitlements='helper-plugin-entitlements.plist',
verify_options=verify_options),
'app-mode-app':
CodeSignedProduct(
'{.framework_dir}/Helpers/app_mode_loader'.format(config),
'app_mode_loader',
options=full_hardened_runtime_options,
verify_options=verify_options),
}
dylibs = (
'libEGL.dylib',
'libGLESv2.dylib',
'libswiftshader_libEGL.dylib',
'libswiftshader_libGLESv2.dylib',
)
for library in dylibs:
library_basename = os.path.basename(library)
parts[library_basename] = CodeSignedProduct(
'{.framework_dir}/Libraries/{library}'.format(
config, library=library),
library_basename.replace('.dylib', ''),
verify_options=verify_options)
return parts
def get_installer_tools(config):
"""Returns all the |model.CodeSignedProduct| objects to be signed for
creating the installer tools package.
Args:
config: The |config.CodeSignConfig|.
Returns:
A dictionary of |model.CodeSignedProduct|. The keys are short
identifiers that have no bearing on the actual signing operations.
"""
tools = {}
binaries = (
'goobsdiff',
'goobspatch',
'liblzma_decompress.dylib',
'xz',
'xzdec',
)
for binary in binaries:
options = (
CodeSignOptions.HARDENED_RUNTIME + CodeSignOptions.RESTRICT +
CodeSignOptions.LIBRARY_VALIDATION + CodeSignOptions.KILL)
tools[binary] = CodeSignedProduct(
'{.packaging_dir}/{binary}'.format(config, binary=binary),
binary.replace('.dylib', ''),
options=options if not binary.endswith('dylib') else None,
verify_options=VerifyOptions.DEEP + VerifyOptions.STRICT)
return tools
def sign_part(paths, config, part): def sign_part(paths, config, part):
...@@ -217,95 +57,17 @@ def verify_part(paths, part): ...@@ -217,95 +57,17 @@ def verify_part(paths, part):
verify_options + [part_path]) verify_options + [part_path])
def _validate_chrome(paths, config, app): def validate_app(paths, config, part):
"""Displays and verifies the signature of the outer Chrome application """Displays and verifies the signature of a CodeSignedProduct.
bundle.
Args: Args:
paths: A |model.Paths| object. paths: A |model.Paths| object.
conifg: The |model.CodeSignConfig| object. conifg: The |model.CodeSignConfig| object.
part: The |model.CodeSignedProduct| for the outer application bundle. part: The |model.CodeSignedProduct| for the outer application bundle.
""" """
app_path = os.path.join(paths.work, app.path) app_path = os.path.join(paths.work, part.path)
commands.run_command([ commands.run_command([
'codesign', '--display', '--requirements', '-', '--verbose=5', app_path 'codesign', '--display', '--requirements', '-', '--verbose=5', app_path
]) ])
if config.run_spctl_assess: if config.run_spctl_assess:
commands.run_command(['spctl', '--assess', '-vv', app_path]) commands.run_command(['spctl', '--assess', '-vv', app_path])
def sign_chrome(paths, config, sign_framework=False):
"""Code signs the Chrome application bundle and all of its internal nested
code parts.
Args:
paths: A |model.Paths| object.
config: The |model.CodeSignConfig| object. The |app_product| binary and
nested binaries must exist in |paths.work|.
sign_framework: True if the inner framework is to be signed in addition
to the outer application. False if only the outer application is to
be signed.
"""
parts = get_parts(config)
_sanity_check_version_keys(paths, parts)
if sign_framework:
# To sign an .app bundle that contains nested code, the nested
# components themselves must be signed. Each of these components is
# signed below. Note that unless a framework has multiple versions
# (which is discouraged), signing the entire framework is equivalent to
# signing the Current version.
# https://developer.apple.com/library/content/technotes/tn2206/_index.html#//apple_ref/doc/uid/DTS40007919-CH1-TNTAG13
for name, part in parts.items():
if name in ('app', 'framework'):
continue
sign_part(paths, config, part)
# Sign the framework bundle.
sign_part(paths, config, parts['framework'])
provisioning_profile_basename = config.provisioning_profile_basename
if provisioning_profile_basename:
commands.copy_files(
os.path.join(
paths.packaging_dir(config),
provisioning_profile_basename + _PROVISIONPROFILE_EXT),
os.path.join(paths.work, parts['app'].path, 'Contents',
_PROVISIONPROFILE_DEST))
# Sign the outer app bundle.
sign_part(paths, config, parts['app'])
# Verify all the parts.
for part in parts.values():
verify_part(paths, part)
# Display the code signature.
_validate_chrome(paths, config, parts['app'])
def _sanity_check_version_keys(paths, parts):
"""Verifies that the various version keys in Info.plists match.
Args:
paths: A |model.Paths| object.
parts: The dictionary returned from get_parts().
"""
app_plist_path = os.path.join(paths.work, parts['app'].path, 'Contents',
'Info.plist')
framework_plist_path = os.path.join(paths.work, parts['framework'].path,
'Resources', 'Info.plist')
with commands.PlistContext(
app_plist_path) as app_plist, commands.PlistContext(
framework_plist_path) as framework_plist:
if not 'KSVersion' in app_plist:
assert 'com.google.Chrome' not in app_plist['CFBundleIdentifier']
return
ks_version = app_plist['KSVersion']
cf_version = framework_plist['CFBundleShortVersionString']
if cf_version != ks_version:
raise ValueError(
'CFBundleVersion ({}) does not mach KSVersion ({})'.format(
cf_version, ks_version))
...@@ -15,94 +15,6 @@ except NameError: ...@@ -15,94 +15,6 @@ except NameError:
FileNotFoundError = IOError FileNotFoundError = IOError
class TestGetParts(unittest.TestCase):
def test_get_parts_no_base(self):
config = test_config.TestConfig()
all_parts = signing.get_parts(config)
self.assertEqual('test.signing.bundle_id', all_parts['app'].identifier)
self.assertEqual('test.signing.bundle_id.framework',
all_parts['framework'].identifier)
self.assertEqual(
'test.signing.bundle_id.framework.AlertNotificationService',
all_parts['notification-xpc'].identifier)
self.assertEqual('test.signing.bundle_id.helper',
all_parts['helper-app'].identifier)
def test_get_parts_no_customize(self):
config = model.Distribution(channel='dev').to_config(
test_config.TestConfig())
all_parts = signing.get_parts(config)
self.assertEqual('test.signing.bundle_id', all_parts['app'].identifier)
self.assertEqual('test.signing.bundle_id.framework',
all_parts['framework'].identifier)
self.assertEqual(
'test.signing.bundle_id.framework.AlertNotificationService',
all_parts['notification-xpc'].identifier)
self.assertEqual('test.signing.bundle_id.helper',
all_parts['helper-app'].identifier)
def test_get_parts_customize(self):
config = model.Distribution(
channel='canary',
channel_customize=True).to_config(test_config.TestConfig())
all_parts = signing.get_parts(config)
self.assertEqual('test.signing.bundle_id.canary',
all_parts['app'].identifier)
self.assertEqual('test.signing.bundle_id.framework',
all_parts['framework'].identifier)
self.assertEqual(
'test.signing.bundle_id.canary.framework.AlertNotificationService',
all_parts['notification-xpc'].identifier)
self.assertEqual('test.signing.bundle_id.helper',
all_parts['helper-app'].identifier)
def test_part_options(self):
parts = signing.get_parts(test_config.TestConfig())
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(parts['app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(parts['helper-app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT + model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(parts['helper-renderer-app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT + model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(parts['helper-gpu-app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT + model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(parts['helper-plugin-app'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(parts['crashpad'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(parts['notification-xpc'].options))
self.assertEqual(
set(model.CodeSignOptions.RESTRICT +
model.CodeSignOptions.LIBRARY_VALIDATION +
model.CodeSignOptions.KILL +
model.CodeSignOptions.HARDENED_RUNTIME),
set(parts['app-mode-app'].options))
@mock.patch('signing.commands.run_command') @mock.patch('signing.commands.run_command')
class TestSignPart(unittest.TestCase): class TestSignPart(unittest.TestCase):
...@@ -209,168 +121,3 @@ class TestSignPart(unittest.TestCase): ...@@ -209,168 +121,3 @@ class TestSignPart(unittest.TestCase):
'--ignore-resources', '$W/Test.app' '--ignore-resources', '$W/Test.app'
]), ]),
]) ])
def _get_plist_read(other_version):
def _plist_read(*args):
path = args[0]
first_slash = path.find('/')
path = path[first_slash + 1:]
plists = {
'App Product.app/Contents/Info.plist': {
'KSVersion': '99.0.9999.99'
},
'App Product.app/Contents/Frameworks/Product Framework.framework/Resources/Info.plist':
{
'CFBundleShortVersionString': other_version
}
}
return plists[path]
return _plist_read
@mock.patch.multiple('signing.signing',
**{m: mock.DEFAULT for m in ('sign_part', 'verify_part')})
@mock.patch.multiple('signing.commands', **{
m: mock.DEFAULT
for m in ('copy_files', 'move_file', 'make_dir', 'run_command')
})
class TestSignChrome(unittest.TestCase):
def setUp(self):
self.paths = model.Paths('$I', '$O', '$W')
@mock.patch('signing.signing._sanity_check_version_keys')
def test_sign_chrome(self, *args, **kwargs):
manager = mock.Mock()
for kwarg in kwargs:
manager.attach_mock(kwargs[kwarg], kwarg)
dist = model.Distribution()
config = dist.to_config(test_config.TestConfig())
signing.sign_chrome(self.paths, config, sign_framework=True)
# No files should be moved.
self.assertEqual(0, kwargs['move_file'].call_count)
# Test that the provisioning profile is copied.
self.assertEqual(kwargs['copy_files'].mock_calls, [
mock.call.copy_files(
'$I/Product Packaging/provisiontest.provisionprofile',
'$W/App Product.app/Contents/embedded.provisionprofile')
])
# Ensure that all the parts are signed.
signed_paths = [
call[1][2].path for call in kwargs['sign_part'].mock_calls
]
self.assertEqual(
set([p.path for p in signing.get_parts(config).values()]),
set(signed_paths))
# Make sure that the framework and the app are the last two parts that
# are signed.
self.assertEqual(signed_paths[-2:], [
'App Product.app/Contents/Frameworks/Product Framework.framework',
'App Product.app'
])
self.assertEqual(kwargs['run_command'].mock_calls, [
mock.call.run_command([
'codesign', '--display', '--requirements', '-', '--verbose=5',
'$W/App Product.app'
]),
mock.call.run_command(
['spctl', '--assess', '-vv', '$W/App Product.app']),
])
@mock.patch('signing.signing._sanity_check_version_keys')
def test_sign_chrome_no_assess(self, *args, **kwargs):
dist = model.Distribution()
class Config(test_config.TestConfig):
@property
def run_spctl_assess(self):
return False
config = dist.to_config(Config())
signing.sign_chrome(self.paths, config, sign_framework=True)
self.assertEqual(kwargs['run_command'].mock_calls, [
mock.call.run_command([
'codesign', '--display', '--requirements', '-', '--verbose=5',
'$W/App Product.app'
]),
])
@mock.patch('signing.signing._sanity_check_version_keys')
def test_sign_chrome_no_provisioning(self, *args, **kwargs):
dist = model.Distribution()
class Config(test_config.TestConfig):
@property
def provisioning_profile_basename(self):
return None
config = dist.to_config(Config())
signing.sign_chrome(self.paths, config, sign_framework=True)
self.assertEqual(0, kwargs['copy_files'].call_count)
@mock.patch('signing.signing._sanity_check_version_keys')
def test_sign_chrome_no_framework(self, *args, **kwargs):
manager = mock.Mock()
for kwarg in kwargs:
manager.attach_mock(kwargs[kwarg], kwarg)
dist = model.Distribution()
config = dist.to_config(test_config.TestConfig())
signing.sign_chrome(self.paths, config, sign_framework=False)
# No files should be moved.
self.assertEqual(0, kwargs['move_file'].call_count)
# Test that the provisioning profile is copied.
self.assertEqual(kwargs['copy_files'].mock_calls, [
mock.call.copy_files(
'$I/Product Packaging/provisiontest.provisionprofile',
'$W/App Product.app/Contents/embedded.provisionprofile')
])
# Ensure that only the app is signed.
signed_paths = [
call[1][2].path for call in kwargs['sign_part'].mock_calls
]
self.assertEqual(signed_paths, ['App Product.app'])
self.assertEqual(kwargs['run_command'].mock_calls, [
mock.call.run_command([
'codesign', '--display', '--requirements', '-', '--verbose=5',
'$W/App Product.app'
]),
mock.call.run_command(
['spctl', '--assess', '-vv', '$W/App Product.app']),
])
@mock.patch(
'signing.commands.plistlib.readPlist',
side_effect=_get_plist_read('99.0.9999.99'))
def test_sanity_check_ok(self, read_plist, **kwargs):
config = model.Distribution().to_config(test_config.TestConfig())
signing.sign_chrome(self.paths, config, sign_framework=True)
@mock.patch(
'signing.commands.plistlib.readPlist',
side_effect=_get_plist_read('55.0.5555.55'))
def test_sanity_check_bad(self, read_plist, **kwargs):
config = model.Distribution().to_config(test_config.TestConfig())
self.assertRaises(ValueError,
lambda: signing.sign_chrome(self.paths, config, sign_framework=True))
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment