blob: b210e5e35d977cdaac0d9848aebd8f5751d1a216 [file]
#!/usr/bin/env dart
// Copyright 2024 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.
// Helper script to import a flutter p12 identity.
import 'dart:io' as io;
const String keychainName = 'build.keychain';
const String keychainPassword = '';
const int totalRetryAttempts = 3;
Future<void> main() async {
final io.File logFile = io.File(
io.Platform.environment['SETUP_KEYCHAIN_LOGS_PATH']!,
);
final logSink = logFile.openWrite();
void log(String line) {
logSink.writeln('$line\n');
}
final SetupKeychain setupKeychain = SetupKeychain(log: log);
int exitCode = 1;
try {
exitCode = await setupKeychain.setup(
passwordPath: io.Platform.environment['FLUTTER_P12_PASSWORD']!,
flutterP12Path: io.Platform.environment['FLUTTER_P12']!,
p12SuffixFilePath: io.Platform.environment['P12_SUFFIX_FILEPATH']!,
codesignPath: io.Platform.environment['CODESIGN_PATH']!,
);
} finally {
await logSink.flush();
await logSink.close();
}
io.exit(exitCode);
}
class SetupKeychain {
SetupKeychain({required this.log});
final void Function(String) log;
/// Create a keychain called `build.keychain` and import the Flutter private key (p12),
/// the password for the p12, and various Apple certs into it.
Future<int> setup({
required String passwordPath,
required String flutterP12Path,
required String p12SuffixFilePath,
required String codesignPath,
}) async {
try {
final String rawPassword = io.File(passwordPath).readAsStringSync();
// Only filepath with a .p12 suffix will be recognized
io.File(flutterP12Path).renameSync(p12SuffixFilePath);
// Delete build.keychain if it exists
_security(const <String>[
'delete-keychain',
keychainName,
], allowNonzero: true);
// Create keychain.
_security(const <String>[
'create-keychain',
'-p',
keychainPassword,
keychainName,
]);
await _downloadAndImportAppleCert(
'DeveloperIDG2CA',
codesignPath,
keychainName,
);
// Retrieve current list of keychains on the search list of current machine.
final keychains =
_security(const <String>[
'list-keychains',
'-d',
'user',
]).split('\n').map<String?>((String line) {
final RegExp pattern = RegExp(r'^\s*".*\/([a-zA-Z0-9.]+)-db"');
final RegExpMatch? match = pattern.firstMatch(line);
if (match == null) {
return null;
}
// The first (and only) capture group is the name of the keychain
return match.group(1);
}).whereType<String>();
print('User keychains on this machine: $keychains');
// Add keychain name to search list.
// Without this, future commands such as `security import`,
// `security find-identity` and `codesign ...` will fail to find the cert
// in our newly created keychain.
_security(<String>[
'-v',
'list-keychains',
// TODO(fujino): we probably don't need $keychains here, only keychainName should be required
'-s', ...keychains, keychainName,
]);
// For diagnostic purposes, list keychains AFTER adding our newly created
// keychain to the search list.
_security(const <String>['list-keychains', '-d', 'user']);
// Set $keychainName as default.
_security(<String>['default-keychain', '-s', keychainName]);
// Unlock keychainName to allow sign commands to use its certs.
_security(<String>[
'unlock-keychain',
'-p',
keychainPassword,
keychainName,
]);
// This will be exponentially increased on retries
int sleepSeconds = 2;
for (int attempt = 0; attempt < totalRetryAttempts; attempt++) {
_security(<String>[
'import',
p12SuffixFilePath,
'-k', keychainName,
'-P', rawPassword,
// -T allows the specified program to access this identity
'-T', codesignPath,
'-T', '/usr/bin/codesign',
]);
_security(<String>[
'set-key-partition-list',
'-S',
'apple-tool:,apple:,codesign:',
'-s',
'-k',
'',
keychainName,
]);
final String identities = _security(const <String>[
'find-identity',
'-v',
keychainName,
]);
if (identities.contains('FLUTTER.IO LLC')) {
log(
'successfully found a Flutter identity in the $keychainName keychain',
);
if (identities.contains('CSSMERR_TP_NOT_TRUSTED')) {
await _verifyCertWithRetries();
}
return 0;
}
log(
'failed to find a Flutter identity in the $keychainName keychain on attempt $attempt',
);
await Future<void>.delayed(Duration(seconds: sleepSeconds));
sleepSeconds *= sleepSeconds;
}
} finally {
_security(const <String>[
'find-certificate',
// Find all matching certificates, not just the first
'-a',
]);
}
log(
'failed to find a Flutter identity after $totalRetryAttempts attempts.',
);
return 1;
}
String _security(List<String> args, {bool allowNonzero = false}) {
log('Executing ${<String>['/usr/bin/security', ...args]}');
final io.ProcessResult result = io.Process.runSync(
'/usr/bin/security',
args,
);
log('process finished with exitCode ${result.exitCode}');
log('STDOUT:\n\n${result.stdout}');
log('STDERR:\n\n${result.stderr}');
if (!allowNonzero && result.exitCode != 0) {
throw io.ProcessException(
'/usr/bin/security',
args,
'failed',
result.exitCode,
);
}
return result.stdout as String;
}
Future<void> _verifyCertWithRetries() async {
// Verify the certificate is valid. If it isn't, check again after n seconds.
int sleepSeconds = 3;
for (int attempt = 0; attempt < totalRetryAttempts; attempt++) {
final String results = _verifyCert(allowNonzero: true);
if (!results.contains('Certificate errors') && !results.contains('Trust evaluation errors')) {
return;
}
await Future<void>.delayed(Duration(seconds: sleepSeconds));
sleepSeconds *= sleepSeconds;
}
_verifyCert();
}
String _verifyCert({bool allowNonzero = false}) {
// Find the FLUTTER.IO LLC certificate and convert it to a .pem file.
final String certContents = _security(const <String>[
'find-certificate',
'-c',
'FLUTTER.IO LLC',
'-p',
]);
final io.Directory tempCertDirectory =
io.Directory.systemTemp.createTempSync();
final io.File tempCertFile = io.File(
'${tempCertDirectory.path}/temp_cert.pem',
);
tempCertFile.createSync();
tempCertFile.writeAsStringSync(certContents, flush: true);
// Verify the cert. This will log the certificate chain and trust
// evaluation results.
final String results = _security(<String>[
'verify-cert',
'-c',
tempCertFile.absolute.path,
'-k',
keychainName,
'-vvv',
], allowNonzero: allowNonzero);
tempCertFile.deleteSync();
return results;
}
Future<void> _downloadAndImportAppleCert(
String certName,
String codesignPath,
String targetKeychain, {
bool allowNonzero = false,
}) async {
// TODO(vashworth): cache this via CIPD
final io.File certFile = await _downloadFile(
// Link from https://www.apple.com/certificateauthority
remoteUri: Uri.parse(
'https://www.apple.com/certificateauthority/$certName.cer',
),
localPath: './$certName.cer',
);
// LOAD certificate authority into the build keychain
await _security(<String>[
'import',
certFile.absolute.path,
'-k', targetKeychain,
// -T allows the specified program to access this identity
'-T', codesignPath,
'-T', '/usr/bin/codesign',
], allowNonzero: allowNonzero);
}
Future<io.File> _downloadFile({
required Uri remoteUri,
required String localPath,
}) async {
final io.HttpClient client = io.HttpClient();
final io.HttpClientRequest request = await client.getUrl(remoteUri);
final io.HttpClientResponse response = await request.close();
final io.File certFile = io.File(localPath);
final io.IOSink sink = certFile.openWrite();
await response.pipe(sink);
client.close();
return certFile;
}
}