diff --git a/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart b/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart new file mode 100644 index 0000000000..91c96f1b33 --- /dev/null +++ b/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart @@ -0,0 +1,372 @@ +// 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:convert'; +import 'dart:io' as io; + +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:process/process.dart'; + +import '../run_command.dart'; +import '../utils.dart'; + +Future verifyCodesignedTestRunner(String flutterRoot) async { + printProgress('${green}Running binaries codesign verification$reset'); + await runCommand( + 'flutter', + [ + 'precache', + '--android', + '--ios', + '--macos' + ], + workingDirectory: flutterRoot, + ); + + await verifyExist(flutterRoot); + await verifySignatures(flutterRoot); +} + +const List expectedEntitlements = [ + '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', +]; + +/// 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. +List binariesWithEntitlements(String flutterRoot) { + return [ + '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', + 'artifacts/engine/darwin-x64-profile/gen_snapshot_arm64', + 'artifacts/engine/darwin-x64-profile/gen_snapshot_x64', + 'artifacts/engine/darwin-x64-release/gen_snapshot', + 'artifacts/engine/darwin-x64-release/gen_snapshot_arm64', + 'artifacts/engine/darwin-x64-release/gen_snapshot_x64', + 'artifacts/engine/darwin-x64/flutter_tester', + 'artifacts/engine/darwin-x64/gen_snapshot', + 'artifacts/engine/darwin-x64/gen_snapshot_arm64', + 'artifacts/engine/darwin-x64/gen_snapshot_x64', + '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', + 'dart-sdk/bin/utils/wasm-opt', + ] + .map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); +} + +/// Binaries that are only expected to be codesigned. +/// +/// This list should be kept in sync with the actual contents of Flutter's +/// cache. +List binariesWithoutEntitlements(String flutterRoot) { + return [ + 'artifacts/engine/darwin-x64-profile/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Versions/A/FlutterMacOS', + 'artifacts/engine/darwin-x64-release/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Versions/A/FlutterMacOS', + 'artifacts/engine/darwin-x64/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Versions/A/FlutterMacOS', + 'artifacts/engine/darwin-x64/font-subset', + 'artifacts/engine/darwin-x64/impellerc', + 'artifacts/engine/darwin-x64/libpath_ops.dylib', + 'artifacts/engine/darwin-x64/libtessellator.dylib', + 'artifacts/engine/ios-profile/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios-profile/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/ios-deploy/ios-deploy', + ] + .map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); +} + +/// xcframeworks that are expected to be codesigned. +/// +/// This list should be kept in sync with the actual contents of Flutter's +/// cache. +List signedXcframeworks(String flutterRoot) { + return [ + 'artifacts/engine/ios-profile/Flutter.xcframework', + 'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework', + 'artifacts/engine/ios-release/Flutter.xcframework', + 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework', + 'artifacts/engine/ios/Flutter.xcframework', + 'artifacts/engine/ios/extension_safe/Flutter.xcframework', + 'artifacts/engine/darwin-x64-profile/FlutterMacOS.xcframework', + 'artifacts/engine/darwin-x64-release/FlutterMacOS.xcframework', + 'artifacts/engine/darwin-x64/FlutterMacOS.xcframework', + ] + .map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); +} + +/// 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. +Future verifyExist( + String flutterRoot, + {@visibleForTesting ProcessManager processManager = const LocalProcessManager() +}) async { + final Set foundFiles = {}; + final String cacheDirectory = path.join(flutterRoot, 'bin', 'cache'); + + for (final String binaryPath + in await findBinaryPaths(cacheDirectory, processManager: processManager)) { + if (binariesWithEntitlements(flutterRoot).contains(binaryPath)) { + foundFiles.add(binaryPath); + } else if (binariesWithoutEntitlements(flutterRoot).contains(binaryPath)) { + foundFiles.add(binaryPath); + } else { + throw Exception( + 'Found unexpected binary in cache: $binaryPath'); + } + } + + final List allExpectedFiles = binariesWithEntitlements(flutterRoot) + binariesWithoutEntitlements(flutterRoot); + if (foundFiles.length < allExpectedFiles.length) { + final List unfoundFiles = allExpectedFiles + .where( + (String file) => !foundFiles.contains(file), + ) + .toList(); + print( + '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.', + ); + throw Exception('Did not find all expected binaries!'); + } + + print('All expected binaries present.'); +} + +/// Verify code signatures and entitlements of all binaries in the cache. +Future verifySignatures( + String flutterRoot, + {@visibleForTesting ProcessManager processManager = const LocalProcessManager()} +) async { + final List unsignedFiles = []; + final List wrongEntitlementBinaries = []; + final List unexpectedFiles = []; + final String cacheDirectory = path.join(flutterRoot, 'bin', 'cache'); + + final List binariesAndXcframeworks = + (await findBinaryPaths(cacheDirectory, processManager: processManager)) + (await findXcframeworksPaths(cacheDirectory, processManager: processManager)); + + for (final String pathToCheck in binariesAndXcframeworks) { + bool verifySignature = false; + bool verifyEntitlements = false; + if (binariesWithEntitlements(flutterRoot).contains(pathToCheck)) { + verifySignature = true; + verifyEntitlements = true; + } + if (binariesWithoutEntitlements(flutterRoot).contains(pathToCheck)) { + verifySignature = true; + } + if (signedXcframeworks(flutterRoot).contains(pathToCheck)) { + verifySignature = true; + } + if (!verifySignature && !verifyEntitlements) { + unexpectedFiles.add(pathToCheck); + print('Unexpected binary or xcframework $pathToCheck found in cache!'); + continue; + } + print('Verifying the code signature of $pathToCheck'); + final io.ProcessResult codeSignResult = await processManager.run( + [ + 'codesign', + '-vvv', + pathToCheck, + ], + ); + if (codeSignResult.exitCode != 0) { + unsignedFiles.add(pathToCheck); + print( + 'File "$pathToCheck" does not appear to be codesigned.\n' + 'The `codesign` command failed with exit code ${codeSignResult.exitCode}:\n' + '${codeSignResult.stderr}\n', + ); + continue; + } + if (verifyEntitlements) { + print('Verifying entitlements of $pathToCheck'); + if (!(await hasExpectedEntitlements(pathToCheck, flutterRoot, processManager: processManager))) { + wrongEntitlementBinaries.add(pathToCheck); + } + } + } + + // First print all deviations from expectations + if (unsignedFiles.isNotEmpty) { + print('Found ${unsignedFiles.length} unsigned files:'); + unsignedFiles.forEach(print); + } + + if (wrongEntitlementBinaries.isNotEmpty) { + print('Found ${wrongEntitlementBinaries.length} files with unexpected entitlements:'); + wrongEntitlementBinaries.forEach(print); + } + + if (unexpectedFiles.isNotEmpty) { + print('Found ${unexpectedFiles.length} unexpected files in the cache:'); + unexpectedFiles.forEach(print); + } + + // Finally, exit on any invalid state + if (unsignedFiles.isNotEmpty) { + throw Exception('Test failed because unsigned files detected.'); + } + + if (wrongEntitlementBinaries.isNotEmpty) { + throw Exception( + 'Test failed because files found with the wrong entitlements:\n' + '${wrongEntitlementBinaries.join('\n')}', + ); + } + + if (unexpectedFiles.isNotEmpty) { + throw Exception('Test failed because unexpected files found in the cache.'); + } + print('Verified that files are codesigned and have expected entitlements.'); +} + +/// Find every binary file in the given [rootDirectory]. +Future> findBinaryPaths( + String rootDirectory, + {@visibleForTesting ProcessManager processManager = const LocalProcessManager() +}) async { + final List allBinaryPaths = []; + final io.ProcessResult result = await processManager.run( + [ + 'find', + rootDirectory, + '-type', + 'f', + ], + ); + final List allFiles = (result.stdout as String) + .split('\n') + .where((String s) => s.isNotEmpty) + .toList(); + + await Future.forEach(allFiles, (String filePath) async { + if (await isBinary(filePath, processManager: processManager)) { + allBinaryPaths.add(filePath); + print('Found: $filePath\n'); + } + }); + return allBinaryPaths; +} + +/// Find every xcframework in the given [rootDirectory]. +Future> findXcframeworksPaths( + String rootDirectory, + {@visibleForTesting ProcessManager processManager = const LocalProcessManager() + }) async { + final io.ProcessResult result = await processManager.run( + [ + 'find', + rootDirectory, + '-type', + 'd', + '-name', + '*xcframework', + ], + ); + final List allXcframeworkPaths = LineSplitter.split(result.stdout as String) + .where((String s) => s.isNotEmpty) + .toList(); + for (final String path in allXcframeworkPaths) { + print('Found: $path\n'); + } + return allXcframeworkPaths; +} + +/// Check mime-type of file at [filePath] to determine if it is binary. +Future isBinary( + String filePath, + {@visibleForTesting ProcessManager processManager = const LocalProcessManager()} +) async { + final io.ProcessResult result = await processManager.run( + [ + '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. +Future hasExpectedEntitlements( + String binaryPath, + String flutterRoot, + {@visibleForTesting ProcessManager processManager = const LocalProcessManager()} +) async { + final io.ProcessResult entitlementResult = await processManager.run( + [ + 'codesign', + '--display', + '--entitlements', + ':-', + binaryPath, + ], + ); + + if (entitlementResult.exitCode != 0) { + print( + 'The `codesign --entitlements` command failed with exit code ${entitlementResult.exitCode}:\n' + '${entitlementResult.stderr}\n', + ); + return false; + } + + bool passes = true; + final String output = entitlementResult.stdout as String; + for (final String entitlement in expectedEntitlements) { + final bool entitlementExpected = + binariesWithEntitlements(flutterRoot).contains(binaryPath); + if (output.contains(entitlement) != entitlementExpected) { + print( + 'File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} ' + 'entitlement $entitlement.', + ); + passes = false; + } + } + return passes; +} diff --git a/dev/bots/test.dart b/dev/bots/test.dart index ef89978518..cb1dab7cd9 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -50,7 +50,6 @@ import 'dart:convert'; import 'dart:core' as system show print; import 'dart:core' hide print; -import 'dart:io' as io; import 'dart:io' as system show exit; import 'dart:io' hide exit; import 'dart:math' as math; @@ -59,9 +58,7 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:file/file.dart' as fs; import 'package:file/local.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; -import 'package:process/process.dart'; import 'run_command.dart'; import 'suite_runners/run_add_to_app_life_cycle_tests.dart'; @@ -72,6 +69,7 @@ import 'suite_runners/run_flutter_packages_tests.dart'; import 'suite_runners/run_fuchsia_precache.dart'; import 'suite_runners/run_realm_checker_tests.dart'; import 'suite_runners/run_skp_generator_tests.dart'; +import 'suite_runners/run_verify_binaries_codesigned_tests.dart'; import 'suite_runners/run_web_long_running_tests.dart'; import 'tool_subsharding.dart'; import 'utils.dart'; @@ -270,7 +268,7 @@ Future main(List args) async { 'analyze': () => analyzeRunner(flutterRoot), 'fuchsia_precache': () => fuchsiaPrecacheRunner(flutterRoot), 'docs': () => docsRunner(flutterRoot), - 'verify_binaries_codesigned': _runVerifyCodesigned, + 'verify_binaries_codesigned': () => verifyCodesignedTestRunner(flutterRoot), kTestHarnessShardName: _runTestHarnessTests, // Used for testing this script; also run as part of SHARD=framework_tests, SUBSHARD=misc. }); } catch (error, stackTrace) { @@ -1211,365 +1209,6 @@ Future _runWebUnitTests(String webRenderer, bool useWasm) async { await selectSubshard(subshards); } -// Verifies binaries are codesigned. -Future _runVerifyCodesigned() async { - printProgress('${green}Running binaries codesign verification$reset'); - await runCommand( - 'flutter', - [ - 'precache', - '--android', - '--ios', - '--macos' - ], - workingDirectory: flutterRoot, - ); - - await verifyExist(flutterRoot); - await verifySignatures(flutterRoot); -} - -const List expectedEntitlements = [ - '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', -]; - -/// 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. -List binariesWithEntitlements(String flutterRoot) { - return [ - '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', - 'artifacts/engine/darwin-x64-profile/gen_snapshot_arm64', - 'artifacts/engine/darwin-x64-profile/gen_snapshot_x64', - 'artifacts/engine/darwin-x64-release/gen_snapshot', - 'artifacts/engine/darwin-x64-release/gen_snapshot_arm64', - 'artifacts/engine/darwin-x64-release/gen_snapshot_x64', - 'artifacts/engine/darwin-x64/flutter_tester', - 'artifacts/engine/darwin-x64/gen_snapshot', - 'artifacts/engine/darwin-x64/gen_snapshot_arm64', - 'artifacts/engine/darwin-x64/gen_snapshot_x64', - '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', - 'dart-sdk/bin/utils/wasm-opt', - ] - .map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); -} - -/// Binaries that are only expected to be codesigned. -/// -/// This list should be kept in sync with the actual contents of Flutter's -/// cache. -List binariesWithoutEntitlements(String flutterRoot) { - return [ - 'artifacts/engine/darwin-x64-profile/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Versions/A/FlutterMacOS', - 'artifacts/engine/darwin-x64-release/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Versions/A/FlutterMacOS', - 'artifacts/engine/darwin-x64/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Versions/A/FlutterMacOS', - 'artifacts/engine/darwin-x64/font-subset', - 'artifacts/engine/darwin-x64/impellerc', - 'artifacts/engine/darwin-x64/libpath_ops.dylib', - 'artifacts/engine/darwin-x64/libtessellator.dylib', - 'artifacts/engine/ios-profile/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', - 'artifacts/engine/ios-profile/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', - 'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', - 'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', - 'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', - 'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', - 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', - 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', - 'artifacts/engine/ios/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', - 'artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', - 'artifacts/engine/ios/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', - 'artifacts/engine/ios/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', - 'artifacts/ios-deploy/ios-deploy', - ] - .map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); -} - -/// xcframeworks that are expected to be codesigned. -/// -/// This list should be kept in sync with the actual contents of Flutter's -/// cache. -List signedXcframeworks(String flutterRoot) { - return [ - 'artifacts/engine/ios-profile/Flutter.xcframework', - 'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework', - 'artifacts/engine/ios-release/Flutter.xcframework', - 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework', - 'artifacts/engine/ios/Flutter.xcframework', - 'artifacts/engine/ios/extension_safe/Flutter.xcframework', - 'artifacts/engine/darwin-x64-profile/FlutterMacOS.xcframework', - 'artifacts/engine/darwin-x64-release/FlutterMacOS.xcframework', - 'artifacts/engine/darwin-x64/FlutterMacOS.xcframework', - ] - .map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); -} - -/// 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. -Future verifyExist( - String flutterRoot, - {@visibleForTesting ProcessManager processManager = const LocalProcessManager() -}) async { - final Set foundFiles = {}; - final String cacheDirectory = path.join(flutterRoot, 'bin', 'cache'); - - for (final String binaryPath - in await findBinaryPaths(cacheDirectory, processManager: processManager)) { - if (binariesWithEntitlements(flutterRoot).contains(binaryPath)) { - foundFiles.add(binaryPath); - } else if (binariesWithoutEntitlements(flutterRoot).contains(binaryPath)) { - foundFiles.add(binaryPath); - } else { - throw Exception( - 'Found unexpected binary in cache: $binaryPath'); - } - } - - final List allExpectedFiles = binariesWithEntitlements(flutterRoot) + binariesWithoutEntitlements(flutterRoot); - if (foundFiles.length < allExpectedFiles.length) { - final List unfoundFiles = allExpectedFiles - .where( - (String file) => !foundFiles.contains(file), - ) - .toList(); - print( - '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.', - ); - throw Exception('Did not find all expected binaries!'); - } - - print('All expected binaries present.'); -} - -/// Verify code signatures and entitlements of all binaries in the cache. -Future verifySignatures( - String flutterRoot, - {@visibleForTesting ProcessManager processManager = const LocalProcessManager()} -) async { - final List unsignedFiles = []; - final List wrongEntitlementBinaries = []; - final List unexpectedFiles = []; - final String cacheDirectory = path.join(flutterRoot, 'bin', 'cache'); - - final List binariesAndXcframeworks = - (await findBinaryPaths(cacheDirectory, processManager: processManager)) + (await findXcframeworksPaths(cacheDirectory, processManager: processManager)); - - for (final String pathToCheck in binariesAndXcframeworks) { - bool verifySignature = false; - bool verifyEntitlements = false; - if (binariesWithEntitlements(flutterRoot).contains(pathToCheck)) { - verifySignature = true; - verifyEntitlements = true; - } - if (binariesWithoutEntitlements(flutterRoot).contains(pathToCheck)) { - verifySignature = true; - } - if (signedXcframeworks(flutterRoot).contains(pathToCheck)) { - verifySignature = true; - } - if (!verifySignature && !verifyEntitlements) { - unexpectedFiles.add(pathToCheck); - print('Unexpected binary or xcframework $pathToCheck found in cache!'); - continue; - } - print('Verifying the code signature of $pathToCheck'); - final io.ProcessResult codeSignResult = await processManager.run( - [ - 'codesign', - '-vvv', - pathToCheck, - ], - ); - if (codeSignResult.exitCode != 0) { - unsignedFiles.add(pathToCheck); - print( - 'File "$pathToCheck" does not appear to be codesigned.\n' - 'The `codesign` command failed with exit code ${codeSignResult.exitCode}:\n' - '${codeSignResult.stderr}\n', - ); - continue; - } - if (verifyEntitlements) { - print('Verifying entitlements of $pathToCheck'); - if (!(await hasExpectedEntitlements(pathToCheck, flutterRoot, processManager: processManager))) { - wrongEntitlementBinaries.add(pathToCheck); - } - } - } - - // First print all deviations from expectations - if (unsignedFiles.isNotEmpty) { - print('Found ${unsignedFiles.length} unsigned files:'); - unsignedFiles.forEach(print); - } - - if (wrongEntitlementBinaries.isNotEmpty) { - print('Found ${wrongEntitlementBinaries.length} files with unexpected entitlements:'); - wrongEntitlementBinaries.forEach(print); - } - - if (unexpectedFiles.isNotEmpty) { - print('Found ${unexpectedFiles.length} unexpected files in the cache:'); - unexpectedFiles.forEach(print); - } - - // Finally, exit on any invalid state - if (unsignedFiles.isNotEmpty) { - throw Exception('Test failed because unsigned files detected.'); - } - - if (wrongEntitlementBinaries.isNotEmpty) { - throw Exception( - 'Test failed because files found with the wrong entitlements:\n' - '${wrongEntitlementBinaries.join('\n')}', - ); - } - - if (unexpectedFiles.isNotEmpty) { - throw Exception('Test failed because unexpected files found in the cache.'); - } - print('Verified that files are codesigned and have expected entitlements.'); -} - -/// Find every binary file in the given [rootDirectory]. -Future> findBinaryPaths( - String rootDirectory, - {@visibleForTesting ProcessManager processManager = const LocalProcessManager() -}) async { - final List allBinaryPaths = []; - final io.ProcessResult result = await processManager.run( - [ - 'find', - rootDirectory, - '-type', - 'f', - ], - ); - final List allFiles = (result.stdout as String) - .split('\n') - .where((String s) => s.isNotEmpty) - .toList(); - - await Future.forEach(allFiles, (String filePath) async { - if (await isBinary(filePath, processManager: processManager)) { - allBinaryPaths.add(filePath); - print('Found: $filePath\n'); - } - }); - return allBinaryPaths; -} - -/// Find every xcframework in the given [rootDirectory]. -Future> findXcframeworksPaths( - String rootDirectory, - {@visibleForTesting ProcessManager processManager = const LocalProcessManager() - }) async { - final io.ProcessResult result = await processManager.run( - [ - 'find', - rootDirectory, - '-type', - 'd', - '-name', - '*xcframework', - ], - ); - final List allXcframeworkPaths = LineSplitter.split(result.stdout as String) - .where((String s) => s.isNotEmpty) - .toList(); - for (final String path in allXcframeworkPaths) { - print('Found: $path\n'); - } - return allXcframeworkPaths; -} - -/// Check mime-type of file at [filePath] to determine if it is binary. -Future isBinary( - String filePath, - {@visibleForTesting ProcessManager processManager = const LocalProcessManager()} -) async { - final io.ProcessResult result = await processManager.run( - [ - '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. -Future hasExpectedEntitlements( - String binaryPath, - String flutterRoot, - {@visibleForTesting ProcessManager processManager = const LocalProcessManager()} -) async { - final io.ProcessResult entitlementResult = await processManager.run( - [ - 'codesign', - '--display', - '--entitlements', - ':-', - binaryPath, - ], - ); - - if (entitlementResult.exitCode != 0) { - print( - 'The `codesign --entitlements` command failed with exit code ${entitlementResult.exitCode}:\n' - '${entitlementResult.stderr}\n', - ); - return false; - } - - bool passes = true; - final String output = entitlementResult.stdout as String; - for (final String entitlement in expectedEntitlements) { - final bool entitlementExpected = - binariesWithEntitlements(flutterRoot).contains(binaryPath); - if (output.contains(entitlement) != entitlementExpected) { - print( - 'File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} ' - 'entitlement $entitlement.', - ); - passes = false; - } - } - return passes; -} Future runFlutterWebTest( String webRenderer, diff --git a/dev/bots/test/codesign_test.dart b/dev/bots/test/codesign_test.dart index fa606ccc2c..c97ca137c1 100644 --- a/dev/bots/test/codesign_test.dart +++ b/dev/bots/test/codesign_test.dart @@ -6,7 +6,7 @@ library; import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; -import '../test.dart'; +import '../suite_runners/run_verify_binaries_codesigned_tests.dart'; import './common.dart'; void main() async {