// 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'; import 'package:meta/meta.dart' show visibleForTesting; 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'; /// Command to codesign and verify the signatures of cached binaries. class CodesignCommand extends Command<void> { CodesignCommand({ required this.checkouts, required this.flutterRoot, FrameworkRepository? framework, }) : fileSystem = checkouts.fileSystem, platform = checkouts.platform, stdio = checkouts.stdio, processManager = checkouts.processManager { if (framework != null) { _framework = framework; } argParser.addFlag( kVerify, help: 'Only verify expected binaries exist and are codesigned with entitlements.', ); argParser.addFlag( kSignatures, defaultsTo: true, 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.', ); argParser.addOption( kUpstream, defaultsTo: FrameworkRepository.defaultUpstream, help: "The git remote URL to use as the Flutter framework's upstream.", ); 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; FrameworkRepository? _framework; FrameworkRepository get framework { return _framework ??= FrameworkRepository( checkouts, upstreamRemote: Remote( name: RemoteName.upstream, url: argResults![kUpstream] as String, ), ); } @override String get name => 'codesign'; @override String get description => 'For codesigning and verifying the signatures of engine binaries.'; @override Future<void> run() async { if (!platform.isMacOS) { throw ConductorException( 'Error! Expected operating system "macos", actual operating system is: ' '"${platform.operatingSystem}"', ); } if (argResults!['verify'] as bool != true) { throw ConductorException( 'Sorry, but codesigning is not implemented yet. Please pass the ' '--$kVerify flag to verify signatures.', ); } String revision; if (argResults!.wasParsed(kRevision)) { 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', ); revision = argResults![kRevision] as String; } else { revision = ((await processManager.run( <String>['git', 'rev-parse', 'HEAD'], workingDirectory: flutterRoot.path, )).stdout as String).trim(); assert(revision.isNotEmpty); } await framework.checkout(revision); // Ensure artifacts present await framework.runFlutter(<String>['precache', '--android', '--ios', '--macos']); await verifyExist(); if (argResults![kSignatures] as bool) { await verifySignatures(); } } /// 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. Future<List<String>> get binariesWithEntitlements async { final String frameworkCacheDirectory = await framework.cacheDirectory; 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', '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', ] .map((String relativePath) => fileSystem.path.join(frameworkCacheDirectory, 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. Future<List<String>> get binariesWithoutEntitlements async { final String frameworkCacheDirectory = await framework.cacheDirectory; 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', '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-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/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', 'artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', 'artifacts/ios-deploy/ios-deploy', ] .map((String relativePath) => fileSystem.path.join(frameworkCacheDirectory, 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. @visibleForTesting Future<void> verifyExist() async { final Set<String> foundFiles = <String>{}; for (final String binaryPath in await findBinaryPaths(await framework.cacheDirectory)) { if ((await binariesWithEntitlements).contains(binaryPath)) { foundFiles.add(binaryPath); } else if ((await binariesWithoutEntitlements).contains(binaryPath)) { foundFiles.add(binaryPath); } else { throw ConductorException( 'Found unexpected binary in cache: $binaryPath'); } } final List<String> allExpectedFiles = (await binariesWithEntitlements) + (await binariesWithoutEntitlements); if (foundFiles.length < allExpectedFiles.length) { final List<String> unfoundFiles = allExpectedFiles .where( (String file) => !foundFiles.contains(file), ) .toList(); stdio.printError( '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 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 Future<void> verifySignatures() async { final List<String> unsignedBinaries = <String>[]; final List<String> wrongEntitlementBinaries = <String>[]; final List<String> unexpectedBinaries = <String>[]; for (final String binaryPath in await findBinaryPaths(await framework.cacheDirectory)) { bool verifySignature = false; bool verifyEntitlements = false; if ((await binariesWithEntitlements).contains(binaryPath)) { verifySignature = true; verifyEntitlements = true; } if ((await binariesWithoutEntitlements).contains(binaryPath)) { 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'); final io.ProcessResult codeSignResult = await processManager.run( <String>[ 'codesign', '-vvv', binaryPath, ], ); if (codeSignResult.exitCode != 0) { unsignedBinaries.add(binaryPath); stdio.printError( 'File "$binaryPath" does not appear to be codesigned.\n' 'The `codesign` command failed with exit code ${codeSignResult.exitCode}:\n' '${codeSignResult.stderr}\n', ); continue; } if (verifyEntitlements) { stdio.printTrace('Verifying entitlements of $binaryPath'); if (!(await hasExpectedEntitlements(binaryPath))) { wrongEntitlementBinaries.add(binaryPath); } } } // First print all deviations from expectations if (unsignedBinaries.isNotEmpty) { stdio.printError('Found ${unsignedBinaries.length} unsigned binaries:'); unsignedBinaries.forEach(stdio.printError); } if (wrongEntitlementBinaries.isNotEmpty) { stdio.printError('Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:'); wrongEntitlementBinaries.forEach(stdio.printError); } if (unexpectedBinaries.isNotEmpty) { stdio.printError('Found ${unexpectedBinaries.length} unexpected binaries in the cache:'); unexpectedBinaries.forEach(print); } // Finally, exit on any invalid state if (unsignedBinaries.isNotEmpty) { throw ConductorException('Test failed because unsigned binaries detected.'); } if (wrongEntitlementBinaries.isNotEmpty) { throw ConductorException( 'Test failed because files found with the wrong entitlements:\n' '${wrongEntitlementBinaries.join('\n')}', ); } if (unexpectedBinaries.isNotEmpty) { throw ConductorException('Test failed because unexpected binaries found in the cache.'); } final String? desiredRevision = argResults![kRevision] as String?; if (desiredRevision == null) { stdio.printStatus('Verified that binaries are codesigned and have expected entitlements.'); } else { stdio.printStatus( 'Verified that binaries for commit $desiredRevision are codesigned and have ' 'expected entitlements.', ); } } List<String>? _allBinaryPaths; /// Find every binary file in the given [rootDirectory]. Future<List<String>> findBinaryPaths(String rootDirectory) async { if (_allBinaryPaths != null) { return _allBinaryPaths!; } final List<String> allBinaryPaths = <String>[]; final io.ProcessResult result = await processManager.run( <String>[ 'find', rootDirectory, '-type', 'f', ], ); final List<String> allFiles = (result.stdout as String) .split('\n') .where((String s) => s.isNotEmpty) .toList(); await Future.forEach(allFiles, (String filePath) async { if (await isBinary(filePath)) { allBinaryPaths.add(filePath); } }); _allBinaryPaths = allBinaryPaths; return _allBinaryPaths!; } /// Check mime-type of file at [filePath] to determine if it is binary. Future<bool> isBinary(String filePath) async { final io.ProcessResult result = await processManager.run( <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. Future<bool> hasExpectedEntitlements(String binaryPath) async { final io.ProcessResult entitlementResult = await processManager.run( <String>[ 'codesign', '--display', '--entitlements', ':-', binaryPath, ], ); if (entitlementResult.exitCode != 0) { stdio.printError( '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 = (await binariesWithEntitlements).contains(binaryPath); if (output.contains(entitlement) != entitlementExpected) { stdio.printError( 'File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} ' 'entitlement $entitlement.', ); passes = false; } } return passes; } }