// 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() async { printProgress('${green}Running binaries codesign verification$reset'); await runCommand(path.join(flutterRoot, 'bin', 'flutter'), [ 'precache', '--android', '--ios', '--macos', ], workingDirectory: flutterRoot); await verifyExist(flutterRoot); await verifySignatures(flutterRoot); await verifyFatBinaries(flutterRoot); } /// Some binaries should always be codesigned, even on master. Verify that they /// are codesigned and have the correct entitlements. Future verifyPreCodesignedTestRunner() async { printProgress('${green}Running binaries codesign verification$reset'); await runCommand(path.join(flutterRoot, 'bin', 'flutter'), [ 'precache', '--android', '--ios', '--macos', ], workingDirectory: flutterRoot); await verifyExist(flutterRoot); await verifySignatures(flutterRoot, forRelease: false); await verifyFatBinaries(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) { final List binaries = [ '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', 'dart-sdk/bin/dart', 'dart-sdk/bin/dartaotruntime', 'dart-sdk/bin/dartvm', 'dart-sdk/bin/utils/gen_snapshot', 'dart-sdk/bin/utils/wasm-opt', ].map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); presignedBinariesWithEntitlements(flutterRoot).forEach(binaries.add); return binaries; } /// 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) { final List binaries = [ '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', ].map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); presignedBinariesWithoutEntitlements(flutterRoot).forEach(binaries.add); return binaries; } /// Binaries that are not expected to be codesigned. /// /// This list should be kept in sync with the actual contents of Flutter's cache. List unsignedBinaries(String flutterRoot) { return [ 'artifacts/engine/darwin-x64-release/FlutterMacOS.xcframework/macos-arm64_x86_64/dSYMs/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS', 'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', 'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework/ios-arm64/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', ].map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); } /// Binaries with entitlements that should always be signed, even on master. List presignedBinariesWithEntitlements(String flutterRoot) { return [ 'artifacts/libimobiledevice/idevicescreenshot', 'artifacts/libimobiledevice/idevicesyslog', 'artifacts/libusbmuxd/iproxy', ].map((String relativePath) => path.join(flutterRoot, 'bin', 'cache', relativePath)).toList(); } /// Binaries without entitlements that should always be signed, even on master. List presignedBinariesWithoutEntitlements(String flutterRoot) { return [ 'artifacts/ios-deploy/ios-deploy', 'artifacts/libimobiledevice/libimobiledevice-1.0.6.dylib', 'artifacts/libimobiledeviceglue/libimobiledevice-glue-1.0.0.dylib', 'artifacts/libplist/libplist-2.0.4.dylib', 'artifacts/openssl/libcrypto.3.dylib', 'artifacts/openssl/libssl.3.dylib', 'artifacts/libusbmuxd/libusbmuxd-2.0.7.dylib', ].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], [binariesWithoutEntitlements], and /// [unsignedBinaries] lists should be updated accordingly. Future verifyExist( String flutterRoot, { @visibleForTesting ProcessManager processManager = const LocalProcessManager(), }) async { final List binaryPaths = await findBinaryPaths( path.join(flutterRoot, 'bin', 'cache'), processManager: processManager, ); final List expectedSigned = binariesWithEntitlements(flutterRoot) + binariesWithoutEntitlements(flutterRoot); final List expectedUnsigned = unsignedBinaries(flutterRoot); final foundFiles = { for (final String binaryPath in binaryPaths) if (expectedSigned.contains(binaryPath)) binaryPath else if (expectedUnsigned.contains(binaryPath)) binaryPath else throw Exception('Found unexpected binary in cache: $binaryPath'), }; if (foundFiles.length < expectedSigned.length) { final unfoundFiles = [ for (final String file in expectedSigned) if (!foundFiles.contains(file)) file, ]; 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 binaries in the cache. /// /// If [forRelease] is true, verify for all binaries. Otherwise, only verify for pre-signed binaries. Future verifySignatures( String flutterRoot, { @visibleForTesting ProcessManager processManager = const LocalProcessManager(), bool forRelease = true, }) async { final unsignedFiles = []; final wrongEntitlementBinaries = []; final unexpectedFiles = []; final String cacheDirectory = path.join(flutterRoot, 'bin', 'cache'); final List binariesAndXcframeworks = (await findBinaryPaths(cacheDirectory, processManager: processManager)) + (await findXcframeworksPaths(cacheDirectory, processManager: processManager)); final List binariesToVerifyEntitlements; final List binariesToVerifyWithoutEntitlements; final List xcframeworksToVerifyCodesigned; if (forRelease) { binariesToVerifyEntitlements = binariesWithEntitlements(flutterRoot); binariesToVerifyWithoutEntitlements = binariesWithoutEntitlements(flutterRoot); xcframeworksToVerifyCodesigned = signedXcframeworks(flutterRoot); } else { binariesToVerifyEntitlements = presignedBinariesWithEntitlements(flutterRoot); binariesToVerifyWithoutEntitlements = presignedBinariesWithoutEntitlements(flutterRoot); xcframeworksToVerifyCodesigned = []; } for (final pathToCheck in binariesAndXcframeworks) { var verifySignature = false; var verifyEntitlements = false; if (binariesToVerifyEntitlements.contains(pathToCheck)) { verifySignature = true; verifyEntitlements = true; } if (binariesToVerifyWithoutEntitlements.contains(pathToCheck)) { verifySignature = true; } if (xcframeworksToVerifyCodesigned.contains(pathToCheck)) { verifySignature = true; } if (unsignedBinaries(flutterRoot).contains(pathToCheck)) { // Binary is expected to be unsigned. No need to check signature, entitlements. continue; } // If not testing for release, skip path if not going to verify it's codesigned/entitlements. if (!forRelease && !verifySignature && !verifyEntitlements) { continue; } 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.'); } /// Verify that specific binaries are fat binaries containing both x86_64 and arm64 slices. Future verifyFatBinaries( String flutterRoot, { @visibleForTesting ProcessManager processManager = const LocalProcessManager(), }) async { final List fatBinaries = presignedBinariesWithEntitlements(flutterRoot) + presignedBinariesWithoutEntitlements(flutterRoot); final failedBinaries = []; for (final binaryPath in fatBinaries) { print('Verifying fat binary architectures for $binaryPath'); final io.ProcessResult result = await processManager.run(['file', binaryPath]); if (result.exitCode != 0) { print('Failed to run file command on $binaryPath: \n${result.stderr}'); failedBinaries.add(binaryPath); continue; } final output = result.stdout as String; final bool containsX86_64 = output.contains('x86_64'); final bool containsArm64 = output.contains('arm64'); if (!containsX86_64 || !containsArm64) { print('Binary $binaryPath is not a fat binary containing both x86_64 and arm64.'); print('Output: $output'); failedBinaries.add(binaryPath); } } if (failedBinaries.isNotEmpty) { throw Exception('Failed fat binary verification for:\n${failedBinaries.join('\n')}'); } print('All expected fat binaries verified.'); } /// Find every binary file in the given [rootDirectory]. Future> findBinaryPaths( String rootDirectory, { @visibleForTesting ProcessManager processManager = const LocalProcessManager(), }) async { final 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 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; } var passes = true; final 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; }