| // 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; |
| } |
| } |