blob: 15c3d22134d372c74624ba02dbd199c5dda7a227 [file] [log] [blame]
// 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;
}
}