codesign.dart 14.9 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2014 The Flutter 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 'dart:io' as io;

import 'package:args/command_runner.dart';
import 'package:file/file.dart';
9
import 'package:meta/meta.dart' show visibleForTesting;
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
import 'package:platform/platform.dart';
import 'package:process/process.dart';

import './globals.dart';
import './repository.dart';
import './stdio.dart';

const List<String> expectedEntitlements = <String>[
  'com.apple.security.cs.allow-jit',
  'com.apple.security.cs.allow-unsigned-executable-memory',
  'com.apple.security.cs.allow-dyld-environment-variables',
  'com.apple.security.network.client',
  'com.apple.security.network.server',
  'com.apple.security.cs.disable-library-validation',
];

const String kVerify = 'verify';
const String kSignatures = 'signatures';
const String kRevision = 'revision';
const String kUpstream = 'upstream';

31

32 33 34
/// Command to codesign and verify the signatures of cached binaries.
class CodesignCommand extends Command<void> {
  CodesignCommand({
35 36 37 38
    required this.checkouts,
    required this.flutterRoot,
    FrameworkRepository? framework,
  })  : fileSystem = checkouts.fileSystem,
39 40 41
        platform = checkouts.platform,
        stdio = checkouts.stdio,
        processManager = checkouts.processManager {
42 43 44
    if (framework != null) {
      _framework = framework;
    }
45 46
    argParser.addFlag(
      kVerify,
47
      help: 'Only verify expected binaries exist and are codesigned with entitlements.',
48 49 50 51
    );
    argParser.addFlag(
      kSignatures,
      defaultsTo: true,
52 53
      help: 'When off, this command will only verify the existence of binaries, and not their\n'
            'signatures or entitlements. Must be used with --verify flag.',
54 55 56 57
    );
    argParser.addOption(
      kUpstream,
      defaultsTo: FrameworkRepository.defaultUpstream,
58
      help: "The git remote URL to use as the Flutter framework's upstream.",
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
    );
    argParser.addOption(
      kRevision,
      help: 'The Flutter framework revision to use.',
    );
  }

  final Checkouts checkouts;
  final FileSystem fileSystem;
  final Platform platform;
  final ProcessManager processManager;
  final Stdio stdio;

  /// Root directory of the Flutter repository.
  final Directory flutterRoot;

75
  FrameworkRepository? _framework;
76
  FrameworkRepository get framework {
77
    return _framework ??= FrameworkRepository(
78
      checkouts,
79 80 81 82
      upstreamRemote: Remote(
        name: RemoteName.upstream,
        url: argResults![kUpstream] as String,
      ),
83 84
    );
  }
85 86 87 88 89 90 91 92 93

  @override
  String get name => 'codesign';

  @override
  String get description =>
      'For codesigning and verifying the signatures of engine binaries.';

  @override
94
  Future<void> run() async {
95 96
    if (!platform.isMacOS) {
      throw ConductorException(
97 98 99
        'Error! Expected operating system "macos", actual operating system is: '
        '"${platform.operatingSystem}"',
      );
100 101
    }

102
    if (argResults!['verify'] as bool != true) {
103
      throw ConductorException(
104 105 106
        'Sorry, but codesigning is not implemented yet. Please pass the '
        '--$kVerify flag to verify signatures.',
      );
107 108 109
    }

    String revision;
110
    if (argResults!.wasParsed(kRevision)) {
111 112 113 114 115
      stdio.printWarning(
        'Warning! When providing an arbitrary revision, the contents of the cache may not '
        'match the expected binaries in the conductor tool. It is preferred to check out '
        'the desired revision and run that version of the conductor.\n',
      );
116
      revision = argResults![kRevision] as String;
117
    } else {
118
      revision = ((await processManager.run(
119
        <String>['git', 'rev-parse', 'HEAD'],
120
        workingDirectory: flutterRoot.path,
121
      )).stdout as String).trim();
122 123 124
      assert(revision.isNotEmpty);
    }

125
    await framework.checkout(revision);
126 127

    // Ensure artifacts present
128
    await framework.runFlutter(<String>['precache', '--android', '--ios', '--macos']);
129

130
    await verifyExist();
131
    if (argResults![kSignatures] as bool) {
132
      await verifySignatures();
133 134 135 136 137 138 139
    }
  }

