mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
370 lines
14 KiB
Dart
370 lines
14 KiB
Dart
// 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';
|
|
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,
|
|
}) : assert(flutterRoot != null),
|
|
fileSystem = checkouts.fileSystem,
|
|
platform = checkouts.platform,
|
|
stdio = checkouts.stdio,
|
|
processManager = checkouts.processManager {
|
|
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 => _framework ??= FrameworkRepository.localRepoAsUpstream(
|
|
checkouts,
|
|
upstreamPath: flutterRoot.path,
|
|
);
|
|
|
|
@visibleForTesting
|
|
set framework(FrameworkRepository framework) => _framework = framework;
|
|
|
|
@override
|
|
String get name => 'codesign';
|
|
|
|
@override
|
|
String get description =>
|
|
'For codesigning and verifying the signatures of engine binaries.';
|
|
|
|
@override
|
|
void run() {
|
|
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.printError('Warning! When providing an arbitrary revision, the contents of the cache may not');
|
|
stdio.printError('match the expected binaries in the conductor tool. It is preferred to check out');
|
|
stdio.printError('the desired revision and run that version of the conductor.\n');
|
|
revision = argResults[kRevision] as String;
|
|
} else {
|
|
revision = (processManager.runSync(
|
|
<String>['git', 'rev-parse', 'HEAD'],
|
|
workingDirectory: framework.checkoutDirectory.path,
|
|
).stdout as String).trim();
|
|
assert(revision.isNotEmpty);
|
|
}
|
|
|
|
framework.checkout(revision);
|
|
|
|
// Ensure artifacts present
|
|
framework.runFlutter(<String>['precache', '--ios', '--macos']);
|
|
|
|
verifyExist();
|
|
if (argResults[kSignatures] as bool) {
|
|
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.
|
|
List<String> get binariesWithEntitlements {
|
|
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-release/gen_snapshot',
|
|
'artifacts/engine/darwin-x64/flutter_tester',
|
|
'artifacts/engine/darwin-x64/gen_snapshot',
|
|
'artifacts/engine/ios-profile/gen_snapshot_arm64',
|
|
'artifacts/engine/ios-profile/gen_snapshot_armv7',
|
|
'artifacts/engine/ios-release/gen_snapshot_arm64',
|
|
'artifacts/engine/ios-release/gen_snapshot_armv7',
|
|
'artifacts/engine/ios/gen_snapshot_arm64',
|
|
'artifacts/engine/ios/gen_snapshot_armv7',
|
|
'artifacts/ios-deploy/ios-deploy',
|
|
'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(framework.cacheDirectory, 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<String> get binariesWithoutEntitlements {
|
|
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/ios-profile/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/Flutter',
|
|
'artifacts/engine/ios-profile/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework/Flutter',
|
|
'artifacts/engine/ios-release/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/Flutter',
|
|
'artifacts/engine/ios-release/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework/Flutter',
|
|
'artifacts/engine/ios/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/Flutter',
|
|
'artifacts/engine/ios/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework/Flutter',
|
|
].map((String relativePath) => fileSystem.path.join(framework.cacheDirectory, 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
|
|
void verifyExist() {
|
|
final Set<String> foundFiles = <String>{};
|
|
for (final String binaryPath in findBinaryPaths(framework.cacheDirectory)) {
|
|
if (binariesWithEntitlements.contains(binaryPath)) {
|
|
foundFiles.add(binaryPath);
|
|
} else if (binariesWithoutEntitlements.contains(binaryPath)) {
|
|
foundFiles.add(binaryPath);
|
|
} else {
|
|
throw ConductorException('Found unexpected binary in cache: $binaryPath');
|
|
}
|
|
}
|
|
|
|
final List<String> allExpectedFiles = binariesWithEntitlements + 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');
|
|
stdio.printError('If this commit is removing binaries from the cache, this test should be fixed by');
|
|
stdio.printError('removing the relevant entry from either the `binariesWithEntitlements` or');
|
|
stdio.printError('`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
|
|
void verifySignatures() {
|
|
final List<String> unsignedBinaries = <String>[];
|
|
final List<String> wrongEntitlementBinaries = <String>[];
|
|
final List<String> unexpectedBinaries = <String>[];
|
|
|
|
for (final String binaryPath in findBinaryPaths(framework.cacheDirectory)) {
|
|
bool verifySignature = false;
|
|
bool verifyEntitlements = false;
|
|
if (binariesWithEntitlements.contains(binaryPath)) {
|
|
verifySignature = true;
|
|
verifyEntitlements = true;
|
|
}
|
|
if (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 = processManager.runSync(
|
|
<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 (!hasExpectedEntitlements(binaryPath)) {
|
|
wrongEntitlementBinaries.add(binaryPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
// First print all deviations from expectations
|
|
if (unsignedBinaries.isNotEmpty) {
|
|
stdio.printError('Found ${unsignedBinaries.length} unsigned binaries:');
|
|
unsignedBinaries.forEach(print);
|
|
}
|
|
|
|
if (wrongEntitlementBinaries.isNotEmpty) {
|
|
stdio.printError(
|
|
'Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:');
|
|
wrongEntitlementBinaries.forEach(print);
|
|
}
|
|
|
|
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.');
|
|
}
|
|
|
|
stdio.printStatus(
|
|
'Verified that binaries for commit ${argResults[kRevision] as String} are codesigned and have '
|
|
'expected entitlements.');
|
|
}
|
|
|
|
List<String> _allBinaryPaths;
|
|
/// Find every binary file in the given [rootDirectory].
|
|
List<String> findBinaryPaths(String rootDirectory) {
|
|
if (_allBinaryPaths != null) {
|
|
return _allBinaryPaths;
|
|
}
|
|
final io.ProcessResult result = processManager.runSync(
|
|
<String>[
|
|
'find',
|
|
rootDirectory,
|
|
'-type',
|
|
'f',
|
|
],
|
|
);
|
|
final List<String> allFiles = (result.stdout as String)
|
|
.split('\n')
|
|
.where((String s) => s.isNotEmpty)
|
|
.toList();
|
|
_allBinaryPaths = allFiles.where(isBinary).toList();
|
|
return _allBinaryPaths;
|
|
}
|
|
|
|
/// Check mime-type of file at [filePath] to determine if it is binary.
|
|
bool isBinary(String filePath) {
|
|
final io.ProcessResult result = processManager.runSync(
|
|
<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.
|
|
bool hasExpectedEntitlements(String binaryPath) {
|
|
final io.ProcessResult entitlementResult = processManager.runSync(
|
|
<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 = 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;
|
|
}
|
|
}
|