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.'),
+ ),
+ );
+ });
+}