  /// Binaries that are expected to be codesigned and have entitlements.
  ///
  /// This list should be kept in sync with the actual contents of Flutter's
  /// cache.
140 141
  Future<List<String>> get binariesWithEntitlements async {
    final String frameworkCacheDirectory = await framework.cacheDirectory;
142 143 144 145 146 147 148 149
    return <String>[
      'artifacts/engine/android-arm-profile/darwin-x64/gen_snapshot',
      'artifacts/engine/android-arm-release/darwin-x64/gen_snapshot',
      'artifacts/engine/android-arm64-profile/darwin-x64/gen_snapshot',
      'artifacts/engine/android-arm64-release/darwin-x64/gen_snapshot',
      'artifacts/engine/android-x64-profile/darwin-x64/gen_snapshot',
      'artifacts/engine/android-x64-release/darwin-x64/gen_snapshot',
      'artifacts/engine/darwin-x64-profile/gen_snapshot',
150 151
      'artifacts/engine/darwin-x64-profile/gen_snapshot_arm64',
      'artifacts/engine/darwin-x64-profile/gen_snapshot_x64',
152
      'artifacts/engine/darwin-x64-release/gen_snapshot',
153 154
      'artifacts/engine/darwin-x64-release/gen_snapshot_arm64',
      'artifacts/engine/darwin-x64-release/gen_snapshot_x64',
155 156
      'artifacts/engine/darwin-x64/flutter_tester',
      'artifacts/engine/darwin-x64/gen_snapshot',
157 158
      'artifacts/engine/darwin-x64/gen_snapshot_arm64',
      'artifacts/engine/darwin-x64/gen_snapshot_x64',
159 160 161 162 163 164 165 166 167 168 169 170 171 172
      'artifacts/engine/ios-profile/gen_snapshot_arm64',
      'artifacts/engine/ios-release/gen_snapshot_arm64',
      'artifacts/engine/ios/gen_snapshot_arm64',
      'artifacts/libimobiledevice/idevicescreenshot',
      'artifacts/libimobiledevice/idevicesyslog',
      'artifacts/libimobiledevice/libimobiledevice-1.0.6.dylib',
      'artifacts/libplist/libplist-2.0.3.dylib',
      'artifacts/openssl/libcrypto.1.1.dylib',
      'artifacts/openssl/libssl.1.1.dylib',
      'artifacts/usbmuxd/iproxy',
      'artifacts/usbmuxd/libusbmuxd-2.0.6.dylib',
      'dart-sdk/bin/dart',
      'dart-sdk/bin/dartaotruntime',
      'dart-sdk/bin/utils/gen_snapshot',
173 174
    ]
        .map((String relativePath) =>
175
            fileSystem.path.join(frameworkCacheDirectory, relativePath))
176
        .toList();
177 178 179 180 181 182
  }

  /// Binaries that are only expected to be codesigned.
  ///
  /// This list should be kept in sync with the actual contents of Flutter's
  /// cache.
183 184
  Future<List<String>> get binariesWithoutEntitlements async {
    final String frameworkCacheDirectory = await framework.cacheDirectory;
185 186 187 188 189
    return <String>[
      'artifacts/engine/darwin-x64-profile/FlutterMacOS.framework/Versions/A/FlutterMacOS',
      'artifacts/engine/darwin-x64-release/FlutterMacOS.framework/Versions/A/FlutterMacOS',
      'artifacts/engine/darwin-x64/FlutterMacOS.framework/Versions/A/FlutterMacOS',
      'artifacts/engine/darwin-x64/font-subset',
190
      'artifacts/engine/darwin-x64/impellerc',
191
      'artifacts/engine/darwin-x64/libpath_ops.dylib',
192
      'artifacts/engine/darwin-x64/libtessellator.dylib',
193
      'artifacts/engine/ios-profile/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter',
194
      'artifacts/engine/ios-profile/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter',
195
      'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter',
196
      'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter',
197
      'artifacts/engine/ios/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter',
198
      'artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter',
199
      'artifacts/ios-deploy/ios-deploy',
200 201
    ]
        .map((String relativePath) =>
202
            fileSystem.path.join(frameworkCacheDirectory, relativePath))
203
        .toList();
204 205 206 207 208 209 210 211 212 213
  }

