CP: Migrate verify_codesigned. (#139328) (#140991)

CP: c73bffe74709c5df228447a6ddf3c8eb811d36bf

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
diff --git a/.ci.yaml b/.ci.yaml
index 55be8eb..f53f891 100644
--- a/.ci.yaml
+++ b/.ci.yaml
@@ -3650,26 +3650,24 @@
   - name: Mac_x64 verify_binaries_codesigned
     enabled_branches:
       - flutter-\d+\.\d+-candidate\.\d+
-    recipe: flutter/flutter
+    recipe: flutter/flutter_drone
     presubmit: false
     timeout: 60
     properties:
       tags: >
         ["framework", "hostonly", "shard", "mac"]
-      validation: verify_binaries_codesigned
-      validation_name: Verify x64 binaries codesigned
+      shard: verify_binaries_codesigned
 
   - name: Mac_arm64 verify_binaries_codesigned
     enabled_branches:
       - flutter-\d+\.\d+-candidate\.\d+
-    recipe: flutter/flutter
+    recipe: flutter/flutter_drone
     presubmit: false
     timeout: 60
     properties:
       tags: >
         ["framework", "hostonly", "shard", "mac"]
-      validation: verify_binaries_codesigned
-      validation_name: Verify arm64 binaries codesigned
+      shard: verify_binaries_codesigned
 
   - name: Mac web_tool_tests
     recipe: flutter/flutter_drone
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index 041f1d8..2b8c49d 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -52,13 +52,16 @@
 import 'dart:core' hide print;
 import 'dart:io' as system show exit;
 import 'dart:io' hide exit;
+import 'dart:io' as io;
 import 'dart:math' as math;
 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 'browser.dart';
 import 'run_command.dart';
@@ -267,6 +270,7 @@
       'analyze': _runAnalyze,
       'fuchsia_precache': _runFuchsiaPrecache,
       'docs': _runDocs,
+      'verify_binaries_codesigned': _runVerifyCodesigned,
       kTestHarnessShardName: _runTestHarnessTests, // Used for testing this script; also run as part of SHARD=framework_tests, SUBSHARD=misc.
     });
   } catch (error, stackTrace) {
@@ -1645,6 +1649,320 @@
   );
 }
 
