blob: 5a0a40a363918706cfa33d1c62f11679ed453bc5 [file] [log] [blame]
# Copyright 2019 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.
"""Signing Model Objects
This module contains classes that encapsulate data about the signing process.
"""
import os.path
class CodeSignedProduct(object):
"""Represents a build product that will be signed with `codesign(1)`."""
def __init__(self,
path,
identifier,
options=None,
requirements=None,
identifier_requirement=True,
sign_with_identifier=False,
resource_rules=None,
entitlements=None,
verify_options=None):
"""A build product to be codesigned.
Args:
path: The path to the product to be signed. This is relative to a
work directory containing the build products.
identifier: The unique identifier set when code signing. This is
only explicitly passed with the `--identifier` flag if
|sign_with_identifier| is True.
options: Options flags to pass to `codesign --options`, from
|CodeSignOptions|.
requirements: String for additional `--requirements` to pass to the
`codesign` command. These are joined with a space to the
|config.CodeSignConfig.codesign_requirements_basic| string. See
|CodeSignedProduct.requirements_string()| for details.
identifier_requirement: If True, a designated identifier requirement
based on |identifier| will be inserted into the requirements
string. If False, then no designated requirement will be
generated based on the identifier.
sign_with_identifier: If True, then the identifier will be specified
when running the `codesign` command. If False, `codesign` will
infer the identifier itself.
resource_rules: A file in |Paths.packaging_dir| to specify for
`codesign --resource-rules`. macOS has deprecated resource rules
and this should not be used for new products.
entitlements: File name of the entitlements file to sign the product
with. The file should reside in the |Paths.packaging_dir|.
verify_options: Flags to pass to `codesign --verify`, from
|VerifyOptions|.
"""
self.path = path
self.identifier = identifier
if not CodeSignOptions.valid(options):
raise ValueError('Invalid CodeSignOptions: {}'.format(options))
self.options = options
self.requirements = requirements
self.identifier_requirement = identifier_requirement
self.sign_with_identifier = sign_with_identifier
self.resource_rules = resource_rules
self.entitlements = entitlements
if not VerifyOptions.valid(verify_options):
raise ValueError('Invalid VerifyOptions: {}'.format(verify_options))
self.verify_options = verify_options
def requirements_string(self, config):
"""Produces a full requirements string for the product.
Args:
config: A |config.CodeSignConfig| object.
Returns:
A string for designated requirements of the product, which can be
passed to `codesign --requirements`.
"""
# If the signing identity indicates ad-hoc (i.e. no real signing
# identity), do not enforce any requirements. Ad hoc signing will append
# a hash to the identifier, which would violate the
# identifier_requirement and most other requirements that would be
# specified.
if config.identity == '-':
return ''
reqs = []
if self.identifier_requirement:
reqs.append('designated => identifier "{identifier}"'.format(
identifier=self.identifier))
if self.requirements:
reqs.append(self.requirements)
if config.codesign_requirements_basic:
reqs.append(config.codesign_requirements_basic)
return ' '.join(reqs)
def __repr__(self):
return 'CodeSignedProduct(identifier={0.identifier}, ' \
'options={0.options}, path={0.path})'.format(self)
def make_enum(class_name, options):
"""Makes a new class type for an enum.
Args:
class_name: Name of the new type to make.
options: A dictionary of enum options to use. The keys will become
attributes on the class, and the values will be wrapped in a tuple
so that the options can be joined together.
Returns:
A new class for the enum.
"""
attrs = {}
@classmethod
def valid(cls, opts_to_check):
"""Tests if the specified |opts_to_check| are valid.
Args:
options: Iterable of option strings.
Returns:
True if all the options are valid, False if otherwise.
"""
if opts_to_check is None:
return True
valid_values = options.values()
return all([option in valid_values for option in opts_to_check])
attrs['valid'] = valid
for name, value in options.items():
assert type(name) is str
assert type(value) is str
attrs[name] = (value,)
return type(class_name, (object,), attrs)
"""Enum for the options that can be specified when validating the results of
code signing.
These options are passed to `codesign --verify` after the
|CodeSignedProduct| has been signed.
"""
VerifyOptions = make_enum(
'signing.model.VerifyOptions', {
'DEEP': '--deep',
'NO_STRICT': '--no-strict',
'IGNORE_RESOURCES': '--ignore-resources',
})
CodeSignOptions = make_enum(
'signing.model.CodeSignOptions', {
'RESTRICT': 'restrict',
'LIBRARY_VALIDATION': 'library',
'HARDENED_RUNTIME': 'runtime',
'KILL': 'kill',
})
class Distribution(object):
"""A Distribution represents a final, signed, and potentially channel-
customized Chrome product.
Channel customization refers to modifying parts of the app bundle structure
to have different file names, internal identifiers, and assets.
"""
def __init__(self,
channel=None,
branding_code=None,
app_name_fragment=None,
dmg_name_fragment=None,
product_dirname=None,
creator_code=None,
channel_customize=False):
"""Creates a new Distribution object. All arguments are optional.
Args:
channel: The release channel for the product.
branding_code: A branding code helps track how users acquired the
product from various marketing channels.
app_name_fragment: If present, this string fragment is appended to
the |config.CodeSignConfig.app_product|. This renames the binary
and outer app bundle.
dmg_name_fragment: If present, this is appended to the
|config.CodeSignConfig.dmg_basename| to help differentiate
different |branding_code|s.
product_dirname: If present, this string value is set in the app's
Info.plist with the key "CrProductDirName". This key influences
the browser's default user-data-dir location.
creator_code: If present, this will set a new macOS creator code
in the Info.plist "CFBundleSignature" key and in the PkgInfo
file. If this is not specified, the original values from the
build products will be kept.
channel_customize: If True, then the product will be modified in
several ways:
- The |channel| will be appended to the
|config.CodeSignConfig.base_bundle_id|.
- The product will be renamed with |app_name_fragment|.
- Different assets will be used for icons in the app.
"""
self.channel = channel
self.branding_code = branding_code
self.app_name_fragment = app_name_fragment
self.dmg_name_fragment = dmg_name_fragment
self.product_dirname = product_dirname
self.creator_code = creator_code
self.channel_customize = channel_customize
def to_config(self, base_config):
"""Produces a derived |config.CodeSignConfig| for the Distribution.
Args:
base_config: The base CodeSignConfig to derive.
Returns:
A new CodeSignConfig instance that uses information in the
Distribution to alter various properties of the |base_config|.
"""
this = self
class DistributionCodeSignConfig(base_config.__class__):
@property
def base_config(self):
return base_config
@property
def distribution(self):
return this
@property
def app_product(self):
if this.channel_customize:
return '{} {}'.format(base_config.app_product,
this.app_name_fragment)
return base_config.app_product
@property
def base_bundle_id(self):
base_bundle_id = base_config.base_bundle_id
if this.channel_customize:
return base_bundle_id + '.' + this.channel
return base_bundle_id
@property
def provisioning_profile_basename(self):
profile = base_config.provisioning_profile_basename
if profile and this.channel_customize:
return '{}_{}'.format(profile, this.app_name_fragment)
return profile
@property
def dmg_basename(self):
if this.dmg_name_fragment:
return '{}-{}-{}'.format(
self.app_product.replace(' ', ''), self.version,
this.dmg_name_fragment)
return super(DistributionCodeSignConfig, self).dmg_basename
return DistributionCodeSignConfig(
base_config.identity, base_config.keychain, base_config.notary_user,
base_config.notary_password)
class Paths(object):
"""Paths holds the three file path contexts for signing operations.
The input directory always remains un-modified.
The output directory is where final, signed products are stored.
The work directory is set by internal operations.
"""
def __init__(self, input, output, work):
self._input = input
self._output = output
self._work = work
@property
def input(self):
return self._input
@property
def output(self):
return self._output
@property
def work(self):
return self._work
def packaging_dir(self, config):
"""Returns the path to the product packaging directory, which contains
scripts and assets used in signing.
Args:
config: The |config.CodeSignConfig| object.
Returns:
Path to the packaging directory.
"""
return os.path.join(self.input, '{} Packaging'.format(config.product))
def replace_work(self, new_work):
"""Creates a new Paths with the same input and output directories, but
with |work| set to |new_work|."""
return Paths(self.input, self.output, new_work)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return (self._input == other._input and
self._output == other._output and self._work == other._work)
def __repr__(self):
return 'Paths(input={0.input}, output={0.output}, ' \
'work={0.work})'.format(self)