  /// Verify the existence of all expected binaries in cache.
  ///
  /// This function ignores code signatures and entitlements, and is intended to
  /// be run on every commit. It should throw if either new binaries are added
  /// to the cache or expected binaries removed. In either case, this class'
  /// [binariesWithEntitlements] or [binariesWithoutEntitlements] lists should
  /// be updated accordingly.
  @visibleForTesting
214
  Future<void> verifyExist() async {
215
    final Set<String> foundFiles = <String>{};
216 217 218
    for (final String binaryPath
        in await findBinaryPaths(await framework.cacheDirectory)) {
      if ((await binariesWithEntitlements).contains(binaryPath)) {
219
        foundFiles.add(binaryPath);
220
      } else if ((await binariesWithoutEntitlements).contains(binaryPath)) {
221 222
        foundFiles.add(binaryPath);
      } else {
223 224
        throw ConductorException(
            'Found unexpected binary in cache: $binaryPath');
225 226 227
      }
    }

228
    final List<String> allExpectedFiles =
229
        (await binariesWithEntitlements) + (await binariesWithoutEntitlements);
230
    if (foundFiles.length < allExpectedFiles.length) {
231 232 233 234 235 236
      final List<String> unfoundFiles = allExpectedFiles
          .where(
            (String file) => !foundFiles.contains(file),
          )
          .toList();
      stdio.printError(
237 238 239 240 241
        'Expected binaries not found in cache:\n\n${unfoundFiles.join('\n')}\n\n'
        'If this commit is removing binaries from the cache, this test should be fixed by\n'
        'removing the relevant entry from either the "binariesWithEntitlements" or\n'
        '"binariesWithoutEntitlements" getters in dev/tools/lib/codesign.dart.',
      );
242 243 244 245 246 247 248 249
      throw ConductorException('Did not find all expected binaries!');
    }

    stdio.printStatus('All expected binaries present.');
  }