+// Verifies binaries are codesigned.
+Future<void> _runVerifyCodesigned() async {
+  printProgress('${green}Running binaries codesign verification$reset');
+  await runCommand(
+    'flutter',
+    <String>[
+      'precache',
+      '--android',
+      '--ios',
+      '--macos'
+    ],
+    workingDirectory: flutterRoot,
+  );
+
+  await verifyExist(flutterRoot);
+  await verifySignatures(flutterRoot);
+}
+
+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',
+];
+
+/// 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>> binariesWithEntitlements(String flutterRoot) async {
+  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',
+    '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.
+Future<List<String>> binariesWithoutEntitlements(String flutterRoot) async {
+  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-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();
+}
+
+/// 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<void> verifyExist(
+  String flutterRoot,
+  {@visibleForTesting ProcessManager processManager = const LocalProcessManager()
+}) async {
+  final Set<String> foundFiles = <String>{};
+  final String cacheDirectory =  path.join(flutterRoot, 'bin', 'cache');
+
+
+
+  for (final String binaryPath
+      in await findBinaryPaths(cacheDirectory, processManager: processManager)) {
+    if ((await binariesWithEntitlements(flutterRoot)).contains(binaryPath)) {
+      foundFiles.add(binaryPath);
+    } else if ((await binariesWithoutEntitlements(flutterRoot)).contains(binaryPath)) {
+      foundFiles.add(binaryPath);
+    } else {
+      throw Exception(
+          'Found unexpected binary in cache: $binaryPath');
+    }
+  }
+
+  final List<String> allExpectedFiles = await binariesWithEntitlements(flutterRoot) + await binariesWithoutEntitlements(flutterRoot);
+  if (foundFiles.length < allExpectedFiles.length) {
+    final List<String> 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<void> verifySignatures(
+  String flutterRoot,
+  {@visibleForTesting ProcessManager processManager = const LocalProcessManager()}
+) async {
+  final List<String> unsignedBinaries = <String>[];
+  final List<String> wrongEntitlementBinaries = <String>[];
+  final List<String> unexpectedBinaries = <String>[];
+  final String cacheDirectory =  path.join(flutterRoot, 'bin', 'cache');
+
+  for (final String binaryPath
+      in await findBinaryPaths(cacheDirectory, processManager: processManager)) {
+    bool verifySignature = false;
+    bool verifyEntitlements = false;
+    if ((await binariesWithEntitlements(flutterRoot)).contains(binaryPath)) {
+      verifySignature = true;
+      verifyEntitlements = true;
+    }
+    if ((await binariesWithoutEntitlements(flutterRoot)).contains(binaryPath)) {
+      verifySignature = true;
+    }
+    if (!verifySignature && !verifyEntitlements) {
+      unexpectedBinaries.add(binaryPath);
+      print('Unexpected binary $binaryPath found in cache!');
+      continue;
+    }
+    print('Verifying the code signature of $binaryPath');
+    final io.ProcessResult codeSignResult = await processManager.run(
+      <String>[
+        'codesign',
+        '-vvv',
+        binaryPath,
+      ],
+    );
+    if (codeSignResult.exitCode != 0) {
+      unsignedBinaries.add(binaryPath);
+      print(
+        '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) {
+      print('Verifying entitlements of $binaryPath');
+      if (!(await hasExpectedEntitlements(binaryPath, flutterRoot, processManager: processManager))) {
+        wrongEntitlementBinaries.add(binaryPath);
+      }
+    }
+  }
+
+  // First print all deviations from expectations
+  if (unsignedBinaries.isNotEmpty) {
+    print('Found ${unsignedBinaries.length} unsigned binaries:');
+    unsignedBinaries.forEach(print);
+  }
+
+  if (wrongEntitlementBinaries.isNotEmpty) {
+    print('Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:');
+    wrongEntitlementBinaries.forEach(print);
+  }
+
+  if (unexpectedBinaries.isNotEmpty) {
+    print('Found ${unexpectedBinaries.length} unexpected binaries in the cache:');
+    unexpectedBinaries.forEach(print);
+  }
+
+  // Finally, exit on any invalid state
+  if (unsignedBinaries.isNotEmpty) {
+    throw Exception('Test failed because unsigned binaries detected.');
+  }
+
+  if (wrongEntitlementBinaries.isNotEmpty) {
+    throw Exception(
+      'Test failed because files found with the wrong entitlements:\n'
+      '${wrongEntitlementBinaries.join('\n')}',
+    );
+  }
+
+  if (unexpectedBinaries.isNotEmpty) {
+    throw Exception('Test failed because unexpected binaries found in the cache.');
+  }
+  print('Verified that binaries are codesigned and have expected entitlements.');
+}
+
+/// Find every binary file in the given [rootDirectory].
+Future<List<String>> findBinaryPaths(
+  String rootDirectory,
+  {@visibleForTesting ProcessManager processManager = const LocalProcessManager()
+}) async {
+  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, processManager: processManager)) {
+      allBinaryPaths.add(filePath);
+      print('Found: $filePath\n');
+    }
+  });
+  return allBinaryPaths;
+}
+
+/// Check mime-type of file at [filePath] to determine if it is binary.
+Future<bool> isBinary(
+  String filePath,
+  {@visibleForTesting ProcessManager processManager = const LocalProcessManager()}
+) 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,
+  String flutterRoot,
+  {@visibleForTesting ProcessManager processManager = const LocalProcessManager()}
+) async {
+  final io.ProcessResult entitlementResult = await processManager.run(
+    <String>[
+      '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 =
+        (await 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;
+}
+
 /// Runs the skp_generator from the flutter/tests repo.
 ///
 /// See also the customer_tests shard.
diff --git a/dev/bots/test/codesign_test.dart b/dev/bots/test/codesign_test.dart
new file mode 100644
index 0000000..7d2e5ff
--- /dev/null
+++ b/dev/bots/test/codesign_test.dart
@@ -0,0 +1,311 @@
+// 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.
+
+@TestOn('mac-os')
+library;
+
+import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
+import '../test.dart';
+import './common.dart';
+
+void main() async {
+  const String flutterRoot = '/a/b/c';
+  final List<String> allExpectedFiles = await binariesWithEntitlements(flutterRoot) + await binariesWithoutEntitlements(flutterRoot);
+  final String allFilesStdout = allExpectedFiles.join('\n');
+  final List<String> withEntitlements = await binariesWithEntitlements(flutterRoot);
+
+  group('verifyExist', () {
+    test('Not all files found', () async {
+      final ProcessManager processManager = FakeProcessManager.list(
+        <FakeCommand>[
+          const FakeCommand(
+            command: <String>[
+              'find',
+              '/a/b/c/bin/cache',
+              '-type',
+              'f',
+            ],
+            stdout: '/a/b/c/bin/cache/artifacts/engine/android-arm-profile/darwin-x64/gen_snapshot',
+          ),
+          const FakeCommand(
+            command: <String>[
+              'file',
+              '--mime-type',
+              '-b',
+              '/a/b/c/bin/cache/artifacts/engine/android-arm-profile/darwin-x64/gen_snapshot',
+            ],
+            stdout: 'application/x-mach-binary',
+          ),
+        ],
+      );
+      expect(
+        () async => verifyExist(flutterRoot, processManager: processManager),
+        throwsExceptionWith('Did not find all expected binaries!'),
+      );
+    });
+
+    test('All files found', () async {
+      final List<FakeCommand> commandList = <FakeCommand>[];
+      final FakeCommand findCmd = FakeCommand(
+        command: const <String>[
+          'find',
+          '$flutterRoot/bin/cache',
+          '-type',
+          'f',],
+        stdout: allFilesStdout,
+          );
+      commandList.add(findCmd);
+      for (final String expectedFile in allExpectedFiles) {
+        commandList.add(
+          FakeCommand(
+            command: <String>[
+              'file',
+              '--mime-type',
+              '-b',
+              expectedFile,
+            ],
+            stdout: 'application/x-mach-binary',
+          )
+        );
+      }
+      final ProcessManager processManager = FakeProcessManager.list(commandList);
+      await expectLater(verifyExist('/a/b/c', processManager: processManager), completes);
+    });
+  });
+
+  group('findBinaryPaths', () {
+    test('All files found', () async {
+      final List<FakeCommand> commandList = <FakeCommand>[];
+      final FakeCommand findCmd = FakeCommand(
+        command: const <String>[
+          'find',
+          '$flutterRoot/bin/cache',
+          '-type',
+          'f',],
+        stdout: allFilesStdout,
+      );
+      commandList.add(findCmd);
+      for (final String expectedFile in allExpectedFiles) {
+        commandList.add(
+          FakeCommand(
+            command: <String>[
+              'file',
+              '--mime-type',
+              '-b',
+              expectedFile,
+            ],
+            stdout: 'application/x-mach-binary',
+          )
+        );
+      }
+      final ProcessManager processManager = FakeProcessManager.list(commandList);
+      final List<String> foundFiles = await findBinaryPaths('$flutterRoot/bin/cache', processManager: processManager);
+      expect(foundFiles, allExpectedFiles);
+    });
+
+    test('Empty file list', () async {
+      final List<FakeCommand> commandList = <FakeCommand>[];
+      const FakeCommand findCmd = FakeCommand(
+        command: <String>[
+          'find',
+          '$flutterRoot/bin/cache',
+          '-type',
+          'f',],
+      );
+      commandList.add(findCmd);
+      final ProcessManager processManager = FakeProcessManager.list(commandList);
+      final List<String> foundFiles = await findBinaryPaths('$flutterRoot/bin/cache', processManager: processManager);
+      expect(foundFiles, <String>[]);
+  });
+
+  group('isBinary', () {
+    test('isTrue', () async {
+      final List<FakeCommand> commandList = <FakeCommand>[];
+      const String fileToCheck = '/a/b/c/one.zip';
+      const FakeCommand findCmd = FakeCommand(
+        command: <String>[
+          'file',
+          '--mime-type',
+          '-b',
+          fileToCheck,
+        ],
+        stdout: 'application/x-mach-binary',
+      );
+      commandList.add(findCmd);
+      final ProcessManager processManager = FakeProcessManager.list(commandList);
+      final bool result = await isBinary(fileToCheck, processManager: processManager);
+      expect(result, isTrue);
+    });
+
+    test('isFalse', () async {
+      final List<FakeCommand> commandList = <FakeCommand>[];
+      const String fileToCheck = '/a/b/c/one.zip';
+      const FakeCommand findCmd = FakeCommand(
+        command: <String>[
+          'file',
+          '--mime-type',
+          '-b',
+          fileToCheck,
+        ],
+        stdout: 'text/xml',
+      );
+      commandList.add(findCmd);
+      final ProcessManager processManager = FakeProcessManager.list(commandList);
+      final bool result = await isBinary(fileToCheck, processManager: processManager);
+      expect(result, isFalse);
+    });
+  });
+
+  group('hasExpectedEntitlements', () {
+     test('expected entitlements', () async {
+       final List<FakeCommand> commandList = <FakeCommand>[];
+       const String fileToCheck = '/a/b/c/one.zip';
+       const FakeCommand codesignCmd = FakeCommand(
+         command: <String>[
+           'codesign',
+           '--display',
+           '--entitlements',
+           ':-',
+           fileToCheck,
+         ],
+       );
+       commandList.add(codesignCmd);
+       final ProcessManager processManager = FakeProcessManager.list(commandList);
+       final bool result = await hasExpectedEntitlements(fileToCheck, flutterRoot, processManager: processManager);
+       expect(result, isTrue);
+     });
+
+     test('unexpected entitlements', () async {
+       final List<FakeCommand> commandList = <FakeCommand>[];
+       const String fileToCheck = '/a/b/c/one.zip';
+       const FakeCommand codesignCmd = FakeCommand(
+         command: <String>[
+           'codesign',
+           '--display',
+           '--entitlements',
+           ':-',
+           fileToCheck,
+         ],
+         exitCode: 1,
+       );
+       commandList.add(codesignCmd);
+       final ProcessManager processManager = FakeProcessManager.list(commandList);
+       final bool result = await hasExpectedEntitlements(fileToCheck, flutterRoot, processManager: processManager);
+       expect(result, isFalse);
+     });
+    });
+  });
+
+  group('verifySignatures', () {
+
+    test('succeeds if every binary is codesigned and has correct entitlements', () async {
+      final List<FakeCommand> commandList = <FakeCommand>[];
+      final FakeCommand findCmd = FakeCommand(
+        command: const <String>[
+          'find',
+          '$flutterRoot/bin/cache',
+          '-type',
+          'f',],
+        stdout: allFilesStdout,
+          );
+      commandList.add(findCmd);
+      for (final String expectedFile in allExpectedFiles) {
+        commandList.add(
+          FakeCommand(
+            command: <String>[
+              'file',
+              '--mime-type',
+              '-b',
+              expectedFile,
+            ],
+            stdout: 'application/x-mach-binary',
+          )
+        );
+      }
+      for (final String expectedFile in allExpectedFiles) {
+        commandList.add(
+          FakeCommand(
+            command: <String>[
+              'codesign',
+              '-vvv',
+              expectedFile,
+            ],
+          )
+        );
+        if (withEntitlements.contains(expectedFile)) {
+          commandList.add(
+            FakeCommand(
+              command: <String>[
+                'codesign',
+                '--display',
+                '--entitlements',
+                ':-',
+                expectedFile,
+              ],
+              stdout: expectedEntitlements.join('\n'),
+            )
+          );
+        }
+      }
+      final ProcessManager processManager = FakeProcessManager.list(commandList);
+      await expectLater(verifySignatures(flutterRoot, processManager: processManager), completes);
+    });
+
+    test('fails if binaries do not have the right entitlements', () async {
+      final List<FakeCommand> commandList = <FakeCommand>[];
+      final FakeCommand findCmd = FakeCommand(
+        command: const <String>[
+          'find',
+          '$flutterRoot/bin/cache',
+          '-type',
+          'f',],
+        stdout: allFilesStdout,
+          );
+      commandList.add(findCmd);
+      for (final String expectedFile in allExpectedFiles) {
+        commandList.add(
+          FakeCommand(
+            command: <String>[
+              'file',
+              '--mime-type',
+              '-b',
+              expectedFile,
+            ],
+            stdout: 'application/x-mach-binary',
+          )
+        );
+      }
+      for (final String expectedFile in allExpectedFiles) {
+        commandList.add(
+          FakeCommand(
+            command: <String>[
+              'codesign',
+              '-vvv',
+              expectedFile,
+            ],
+          )
+        );
+        if (withEntitlements.contains(expectedFile)) {
+          commandList.add(
+            FakeCommand(
+              command: <String>[
+                'codesign',
+                '--display',
+                '--entitlements',
+                ':-',
+                expectedFile,
+              ],
+            )
+          );
+        }
+      }
+      final ProcessManager processManager = FakeProcessManager.list(commandList);
+
+      expect(
+        () async => verifySignatures(flutterRoot, processManager: processManager),
+        throwsExceptionWith('Test failed because files found with the wrong entitlements'),
+      );
+    });
+  });
+}
diff --git a/dev/bots/test/common.dart b/dev/bots/test/common.dart
index 86a3628..876895c 100644
--- a/dev/bots/test/common.dart
+++ b/dev/bots/test/common.dart
@@ -21,3 +21,13 @@
     print('Failed to delete ${directory.path}: $error');
   }
 }
+
+Matcher throwsExceptionWith(String messageSubString) {
+  return throwsA(
+      isA<Exception>().having(
+          (Exception e) => e.toString(),
+          'description',
+          contains(messageSubString),
+      ),
+  );
+}