add codesignature verification tool (#2386)

* rebase upstream

* update licenses

* un-bump pubspec.lock sdk version

* format

* code review

* add macOS check
diff --git a/codesign/bin/verify.dart b/codesign/bin/verify.dart
new file mode 100644
index 0000000..f467d7a
--- /dev/null
+++ b/codesign/bin/verify.dart
@@ -0,0 +1,29 @@
+// Copyright 2019 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:codesign/verify.dart';
+import 'package:logging/logging.dart';
+
+Future<void> main(List<String> args) async {
+  final Logger logger = Logger('root');
+  logger.onRecord.listen((LogRecord record) {
+    io.stdout.writeln(record.message);
+  });
+  if (args.length != 1) {
+    logger.info('Usage: dart verify.dart [FILE]');
+    io.exit(1);
+  }
+  if (!io.Platform.isMacOS) {
+    logger.severe('This tool must be run from macOS.');
+    io.exit(1);
+  }
+  final inputFile = args[0];
+  final VerificationResult result = await VerificationService(
+    binaryPath: inputFile,
+    logger: logger,
+  ).run();
+  io.exit(result == VerificationResult.codesignedAndNotarized ? 0 : 1);
+}
diff --git a/codesign/lib/verify.dart b/codesign/lib/verify.dart
new file mode 100644
index 0000000..15c3d22
--- /dev/null
+++ b/codesign/lib/verify.dart
@@ -0,0 +1,185 @@
+// Copyright 2019 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:file/file.dart';
+import 'package:file/local.dart';
+import 'package:logging/logging.dart';
+import 'package:process/process.dart';
+
+const String kNotNotarizedMessage = 'test-requirement: code failed to satisfy specified code requirement(s)';
+
+enum VerificationResult {
+  unsigned,
+  codesignedOnly,
+  codesignedAndNotarized,
+}
+
+class VerificationService {
+  VerificationService({
+    required this.binaryPath,
+    required this.logger,
+    this.fs = const LocalFileSystem(),
+    this.pm = const LocalProcessManager(),
+  }) {
+    if (!fs.file(binaryPath).existsSync()) {
+      throw Exception(
+        'Input file `$binaryPath` does not exist--please provide the path to a '
+        'valid binary to verify.',
+      );
+    }
+    if (!pm.canRun('codesign')) {
+      throw Exception(
+        'The binary `codesign` is required to run this tool. Do you have '
+        'Xcode installed?',
+      );
+    }
+  }
+
+  final Logger logger;
+  final String binaryPath;
+  final FileSystem fs;
+  final ProcessManager pm;
+
+  late final String _codesignTimestamp;
+  late final String _format;
+  late final String _signatureSize;
+  late final String _codesignId;
+  bool? _notarizationStatus;
+
+  Future<VerificationResult> run() async {
+    if (!await _codesignDisplay()) {
+      return VerificationResult.unsigned;
+    }
+    await _notarization();
+    logger.info(present());
+    return _notarizationStatus! ? VerificationResult.codesignedAndNotarized : VerificationResult.codesignedOnly;
+  }
+
+  Future<void> _notarization() async {
+    final List<String> command = <String>[
+      'codesign',
+      '--verify',
+      '-v',
+      // force online notarization check
+      '-R=notarized',
+      '--check-notarization',
+      binaryPath,
+    ];
+    final io.ProcessResult result = await pm.run(command);
+
+    // This usually means it does not satisfy notarization requirement
+    if (result.exitCode == 3 && (result.stderr as String).contains(kNotNotarizedMessage)) {
+      _notarizationStatus = false;
+      return;
+    }
+    if (result.exitCode != 0) {
+      throw Exception('''
+Command `${command.join(' ')}` failed with code ${result.exitCode}
+
+${result.stderr}
+''');
+    }
+    final String stderr = result.stderr as String;
+    if (stderr.contains('explicit requirement satisfied')) {
+      _notarizationStatus = true;
+      return;
+    }
+    throw UnimplementedError('Failed parsing the output of `${command.join(' ')}`:\n\n$stderr');
+  }
+
+  String present() {
+    return '''
+Authority:      $_codesignId
+Time stamp:     $_codesignTimestamp
+Format:         $_format
+Signature size: $_signatureSize
+Notarization:   $_notarizationStatus
+''';
+  }
+
+  /// Display overall information, intended to be machine parseable
+  ///
+  /// Output is of the format:
+  ///
+  /// Executable=/Users/developer/Downloads/mybinary
+  /// Identifier=mybinary
+  /// Format=Mach-O thin (x86_64)
+  /// CodeDirectory v=20500 size=38000 flags=0x10000(runtime) hashes=1177+7 location=embedded
+  /// Signature size=8979
+  /// Authority=Developer ID Application: Dev Shop ABC (ABCC0VV123)
+  /// Authority=Developer ID Certification Authority
+  /// Authority=Apple Root CA
+  /// Timestamp=Jan 9, 2023 at 9:39:07 AM
+  /// Info.plist=not bound
+  /// TeamIdentifier=ABCC0VV123
+  /// Runtime Version=13.1.0
+  /// Sealed Resources=none
+  /// Internal requirements count=1 size=164
+  Future<bool> _codesignDisplay() async {
+    final List<String> command = <String>[
+      'codesign',
+      '--display',
+      '-vv',
+      binaryPath,
+    ];
+    final io.ProcessResult result = await pm.run(command);
+
+    if (result.exitCode == 1) {
+      logger.severe('''
+File $binaryPath is not codesigned. To manually verify, run:
+
+codesign --display -vv $binaryPath
+''');
+      return false;
+    } else if (result.exitCode != 0) {
+      throw Exception(
+        'Command `${command.join(' ')}` failed with code ${result.exitCode}\n\n'
+        '${result.stderr}',
+      );
+    }
+
+    final List<String> lines = result.stderr.toString().trim().split('\n');
+    for (final String line in lines) {
+      if (line.trim().isEmpty) {
+        continue;
+      }
+      final List<String> segments = line.split('=');
+      final String name = segments.first;
+
+      switch (name) {
+        case 'Executable':
+        case 'Identifier':
+        case 'CodeDirectory v':
+        case 'Info.plist':
+        // TeamIdentifier is redundant with the Authority field
+        case 'TeamIdentifier':
+        case 'Runtime Version':
+        case 'Sealed Resources':
+        case 'Internal requirements count':
+          break;
+        case 'Signature size':
+          _signatureSize = segments.sublist(1).join();
+          break;
+        case 'Authority':
+          if (segments[1].startsWith('Developer ID Application')) {
+            _codesignId = segments[1];
+          }
+          break;
+        case 'Timestamp':
+          _codesignTimestamp = segments[1];
+          break;
+        case 'Format':
+          _format = segments.sublist(1).join();
+          break;
+        default:
+          logger.warning(
+            'Do not know how to parse a $name, skipping this field.',
+          );
+      }
+    }
+    return true;
+  }
+}
diff --git a/codesign/pubspec.lock b/codesign/pubspec.lock
index 9d1eedc..58adfc1 100644
--- a/codesign/pubspec.lock
+++ b/codesign/pubspec.lock
@@ -5,372 +5,425 @@
     dependency: transitive
     description:
       name: _fe_analyzer_shared
-      url: "https://pub.dartlang.org"
+      sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201"
+      url: "https://pub.dev"
     source: hosted
     version: "52.0.0"
   analyzer:
     dependency: transitive
     description:
       name: analyzer
-      url: "https://pub.dartlang.org"
+      sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4
+      url: "https://pub.dev"
     source: hosted
     version: "5.4.0"
   archive:
     dependency: "direct main"
     description:
       name: archive
-      url: "https://pub.dartlang.org"
+      sha256: ed7cc591a948744994714375caf9a2ce89e1d82e8243997c8a2994d57181c212
+      url: "https://pub.dev"
     source: hosted
     version: "3.3.5"
   args:
     dependency: "direct main"
     description:
       name: args
-      url: "https://pub.dartlang.org"
+      sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
+      url: "https://pub.dev"
     source: hosted
     version: "2.3.2"
   async:
     dependency: transitive
     description:
       name: async
-      url: "https://pub.dartlang.org"
+      sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
+      url: "https://pub.dev"
     source: hosted
     version: "2.10.0"
   boolean_selector:
     dependency: transitive
     description:
       name: boolean_selector
-      url: "https://pub.dartlang.org"
+      sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+      url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
   clock:
     dependency: transitive
     description:
       name: clock
-      url: "https://pub.dartlang.org"
+      sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+      url: "https://pub.dev"
     source: hosted
     version: "1.1.1"
   collection:
     dependency: transitive
     description:
       name: collection
-      url: "https://pub.dartlang.org"
+      sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
+      url: "https://pub.dev"
     source: hosted
     version: "1.17.0"
   convert:
     dependency: transitive
     description:
       name: convert
-      url: "https://pub.dartlang.org"
+      sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+      url: "https://pub.dev"
     source: hosted
     version: "3.1.1"
   coverage:
     dependency: transitive
     description:
       name: coverage
-      url: "https://pub.dartlang.org"
+      sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040"
+      url: "https://pub.dev"
     source: hosted
     version: "1.6.2"
   crypto:
     dependency: "direct main"
     description:
       name: crypto
-      url: "https://pub.dartlang.org"
+      sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
+      url: "https://pub.dev"
     source: hosted
     version: "3.0.2"
   fake_async:
     dependency: "direct main"
     description:
       name: fake_async
-      url: "https://pub.dartlang.org"
+      sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+      url: "https://pub.dev"
     source: hosted
     version: "1.3.1"
   file:
     dependency: "direct main"
     description:
       name: file
-      url: "https://pub.dartlang.org"
+      sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
+      url: "https://pub.dev"
     source: hosted
     version: "6.1.4"
   flutter_lints:
     dependency: "direct main"
     description:
       name: flutter_lints
-      url: "https://pub.dartlang.org"
+      sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
+      url: "https://pub.dev"
     source: hosted
     version: "2.0.1"
   frontend_server_client:
     dependency: transitive
     description:
       name: frontend_server_client
-      url: "https://pub.dartlang.org"
+      sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
+      url: "https://pub.dev"
     source: hosted
     version: "3.2.0"
   glob:
     dependency: transitive
     description:
       name: glob
-      url: "https://pub.dartlang.org"
+      sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
+      url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
   http_multi_server:
     dependency: transitive
     description:
       name: http_multi_server
-      url: "https://pub.dartlang.org"
+      sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
+      url: "https://pub.dev"
     source: hosted
     version: "3.2.1"
   http_parser:
     dependency: transitive
     description:
       name: http_parser
-      url: "https://pub.dartlang.org"
+      sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+      url: "https://pub.dev"
     source: hosted
     version: "4.0.2"
   io:
     dependency: transitive
     description:
       name: io
-      url: "https://pub.dartlang.org"
+      sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
+      url: "https://pub.dev"
     source: hosted
-    version: "1.0.3"
+    version: "1.0.4"
   js:
     dependency: transitive
     description:
       name: js
-      url: "https://pub.dartlang.org"
+      sha256: "323b7c70073cccf6b9b8d8b334be418a3293cfb612a560dc2737160a37bf61bd"
+      url: "https://pub.dev"
     source: hosted
-    version: "0.6.5"
+    version: "0.6.6"
   lints:
     dependency: "direct dev"
     description:
       name: lints
-      url: "https://pub.dartlang.org"
+      sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
+      url: "https://pub.dev"
     source: hosted
     version: "2.0.1"
   logging:
     dependency: "direct main"
     description:
       name: logging
-      url: "https://pub.dartlang.org"
+      sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946
+      url: "https://pub.dev"
     source: hosted
     version: "1.1.0"
   matcher:
     dependency: transitive
     description:
       name: matcher
-      url: "https://pub.dartlang.org"
+      sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8
+      url: "https://pub.dev"
     source: hosted
     version: "0.12.14"
   meta:
     dependency: "direct main"
     description:
       name: meta
-      url: "https://pub.dartlang.org"
+      sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
+      url: "https://pub.dev"
     source: hosted
     version: "1.8.0"
   mime:
     dependency: transitive
     description:
       name: mime
-      url: "https://pub.dartlang.org"
+      sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
+      url: "https://pub.dev"
     source: hosted
     version: "1.0.4"
   node_preamble:
     dependency: transitive
     description:
       name: node_preamble
-      url: "https://pub.dartlang.org"
+      sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d"
+      url: "https://pub.dev"
     source: hosted
     version: "2.0.1"
   package_config:
     dependency: transitive
     description:
       name: package_config
-      url: "https://pub.dartlang.org"
+      sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+      url: "https://pub.dev"
     source: hosted
     version: "2.1.0"
   path:
     dependency: transitive
     description:
       name: path
-      url: "https://pub.dartlang.org"
+      sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
+      url: "https://pub.dev"
     source: hosted
     version: "1.8.3"
   platform:
     dependency: "direct main"
     description:
       name: platform
-      url: "https://pub.dartlang.org"
+      sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
+      url: "https://pub.dev"
     source: hosted
     version: "3.1.0"
   pointycastle:
     dependency: transitive
     description:
       name: pointycastle
-      url: "https://pub.dartlang.org"
+      sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346
+      url: "https://pub.dev"
     source: hosted
     version: "3.6.2"
   pool:
     dependency: transitive
     description:
       name: pool
-      url: "https://pub.dartlang.org"
+      sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
+      url: "https://pub.dev"
     source: hosted
     version: "1.5.1"
   process:
     dependency: "direct main"
     description:
       name: process
-      url: "https://pub.dartlang.org"
+      sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
+      url: "https://pub.dev"
     source: hosted
     version: "4.2.4"
   pub_semver:
     dependency: transitive
     description:
       name: pub_semver
-      url: "https://pub.dartlang.org"
+      sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17"
+      url: "https://pub.dev"
     source: hosted
     version: "2.1.3"
   shelf:
     dependency: transitive
     description:
       name: shelf
-      url: "https://pub.dartlang.org"
+      sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c
+      url: "https://pub.dev"
     source: hosted
     version: "1.4.0"
   shelf_packages_handler:
     dependency: transitive
     description:
       name: shelf_packages_handler
-      url: "https://pub.dartlang.org"
+      sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306
+      url: "https://pub.dev"
     source: hosted
     version: "3.0.1"
   shelf_static:
     dependency: transitive
     description:
       name: shelf_static
-      url: "https://pub.dartlang.org"
+      sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c
+      url: "https://pub.dev"
     source: hosted
     version: "1.1.1"
   shelf_web_socket:
     dependency: transitive
     description:
       name: shelf_web_socket
-      url: "https://pub.dartlang.org"
+      sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8
+      url: "https://pub.dev"
     source: hosted
     version: "1.0.3"
   source_map_stack_trace:
     dependency: transitive
     description:
       name: source_map_stack_trace
-      url: "https://pub.dartlang.org"
+      sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
+      url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
   source_maps:
     dependency: transitive
     description:
       name: source_maps
-      url: "https://pub.dartlang.org"
+      sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427"
+      url: "https://pub.dev"
     source: hosted
     version: "0.10.11"
   source_span:
     dependency: transitive
     description:
       name: source_span
-      url: "https://pub.dartlang.org"
+      sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
+      url: "https://pub.dev"
     source: hosted
     version: "1.9.1"
   stack_trace:
     dependency: transitive
     description:
       name: stack_trace
-      url: "https://pub.dartlang.org"
+      sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
+      url: "https://pub.dev"
     source: hosted
     version: "1.11.0"
   stream_channel:
     dependency: transitive
     description:
       name: stream_channel
-      url: "https://pub.dartlang.org"
+      sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
+      url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
   string_scanner:
     dependency: transitive
     description:
       name: string_scanner
-      url: "https://pub.dartlang.org"
+      sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+      url: "https://pub.dev"
     source: hosted
     version: "1.2.0"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
-      url: "https://pub.dartlang.org"
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.dev"
     source: hosted
     version: "1.2.1"
   test:
     dependency: "direct dev"
     description:
       name: test
-      url: "https://pub.dartlang.org"
+      sha256: b54d427664c00f2013ffb87797a698883c46aee9288e027a50b46eaee7486fa2
+      url: "https://pub.dev"
     source: hosted
     version: "1.22.2"
   test_api:
     dependency: transitive
     description:
       name: test_api
-      url: "https://pub.dartlang.org"
+      sha256: "6182294da5abf431177fccc1ee02401f6df30f766bc6130a0852c6b6d7ee6b2d"
+      url: "https://pub.dev"
     source: hosted
     version: "0.4.18"
   test_core:
     dependency: transitive
     description:
       name: test_core
-      url: "https://pub.dartlang.org"
+      sha256: "95ecc12692d0dd59080ab2d38d9cf32c7e9844caba23ff6cd285690398ee8ef4"
+      url: "https://pub.dev"
     source: hosted
     version: "0.4.22"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
-      url: "https://pub.dartlang.org"
+      sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
+      url: "https://pub.dev"
     source: hosted
     version: "1.3.1"
   vm_service:
     dependency: transitive
     description:
       name: vm_service
-      url: "https://pub.dartlang.org"
+      sha256: d069ad658b700fc5fb774771ac8997f226ca29a45e0a450776fff3d969c8ba8f
+      url: "https://pub.dev"
     source: hosted
-    version: "10.0.0"
+    version: "10.1.0"
   watcher:
     dependency: transitive
     description:
       name: watcher
-      url: "https://pub.dartlang.org"
+      sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0"
+      url: "https://pub.dev"
     source: hosted
     version: "1.0.2"
   web_socket_channel:
     dependency: transitive
     description:
       name: web_socket_channel
-      url: "https://pub.dartlang.org"
+      sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b
+      url: "https://pub.dev"
     source: hosted
     version: "2.3.0"
   webkit_inspection_protocol:
     dependency: transitive
     description:
       name: webkit_inspection_protocol
-      url: "https://pub.dartlang.org"
+      sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
+      url: "https://pub.dev"
     source: hosted
     version: "1.2.0"
   yaml:
     dependency: transitive
     description:
       name: yaml
-      url: "https://pub.dartlang.org"
+      sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
+      url: "https://pub.dev"
     source: hosted
     version: "3.1.1"
 sdks:
-  dart: ">=2.18.0 <3.0.0"
+  dart: ">=2.18.0 <4.0.0"
diff --git a/codesign/test/verify_test.dart b/codesign/test/verify_test.dart
new file mode 100644
index 0000000..49d9d22
--- /dev/null
+++ b/codesign/test/verify_test.dart
@@ -0,0 +1,162 @@
+// Copyright 2019 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 'package:codesign/verify.dart';
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:logging/logging.dart';
+import 'package:test/test.dart';
+
+import 'src/fake_process_manager.dart';
+
+void main() {
+  const String binaryPath = '/path/to/binary';
+
+  late FakeProcessManager processManager;
+  late FileSystem fs;
+  late Logger logger;
+  late VerificationService service;
+  late List<LogRecord> logs;
+
+  setUp(() {
+    fs = MemoryFileSystem.test();
+    fs.file(binaryPath).createSync(recursive: true);
+    processManager = FakeProcessManager.empty();
+    logs = <LogRecord>[];
+    logger = Logger.detached('test');
+    logger.onRecord.listen((LogRecord record) => logs.add(record));
+    service = VerificationService(
+      binaryPath: binaryPath,
+      fs: fs,
+      logger: logger,
+      pm: processManager,
+    );
+  });
+
+  test('parses codesign output and can present', () async {
+    processManager.addCommands(const <FakeCommand>[
+      FakeCommand(
+        command: <String>['codesign', '--display', '-vv', binaryPath],
+        stderr: '''
+Executable=$binaryPath
+Identifier=mybinary
+Format=Mach-O thin (x86_64)
+CodeDirectory v=20500 size=38000 flags=0x10000(runtime) hashes=1177+7 location=embedded
+Signature size=8979
+Authority=Developer ID Application: Dev Shop ABC (ABCC0VV123)
+Authority=Developer ID Certification Authority
+Authority=Apple Root CA
+Timestamp=Jan 9, 2023 at 9:39:07 AM
+Info.plist=not bound
+TeamIdentifier=ABCC0VV123
+Runtime Version=13.1.0
+Sealed Resources=none
+Internal requirements count=1 size=164
+''',
+      ),
+      FakeCommand(
+        command: <String>[
+          'codesign',
+          '--verify',
+          '-v',
+          '-R=notarized',
+          '--check-notarization',
+          binaryPath,
+        ],
+        stderr: '''
+$binaryPath: valid on disk
+$binaryPath: satisfies its Designated Requirement
+$binaryPath: explicit requirement satisfied
+''',
+      ),
+    ]);
+    final VerificationResult result = await service.run();
+    expect(processManager, hasNoRemainingExpectations);
+    expect(result, VerificationResult.codesignedAndNotarized);
+    expect(
+      service.present(),
+      '''
+Authority:      Developer ID Application: Dev Shop ABC (ABCC0VV123)
+Time stamp:     Jan 9, 2023 at 9:39:07 AM
+Format:         Mach-O thin (x86_64)
+Signature size: 8979
+Notarization:   true
+''',
+    );
+  });
+
+  test('detects codesigned but not notarized binary', () async {
+    processManager.addCommands(const <FakeCommand>[
+      FakeCommand(
+        command: <String>['codesign', '--display', '-vv', binaryPath],
+        stderr: '''
+Executable=$binaryPath
+Identifier=mybinary
+Format=Mach-O thin (x86_64)
+CodeDirectory v=20500 size=38000 flags=0x10000(runtime) hashes=1177+7 location=embedded
+Signature size=8979
+Authority=Developer ID Application: Dev Shop ABC (ABCC0VV123)
+Authority=Developer ID Certification Authority
+Authority=Apple Root CA
+Timestamp=Jan 9, 2023 at 9:39:07 AM
+Info.plist=not bound
+TeamIdentifier=ABCC0VV123
+Runtime Version=13.1.0
+Sealed Resources=none
+Internal requirements count=1 size=164
+''',
+      ),
+      FakeCommand(
+        command: <String>[
+          'codesign',
+          '--verify',
+          '-v',
+          '-R=notarized',
+          '--check-notarization',
+          binaryPath,
+        ],
+        stderr: '''
+$binaryPath: valid on disk
+$binaryPath: satisfies its Designated Requirement
+test-requirement: code failed to satisfy specified code requirement(s)
+''',
+        exitCode: 3,
+      ),
+    ]);
+    final VerificationResult result = await service.run();
+    expect(processManager, hasNoRemainingExpectations);
+    expect(result, VerificationResult.codesignedOnly);
+    expect(
+      service.present(),
+      '''
+Authority:      Developer ID Application: Dev Shop ABC (ABCC0VV123)
+Time stamp:     Jan 9, 2023 at 9:39:07 AM
+Format:         Mach-O thin (x86_64)
+Signature size: 8979
+Notarization:   false
+''',
+    );
+  });
+
+  test('detects unsigned binary', () async {
+    processManager.addCommands(const <FakeCommand>[
+      FakeCommand(
+        command: <String>['codesign', '--display', '-vv', binaryPath],
+        stderr: '$binaryPath: code object is not signed at all',
+        exitCode: 1,
+      ),
+    ]);
+    final VerificationResult result = await service.run();
+    expect(processManager, hasNoRemainingExpectations);
+    expect(result, VerificationResult.unsigned);
+    expect(
+      logs.first,
+      isA<LogRecord>().having(
+        (LogRecord record) => record.message,
+        'message',
+        contains('File $binaryPath is not codesigned.'),
+      ),
+    );
+  });
+}