  /// Verify code signatures and entitlements of all binaries in the cache.
  @visibleForTesting
250
  Future<void> verifySignatures() async {
251 252 253
    final List<String> unsignedBinaries = <String>[];
    final List<String> wrongEntitlementBinaries = <String>[];
    final List<String> unexpectedBinaries = <String>[];
254 255
    for (final String binaryPath
        in await findBinaryPaths(await framework.cacheDirectory)) {
256 257
      bool verifySignature = false;
      bool verifyEntitlements = false;
258
      if ((await binariesWithEntitlements).contains(binaryPath)) {
259 260 261
        verifySignature = true;
        verifyEntitlements = true;
      }
262
      if ((await binariesWithoutEntitlements).contains(binaryPath)) {
263 264 265 266 267 268 269 270
        verifySignature = true;
      }
      if (!verifySignature && !verifyEntitlements) {
        unexpectedBinaries.add(binaryPath);
        stdio.printError('Unexpected binary $binaryPath found in cache!');
        continue;
      }
      stdio.printTrace('Verifying the code signature of $binaryPath');
271
      final io.ProcessResult codeSignResult = await processManager.run(
272 273 274 275 276 277 278 279 280
        <String>[
          'codesign',
          '-vvv',
          binaryPath,
        ],
      );
      if (codeSignResult.exitCode != 0) {
        unsignedBinaries.add(binaryPath);
        stdio.printError(
281 282 283 284
          'File "$binaryPath" does not appear to be codesigned.\n'
          'The `codesign` command failed with exit code ${codeSignResult.exitCode}:\n'
          '${codeSignResult.stderr}\n',
        );
285 286 287 288
        continue;
      }
      if (verifyEntitlements) {
        stdio.printTrace('Verifying entitlements of $binaryPath');
289
        if (!(await hasExpectedEntitlements(binaryPath))) {
290 291 292 293 294 295 296 297
          wrongEntitlementBinaries.add(binaryPath);
        }
      }
    }

    // First print all deviations from expectations
    if (unsignedBinaries.isNotEmpty) {
      stdio.printError('Found ${unsignedBinaries.length} unsigned binaries:');
298
      unsignedBinaries.forEach(stdio.printError);
299 300 301
    }

    if (wrongEntitlementBinaries.isNotEmpty) {
302
      stdio.printError('Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:');
303
      wrongEntitlementBinaries.forEach(stdio.printError);
304 305 306
    }

    if (unexpectedBinaries.isNotEmpty) {
307
      stdio.printError('Found ${unexpectedBinaries.length} unexpected binaries in the cache:');
308 309 310 311 312
      unexpectedBinaries.forEach(print);
    }

    // Finally, exit on any invalid state
    if (unsignedBinaries.isNotEmpty) {
313
      throw ConductorException('Test failed because unsigned binaries detected.');
314 315 316 317
    }

    if (wrongEntitlementBinaries.isNotEmpty) {
      throw ConductorException(
318 319 320
        'Test failed because files found with the wrong entitlements:\n'
        '${wrongEntitlementBinaries.join('\n')}',
      );
321 322 323
    }

    if (unexpectedBinaries.isNotEmpty) {
324
      throw ConductorException('Test failed because unexpected binaries found in the cache.');
325 326
    }

327 328
    final String? desiredRevision = argResults![kRevision] as String?;
    if (desiredRevision == null) {
329
      stdio.printStatus('Verified that binaries are codesigned and have expected entitlements.');
330 331
    } else {
      stdio.printStatus(
332 333 334
        'Verified that binaries for commit $desiredRevision are codesigned and have '
        'expected entitlements.',
      );
335
    }
336 337
  }

338
  List<String>? _allBinaryPaths;
339

340
  /// Find every binary file in the given [rootDirectory].
341
  Future<List<String>> findBinaryPaths(String rootDirectory) async {
342
    if (_allBinaryPaths != null) {
343
      return _allBinaryPaths!;
344
    }
345 346
    final List<String> allBinaryPaths = <String>[];
    final io.ProcessResult result = await processManager.run(
347 348 349 350 351 352 353 354 355 356 357
      <String>[
        'find',
        rootDirectory,
        '-type',
        'f',
      ],
    );
    final List<String> allFiles = (result.stdout as String)
        .split('\n')
        .where((String s) => s.isNotEmpty)
        .toList();
358 359 360 361 362 363 364

    await Future.forEach(allFiles, (String filePath) async {
      if (await isBinary(filePath)) {
        allBinaryPaths.add(filePath);
      }
    });
    _allBinaryPaths = allBinaryPaths;
365
    return _allBinaryPaths!;
366 367 368
  }

  /// Check mime-type of file at [filePath] to determine if it is binary.
369 370
  Future<bool> isBinary(String filePath) async {
    final io.ProcessResult result = await processManager.run(
371 372 373 374 375 376 377 378 379 380 381
      <String>[
        'file',
        '--mime-type',
        '-b', // is binary
        filePath,
      ],
    );
    return (result.stdout as String).contains('application/x-mach-binary');
  }

  /// Check if the binary has the expected entitlements.
382 383
  Future<bool> hasExpectedEntitlements(String binaryPath) async {
    final io.ProcessResult entitlementResult = await processManager.run(
384 385 386 387 388 389 390 391 392 393 394
      <String>[
        'codesign',
        '--display',
        '--entitlements',
        ':-',
        binaryPath,
      ],
    );

    if (entitlementResult.exitCode != 0) {
      stdio.printError(
395 396 397
        'The `codesign --entitlements` command failed with exit code ${entitlementResult.exitCode}:\n'
        '${entitlementResult.stderr}\n',
      );
398 399 400 401 402 403
      return false;
    }

    bool passes = true;
    final String output = entitlementResult.stdout as String;
    for (final String entitlement in expectedEntitlements) {
404
      final bool entitlementExpected =
405
          (await binariesWithEntitlements).contains(binaryPath);
406 407
      if (output.contains(entitlement) != entitlementExpected) {
        stdio.printError(
408 409 410
          'File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} '
          'entitlement $entitlement.',
        );
411 412 413 414 415 416
        passes = false;
      }
    }
    return passes;
  }
}