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