1# Copyright 2019 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"""Signing Model Objects
5
6This module contains classes that encapsulate data about the signing process.
7"""
8
9import os.path
10
11
12class CodeSignedProduct(object):
13    """Represents a build product that will be signed with `codesign(1)`."""
14
15    def __init__(self,
16                 path,
17                 identifier,
18                 options=None,
19                 requirements=None,
20                 identifier_requirement=True,
21                 sign_with_identifier=False,
22                 entitlements=None,
23                 verify_options=None):
24        """A build product to be codesigned.
25
26        Args:
27            path: The path to the product to be signed. This is relative to a
28                work directory containing the build products.
29            identifier: The unique identifier set when code signing. This is
30                only explicitly passed with the `--identifier` flag if
31                |sign_with_identifier| is True.
32            options: Options flags to pass to `codesign --options`, from
33                |CodeSignOptions|.
34            requirements: String for additional `--requirements` to pass to the
35                `codesign` command. These are joined with a space to the
36                |config.CodeSignConfig.codesign_requirements_basic| string. See
37                |CodeSignedProduct.requirements_string()| for details.
38            identifier_requirement: If True, a designated identifier requirement
39                based on |identifier| will be inserted into the requirements
40                string. If False, then no designated requirement will be
41                generated based on the identifier.
42            sign_with_identifier: If True, then the identifier will be specified
43                when running the `codesign` command. If False, `codesign` will
44                infer the identifier itself.
45            entitlements: File name of the entitlements file to sign the product
46                with. The file should reside in the |Paths.packaging_dir|.
47            verify_options: Flags to pass to `codesign --verify`, from
48                |VerifyOptions|.
49        """
50        self.path = path
51        self.identifier = identifier
52        if not CodeSignOptions.valid(options):
53            raise ValueError('Invalid CodeSignOptions: {}'.format(options))
54        self.options = options
55        self.requirements = requirements
56        self.identifier_requirement = identifier_requirement
57        self.sign_with_identifier = sign_with_identifier
58        self.entitlements = entitlements
59        if not VerifyOptions.valid(verify_options):
60            raise ValueError('Invalid VerifyOptions: {}'.format(verify_options))
61        self.verify_options = verify_options
62
63    def requirements_string(self, config):
64        """Produces a full requirements string for the product.
65
66        Args:
67            config: A |config.CodeSignConfig| object.
68
69        Returns:
70            A string for designated requirements of the product, which can be
71            passed to `codesign --requirements`.
72        """
73        # If the signing identity indicates ad-hoc (i.e. no real signing
74        # identity), do not enforce any requirements. Ad hoc signing will append
75        # a hash to the identifier, which would violate the
76        # identifier_requirement and most other requirements that would be
77        # specified.
78        if config.identity == '-':
79            return ''
80
81        reqs = []
82        if self.identifier_requirement:
83            reqs.append('designated => identifier "{identifier}"'.format(
84                identifier=self.identifier))
85        if self.requirements:
86            reqs.append(self.requirements)
87        if config.codesign_requirements_basic:
88            reqs.append(config.codesign_requirements_basic)
89        return ' '.join(reqs)
90
91    def __repr__(self):
92        return 'CodeSignedProduct(identifier={0.identifier}, ' \
93                'options={0.options}, path={0.path})'.format(self)
94
95
96def make_enum(class_name, options):
97    """Makes a new class type for an enum.
98
99    Args:
100        class_name: Name of the new type to make.
101        options: A dictionary of enum options to use. The keys will become
102            attributes on the class, and the values will be wrapped in a tuple
103            so that the options can be joined together.
104
105    Returns:
106        A new class for the enum.
107    """
108    attrs = {}
109
110    @classmethod
111    def valid(cls, opts_to_check):
112        """Tests if the specified |opts_to_check| are valid.
113
114        Args:
115            options: Iterable of option strings.
116
117        Returns:
118            True if all the options are valid, False if otherwise.
119        """
120        if opts_to_check is None:
121            return True
122        valid_values = options.values()
123        return all([option in valid_values for option in opts_to_check])
124
125    attrs['valid'] = valid
126
127    for name, value in options.items():
128        assert type(name) is str
129        assert type(value) is str
130        attrs[name] = (value,)
131
132    return type(class_name, (object,), attrs)
133
134
135"""Enum for the options that can be specified when validating the results of
136code signing.
137
138These options are passed to `codesign --verify` after the
139|CodeSignedProduct| has been signed.
140"""
141VerifyOptions = make_enum(
142    'signing.model.VerifyOptions', {
143        'DEEP': '--deep',
144        'STRICT': '--strict',
145        'NO_STRICT': '--no-strict',
146        'IGNORE_RESOURCES': '--ignore-resources',
147    })
148
149CodeSignOptions = make_enum(
150    'signing.model.CodeSignOptions', {
151        'RESTRICT': 'restrict',
152        'LIBRARY_VALIDATION': 'library',
153        'HARDENED_RUNTIME': 'runtime',
154        'KILL': 'kill',
155    })
156
157# Specify the components of HARDENED_RUNTIME that are also available on
158# older macOS versions.
159CodeSignOptions.FULL_HARDENED_RUNTIME_OPTIONS = (
160    CodeSignOptions.HARDENED_RUNTIME + CodeSignOptions.RESTRICT +
161    CodeSignOptions.LIBRARY_VALIDATION + CodeSignOptions.KILL)
162
163
164class Distribution(object):
165    """A Distribution represents a final, signed, and potentially channel-
166    customized Chrome product.
167
168    Channel customization refers to modifying parts of the app bundle structure
169    to have different file names, internal identifiers, and assets.
170    """
171
172    def __init__(self,
173                 channel=None,
174                 branding_code=None,
175                 app_name_fragment=None,
176                 packaging_name_fragment=None,
177                 product_dirname=None,
178                 creator_code=None,
179                 channel_customize=False,
180                 package_as_dmg=True,
181                 package_as_pkg=False,
182                 inflation_kilobytes=0):
183        """Creates a new Distribution object. All arguments are optional.
184
185        Args:
186            channel: The release channel for the product.
187            branding_code: A branding code helps track how users acquired the
188                product from various marketing channels.
189            app_name_fragment: If present, this string fragment is appended to
190                the |config.CodeSignConfig.app_product|. This renames the binary
191                and outer app bundle.
192            packaging_name_fragment: If present, this is appended to the
193                |config.CodeSignConfig.packaging_basename| to help differentiate
194                different |branding_code|s.
195            product_dirname: If present, this string value is set in the app's
196                Info.plist with the key "CrProductDirName". This key influences
197                the browser's default user-data-dir location.
198            creator_code: If present, this will set a new macOS creator code
199                in the Info.plist "CFBundleSignature" key and in the PkgInfo
200                file. If this is not specified, the original values from the
201                build products will be kept.
202            channel_customize: If True, then the product will be modified in
203                several ways:
204                - The |channel| will be appended to the
205                  |config.CodeSignConfig.base_bundle_id|.
206                - The product will be renamed with |app_name_fragment|.
207                - Different assets will be used for icons in the app.
208            package_as_dmg: If True, then a .dmg file will be created containing
209                the product.
210            package_as_pkg: If True, then a .pkg file will be created containing
211                the product.
212            inflation_kilobytes: If non-zero, a blob of this size will be
213                inserted into the DMG. Incompatible with package_as_pkg = True.
214        """
215        if channel_customize:
216            # Side-by-side channels must have a distinct names and creator
217            # codes, as well as keep their user data in separate locations.
218            assert channel
219            assert app_name_fragment
220            assert product_dirname
221            assert creator_code
222
223        self.channel = channel
224        self.branding_code = branding_code
225        self.app_name_fragment = app_name_fragment
226        self.packaging_name_fragment = packaging_name_fragment
227        self.product_dirname = product_dirname
228        self.creator_code = creator_code
229        self.channel_customize = channel_customize
230        self.package_as_dmg = package_as_dmg
231        self.package_as_pkg = package_as_pkg
232        self.inflation_kilobytes = inflation_kilobytes
233
234        # inflation_kilobytes are only inserted into DMGs
235        assert not self.inflation_kilobytes or self.package_as_dmg
236
237    def brandless_copy(self):
238        """Derives and returns a copy of this Distribution object, identical
239        except for not having a branding code.
240
241        This is useful in the case where a non-branded app bundle needs to be
242        created with otherwise the same configuration.
243        """
244        return Distribution(self.channel, None, self.app_name_fragment,
245                            self.packaging_name_fragment, self.product_dirname,
246                            self.creator_code, self.channel_customize,
247                            self.package_as_dmg, self.package_as_pkg)
248
249    def to_config(self, base_config):
250        """Produces a derived |config.CodeSignConfig| for the Distribution.
251
252        Args:
253            base_config: The base CodeSignConfig to derive.
254
255        Returns:
256            A new CodeSignConfig instance that uses information in the
257            Distribution to alter various properties of the |base_config|.
258        """
259        this = self
260
261        class DistributionCodeSignConfig(base_config.__class__):
262
263            @property
264            def base_config(self):
265                return base_config
266
267            @property
268            def distribution(self):
269                return this
270
271            @property
272            def app_product(self):
273                if this.channel_customize:
274                    return '{} {}'.format(base_config.app_product,
275                                          this.app_name_fragment)
276                return base_config.app_product
277
278            @property
279            def base_bundle_id(self):
280                base_bundle_id = base_config.base_bundle_id
281                if this.channel_customize:
282                    return base_bundle_id + '.' + this.channel
283                return base_bundle_id
284
285            @property
286            def provisioning_profile_basename(self):
287                profile = base_config.provisioning_profile_basename
288                if profile and this.channel_customize:
289                    return '{}_{}'.format(profile, this.app_name_fragment)
290                return profile
291
292            @property
293            def packaging_basename(self):
294                if this.packaging_name_fragment:
295                    return '{}-{}-{}'.format(
296                        self.app_product.replace(' ', ''), self.version,
297                        this.packaging_name_fragment)
298                return super(DistributionCodeSignConfig,
299                             self).packaging_basename
300
301        return DistributionCodeSignConfig(base_config.identity,
302                                          base_config.installer_identity,
303                                          base_config.notary_user,
304                                          base_config.notary_password,
305                                          base_config.notary_asc_provider)
306
307
308class Paths(object):
309    """Paths holds the three file path contexts for signing operations.
310
311    The input directory always remains un-modified.
312    The output directory is where final, signed products are stored.
313    The work directory is set by internal operations.
314    """
315
316    def __init__(self, input, output, work):
317        self._input = os.path.abspath(input)
318        self._output = os.path.abspath(output)
319        self._work = work
320        if self._work:
321            self._work = os.path.abspath(self._work)
322
323    @property
324    def input(self):
325        return self._input
326
327    @property
328    def output(self):
329        return self._output
330
331    @property
332    def work(self):
333        return self._work
334
335    def packaging_dir(self, config):
336        """Returns the path to the product packaging directory, which contains
337        scripts and assets used in signing.
338
339        Args:
340            config: The |config.CodeSignConfig| object.
341
342        Returns:
343            Path to the packaging directory.
344        """
345        return os.path.join(self.input, '{} Packaging'.format(config.product))
346
347    def replace_work(self, new_work):
348        """Creates a new Paths with the same input and output directories, but
349        with |work| set to |new_work|."""
350        return Paths(self.input, self.output, new_work)
351
352    def __eq__(self, other):
353        if not isinstance(other, self.__class__):
354            return False
355        return (self._input == other._input and
356                self._output == other._output and self._work == other._work)
357
358    def __repr__(self):
359        return 'Paths(input={0.input}, output={0.output}, ' \
360                'work={0.work})'.format(self)
361