| // Copyright 2014 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. | |
| /// @docImport 'application_package.dart'; | |
| library; | |
| import 'dart:async'; | |
| import 'package:process/process.dart'; | |
| import '../base/common.dart'; | |
| import '../base/config.dart'; | |
| import '../base/file_system.dart'; | |
| import '../base/io.dart'; | |
| import '../base/logger.dart'; | |
| import '../base/platform.dart'; | |
| import '../base/process.dart'; | |
| import '../base/terminal.dart'; | |
| import '../convert.dart' show utf8; | |
| import 'plist_parser.dart'; | |
| typedef _CertificateTeamInfo = ({String teamId, String teamName}); | |
| const _developmentTeamBuildSettingName = 'DEVELOPMENT_TEAM'; | |
| const _codeSignStyleBuildSettingName = 'CODE_SIGN_STYLE'; | |
| const _provisioningProfileSpecifierBuildSettingName = 'PROVISIONING_PROFILE_SPECIFIER'; | |
| const _provisioningProfileBuildSettingName = 'PROVISIONING_PROFILE'; | |
| const _codeSignSelectionCanceled = 'Code-signing setup canceled. Your changes have not been saved.'; | |
| /// User message when no development certificates are found in the keychain. | |
| /// | |
| /// The user likely never did any iOS development. | |
| String noCertificatesInstruction({ | |
| bool includeTrustStep = true, | |
| bool includeSimulatorAlternative = true, | |
| }) { | |
| final simulatorAlternative = includeSimulatorAlternative | |
| ? '\nOr run on an iOS simulator without code signing' | |
| : ''; | |
| return ''' | |
| ════════════════════════════════════════════════════════════════════════════════ | |
| No valid code signing certificates were found | |
| You can connect to your Apple Developer account by signing in with your Apple ID | |
| in Xcode and create an iOS Development Certificate as well as a Provisioning\u0020 | |
| Profile for your project by: | |
| ${fixWithDevelopmentTeamInstruction(includeTrustStep: includeTrustStep)} | |
| For more information, please visit: | |
| https://developer.apple.com/library/content/documentation/IDEs/Conceptual/ | |
| AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html | |
| $simulatorAlternative | |
| ════════════════════════════════════════════════════════════════════════════════'''; | |
| } | |
| /// User message when there are no provisioning profile for the current app bundle identifier. | |
| /// | |
| /// The user did iOS development but never on this project and/or device. | |
| String noProvisioningProfileInstruction = | |
| ''' | |
| ════════════════════════════════════════════════════════════════════════════════ | |
| No Provisioning Profile was found for your project's Bundle Identifier or your\u0020 | |
| device. You can create a new Provisioning Profile for your project in Xcode for\u0020 | |
| your team by: | |
| ${fixWithDevelopmentTeamInstruction(includeTrustStep: false)} | |
| It's also possible that a previously installed app with the same Bundle\u0020 | |
| Identifier was signed with a different certificate. | |
| For more information, please visit: | |
| https://flutter.dev/to/ios-app-signing | |
| Or run on an iOS simulator without code signing | |
| ════════════════════════════════════════════════════════════════════════════════'''; | |
| /// Fallback error message for signing issues. | |
| /// | |
| /// Couldn't auto sign the app but can likely solved by retracing the signing flow in Xcode. | |
| String noDevelopmentTeamInstruction = | |
| ''' | |
| ════════════════════════════════════════════════════════════════════════════════ | |
| Building a deployable iOS app requires a selected Development Team with a\u0020 | |
| Provisioning Profile. Please ensure that a Development Team is selected by: | |
| ${fixWithDevelopmentTeamInstruction(includeTrustStep: false)} | |
| For more information, please visit: | |
| https://flutter.dev/to/ios-development-team | |
| Or run on an iOS simulator without code signing | |
| ════════════════════════════════════════════════════════════════════════════════'''; | |
| String fixWithDevelopmentTeamInstruction({required bool includeTrustStep}) { | |
| final trustStep = includeTrustStep | |
| ? '\n 5- Trust your newly created Development Certificate on your iOS device\n' | |
| ' via Settings > General > Device Management > [your new certificate] > Trust' | |
| : ''; | |
| return ''' | |
| 1- Open the Flutter project's Xcode target with | |
| open ios/Runner.xcworkspace | |
| 2- Select the 'Runner' project in the navigator then the 'Runner' target | |
| in the project settings | |
| 3- Make sure a 'Development Team' is selected under Signing & Capabilities > Team.\u0020 | |
| You may need to: | |
| - Log in with your Apple ID in Xcode first | |
| - Ensure you have a valid unique Bundle ID | |
| - Register your device with your Apple Developer Account | |
| - Let Xcode automatically provision a profile for your app | |
| 4- Build or run your project again$trustStep'''; | |
| } | |
| /// Pattern to extract identity from list of identities. | |
| /// | |
| /// Example: | |
| /// | |
| /// ` 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"` | |
| /// extracts `iPhone Developer: Profile 1 (1111AAAA11)` | |
| final _securityFindIdentityDeveloperIdentityExtractionPattern = RegExp( | |
| r'^\s*\d+\).+"(.+Develop(ment|er).+)"$', | |
| ); | |
| /// Pattern to extract unique identifier from certificate Common Name. | |
| /// | |
| /// Example: | |
| /// | |
| /// `iPhone Developer: Profile 1 (1111AAAA11)` | |
| /// extracts `1111AAAA11` | |
| final _securityFindIdentityCertificateCnExtractionPattern = RegExp(r'.*\(([a-zA-Z0-9]+)\)'); | |
| /// Pattern to extract OU (Organizational Unit) from certificate subject. | |
| /// | |
| /// Example: | |
| /// | |
| /// `subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O/O=Company LLC/C=US` | |
| /// extracts `ABCDE1F2DH` | |
| final _certificateOrganizationalUnitExtractionPattern = RegExp(r'OU=([a-zA-Z0-9]+)'); | |
| /// Pattern to extract O (Organization) from certificate subject. | |
| /// | |
| /// Requires a separator character (comma, slash, or space) or start-of-string | |
| /// immediately before `O=` to avoid false positives on fields like `CO=`. | |
| /// | |
| /// Example: | |
| /// | |
| /// `subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US` | |
| /// extracts `Company LLC` | |
| final _certificateOrganizationExtractionPattern = RegExp(r'(?:^|[,/ ])O=([^,/\n]+)'); | |
| /// Pattern to extract CN (Common Name) from certificate subject. | |
| /// | |
| /// Example: | |
| /// | |
| /// `subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O/O=Company LLC/C=US` | |
| /// extracts `Apple Development: Company Development (12ABCD234E)` | |
| final _certificateCommonNameExtractionPattern = RegExp(r'CN=([a-zA-Z0-9\s:\(\)]+)'); | |
| /// Given a [BuildableIOSApp], find build settings for either automatic (identity) | |
| /// or manual (provisioning profile) code-signing. | |
| /// | |
| /// If a valid provisioning profile or code-signing identity is saved in the | |
| /// config, it will use that. Otherwise, it will try to find valid development | |
| /// code-signing identities in the user's keychain, prompting a choice if multiple | |
| /// are found. | |
| /// | |
| /// Throws an error if the user cancels identity selection or if no identities | |
| /// are found. | |
| /// | |
| /// Will return `null` if the `DEVELOPMENT_TEAM` or `PROVISIONING_PROFILE` are | |
| /// already set in the Xcode project's build settings. | |
| Future<Map<String, String>?> getCodeSigningIdentityDevelopmentTeamBuildSetting({ | |
| required Map<String, String> buildSettings, | |
| required ProcessManager processManager, | |
| required Platform platform, | |
| required Logger logger, | |
| required Config config, | |
| required Terminal terminal, | |
| required FileSystem fileSystem, | |
| required FileSystemUtils fileSystemUtils, | |
| required PlistParser plistParser, | |
| }) async { | |
| // If the user already has it set in the project build settings itself, | |
| // continue with that. | |
| if (_isNotEmpty(buildSettings[_developmentTeamBuildSettingName])) { | |
| // Only log "Automatically signing..." if CODE_SIGN_STYLE is Automatic or not set. | |
| // If it's Manual, the signing is not automatic. | |
| final String? codeSignStyle = buildSettings[_codeSignStyleBuildSettingName]; | |
| if (codeSignStyle != _CodeSigningStyle.manual.label) { | |
| logger.printStatus( | |
| 'Automatically signing iOS for device deployment using specified development ' | |
| 'team in Xcode project: ${buildSettings[_developmentTeamBuildSettingName]}', | |
| ); | |
| } | |
| return null; | |
| } | |
| if (_isNotEmpty(buildSettings[_provisioningProfileBuildSettingName])) { | |
| return null; | |
| } | |
| final settings = XcodeCodeSigningSettings( | |
| config: config, | |
| logger: logger, | |
| platform: platform, | |
| processUtils: ProcessUtils(processManager: processManager, logger: logger), | |
| fileSystem: fileSystem, | |
| fileSystemUtils: fileSystemUtils, | |
| terminal: terminal, | |
| plistParser: plistParser, | |
| ); | |
| return settings._getCodeSigningBuildSettings(); | |
| } | |
| /// Returns the `DEVELOPMENT_TEAM` for automatic code-signing. | |
| /// This function should not be used for manual code-signing. | |
| /// | |
| /// This finds the `DEVELOPMENT_TEAM` from the saved `ios-signing-cert` or prompt the | |
| /// user to select a code-signing identity for automatic code-signing if | |
| /// `ios-signing-cert` is not saved or invalid. | |
| /// | |
| /// If `ios-signing-profile` (manual code-signing with a provisioning profile) | |
| /// is saved instead, returns `null`. | |
| Future<String?> getCodeSigningIdentityDevelopmentTeam({ | |
| required ProcessManager processManager, | |
| required Platform platform, | |
| required Logger logger, | |
| required Config config, | |
| required Terminal terminal, | |
| required FileSystem fileSystem, | |
| required FileSystemUtils fileSystemUtils, | |
| required PlistParser plistParser, | |
| }) async { | |
| final settings = XcodeCodeSigningSettings( | |
| config: config, | |
| logger: logger, | |
| platform: platform, | |
| processUtils: ProcessUtils(processManager: processManager, logger: logger), | |
| fileSystem: fileSystem, | |
| fileSystemUtils: fileSystemUtils, | |
| terminal: terminal, | |
| plistParser: plistParser, | |
| ); | |
| final Map<String, String>? buildSettings = await settings._getCodeSigningBuildSettings( | |
| shouldExitOnNoCerts: false, | |
| automaticCodeSignStyleOnly: true, | |
| ); | |
| return buildSettings?[_developmentTeamBuildSettingName]; | |
| } | |
| /// Returns the standard Provisioning Profiles directory, or null if home directory is unavailable. | |
| /// | |
| /// Provisioning profiles are stored in `~/Library/Developer/Xcode/UserData/Provisioning Profiles/` | |
| /// by Xcode. This function constructs the path to this directory. | |
| /// | |
| /// Returns null if the home directory cannot be determined (e.g., in some CI/CD environments). | |
| Directory? getProvisioningProfileDirectory({ | |
| required FileSystemUtils fileSystemUtils, | |
| required FileSystem fileSystem, | |
| }) { | |
| final String? homeDir = fileSystemUtils.homeDirPath; | |
| if (homeDir == null) { | |
| return null; | |
| } | |
| return fileSystem.directory( | |
| fileSystem.path.join( | |
| homeDir, | |
| 'Library', | |
| 'Developer', | |
| 'Xcode', | |
| 'UserData', | |
| 'Provisioning Profiles', | |
| ), | |
| ); | |
| } | |
| class XcodeCodeSigningSettings { | |
| XcodeCodeSigningSettings({ | |
| required Config config, | |
| required Logger logger, | |
| required Platform platform, | |
| required ProcessUtils processUtils, | |
| required FileSystem fileSystem, | |
| required FileSystemUtils fileSystemUtils, | |
| required Terminal terminal, | |
| required PlistParser plistParser, | |
| }) : _config = config, | |
| _logger = logger, | |
| _platform = platform, | |
| _processUtils = processUtils, | |
| _fileSystem = fileSystem, | |
| _fileSystemUtils = fileSystemUtils, | |
| _plistParser = plistParser, | |
| _terminal = terminal; | |
| final Config _config; | |
| final Logger _logger; | |
| final Platform _platform; | |
| final ProcessUtils _processUtils; | |
| final FileSystem _fileSystem; | |
| final FileSystemUtils _fileSystemUtils; | |
| final Terminal _terminal; | |
| final PlistParser _plistParser; | |
| /// Config key for saved code-signing identity. A code-signing identity is a | |
| /// combination of a certificate and the private key that matches the public | |
| /// key in that certificate. | |
| /// | |
| /// Example: Apple Development: My Name (ABC1234EFG) | |
| static const kConfigCodeSignCertificate = 'ios-signing-cert'; | |
| /// Config key for saved provisioning profile file path. A provisioning profile | |
| /// sets criteria for who is allowed to sign code, what apps are allowed to be | |
| /// signed, where and when those apps can be run and how those apps are entitled. | |
| /// | |
| /// Example: ~/Library/Developer/Xcode/UserData/Provisioning Profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision | |
| static const kConfigCodeSignProvisioningProfile = 'ios-signing-profile'; | |
| /// Reset both [kConfigCodeSignCertificate] and [kConfigCodeSignProvisioningProfile] | |
| /// config settings. | |
| static void resetSettings(Config config, Logger logger) { | |
| config.removeValue(kConfigCodeSignCertificate); | |
| logger.printStatus('Removing "$kConfigCodeSignCertificate" value.'); | |
| config.removeValue(kConfigCodeSignProvisioningProfile); | |
| logger.printStatus('Removing "$kConfigCodeSignProvisioningProfile" value.'); | |
| } | |
| /// Get and validate code-sign settings from the config (provisioning profile | |
| /// or identity) or prompt the user to select a code-signing identity for | |
| /// automatic code-signing. | |
| /// | |
| /// If a provisioning profile is saved, return settings with `CODE_SIGN_STYLE=Manual`, | |
| /// `DEVELOPMENT_TEAM`, and `PROVISIONING_PROFILE_SPECIFIER`. If the profile | |
| /// is no longer valid, return null. | |
| /// | |
| /// If a identity is saved, return settings with `DEVELOPMENT_TEAM`. If no | |
| /// settings are found or if the saved identity is no longer valid, prompt | |
| /// the user to select a code-signing identity for automatic code-signing. | |
| /// | |
| /// If [shouldExitOnNoCerts] is true, throw a toolExit if no code-signing | |
| /// identities are found. | |
| /// | |
| /// If [automaticCodeSignStyleOnly] is true, return null if a provisioning | |
| /// profile is saved in the config. | |
| Future<Map<String, String>?> _getCodeSigningBuildSettings({ | |
| bool shouldExitOnNoCerts = true, | |
| bool automaticCodeSignStyleOnly = false, | |
| }) async { | |
| if (!_platform.isMacOS) { | |
| _logger.printTrace('Unable to get code-sign settings on non-Mac platform.'); | |
| return null; | |
| } | |
| final bool toolsValidated = await validateCodeSignSearchTools(); | |
| if (!toolsValidated) { | |
| return null; | |
| } | |
| final List<String> validCodeSigningIdentities = await getSigningIdentities(); | |
| if (validCodeSigningIdentities.isEmpty) { | |
| if (shouldExitOnNoCerts) { | |
| _logger.printError(noCertificatesInstruction(), emphasis: true); | |
| throwToolExit( | |
| 'No development certificates available to code sign app for device deployment', | |
| ); | |
| } else { | |
| _logger.printTrace( | |
| 'No development certificates available to code sign app for device deployment', | |
| ); | |
| return null; | |
| } | |
| } | |
| final savedProfile = | |
| _config.getValue(XcodeCodeSigningSettings.kConfigCodeSignProvisioningProfile) as String?; | |
| if (savedProfile != null) { | |
| // Provisioning profile should be used for manual signing. | |
| if (automaticCodeSignStyleOnly) { | |
| return null; | |
| } | |
| final ProvisioningProfile? validatedProfile = await _validateSavedProfile( | |
| savedProfile, | |
| validCodeSigningIdentities, | |
| ); | |
| if (validatedProfile == null) { | |
| return null; | |
| } | |
| _logger.printStatus( | |
| 'Provisioning profile "${validatedProfile.name}" selected for iOS code signing', | |
| ); | |
| return <String, String>{ | |
| _codeSignStyleBuildSettingName: _CodeSigningStyle.manual.label, | |
| _developmentTeamBuildSettingName: validatedProfile.teamIdentifier, | |
| _provisioningProfileSpecifierBuildSettingName: validatedProfile.name, | |
| }; | |
| } | |
| final savedCertChoice = | |
| _config.getValue(XcodeCodeSigningSettings.kConfigCodeSignCertificate) as String?; | |
| String? identity; | |
| if (savedCertChoice != null) { | |
| identity = _validateSavedIdentity( | |
| savedCertChoice, | |
| validCodeSigningIdentities, | |
| printStatus: true, | |
| ); | |
| if (identity == null) { | |
| _logger.printError( | |
| 'Saved signing certificate "$savedCertChoice" is not a valid development ' | |
| 'certificate. To clear, use "flutter config --clear-ios-signing-settings"', | |
| ); | |
| } | |
| } | |
| if (identity == null) { | |
| identity = await _selectSigningIdentity( | |
| validCodeSigningIdentities, | |
| autoSelectSingle: true, | |
| throwOnCancel: true, | |
| ); | |
| if (identity == null) { | |
| return null; | |
| } | |
| } | |
| final String? developmentTeam = await getDevelopmentTeamFromIdentity(identity); | |
| if (developmentTeam == null) { | |
| return null; | |
| } | |
| _logger.printStatus('Developer identity "$identity" selected for iOS code signing'); | |
| return <String, String>{_developmentTeamBuildSettingName: developmentTeam}; | |
| } | |
| /// Get the [ProvisioningProfile] from the Flutter config if it's saved. | |
| Future<ProvisioningProfile?> getProvisioningProfileFromConfig( | |
| List<String> validCodeSigningIdentities, | |
| ) async { | |
| final savedProfile = | |
| _config.getValue(XcodeCodeSigningSettings.kConfigCodeSignProvisioningProfile) as String?; | |
| if (savedProfile == null) { | |
| return null; | |
| } | |
| return _validateSavedProfile(savedProfile, validCodeSigningIdentities); | |
| } | |
| /// Get the identity from the Flutter config if it's saved. | |
| Future<String?> getIdentityFromCertFromConfig(List<String> validCodeSigningIdentities) async { | |
| final savedCertChoice = | |
| _config.getValue(XcodeCodeSigningSettings.kConfigCodeSignCertificate) as String?; | |
| if (savedCertChoice == null) { | |
| return null; | |
| } | |
| return _validateSavedIdentity(savedCertChoice, validCodeSigningIdentities); | |
| } | |
| void _saveCodeSignIdentity(String identity) { | |
| _logger.printStatus('Certificate choice "$identity" saved.'); | |
| _config.setValue(kConfigCodeSignCertificate, identity); | |
| } | |
| void _saveProvisioningProfile(ProvisioningProfile profile) { | |
| _logger.printStatus('Provisioning Profile "${profile.name}" saved.'); | |
| _config.setValue(kConfigCodeSignProvisioningProfile, profile.filePath); | |
| } | |
| /// Validates that command-line tools `security` and `openssl` are available. | |
| Future<bool> validateCodeSignSearchTools({bool printError = false}) async { | |
| // If the user's environment is missing the tools needed to find and read | |
| // certificates, abandon. Tools should be pre-equipped on macOS. | |
| if (!await _processUtils.exitsHappy(const <String>['which', 'security']) || | |
| !await _processUtils.exitsHappy(const <String>['which', 'openssl'])) { | |
| if (printError) { | |
| _logger.printError('Unable to validate code-signing tools `security` and/or `openssl`.'); | |
| } else { | |
| _logger.printTrace('Unable to validate code-signing tools `security` and/or `openssl`.'); | |
| } | |
| return false; | |
| } | |
| return true; | |
| } | |
| /// Get list of code-signing identities. | |
| Future<List<String>> getSigningIdentities() async { | |
| String findIdentityStdout; | |
| try { | |
| findIdentityStdout = (await _processUtils.run(<String>[ | |
| 'security', | |
| 'find-identity', | |
| '-p', | |
| 'codesigning', | |
| '-v', | |
| ], throwOnError: true)).stdout.trim(); | |
| } on ProcessException catch (error) { | |
| _logger.printError('Unexpected failure from find-identity: $error.'); | |
| return <String>[]; | |
| } | |
| return findIdentityStdout | |
| .split('\n') | |
| .map<String?>((String outputLine) { | |
| return _securityFindIdentityDeveloperIdentityExtractionPattern | |
| .firstMatch(outputLine) | |
| ?.group(1); | |
| }) | |
| .where(_isNotEmpty) | |
| .whereType<String>() | |
| .toSet() // Unique. | |
| .toList(); | |
| } | |
| /// Validates the saved provisioning profile still exists and that there is a | |
| /// valid identity/certificate for the profile. | |
| /// | |
| /// Returns null if profile cannot be found, parsed, or validated. | |
| Future<ProvisioningProfile?> _validateSavedProfile( | |
| String savedProfilePath, | |
| List<String> validCodeSigningIdentities, | |
| ) async { | |
| final File savedProfile = _fileSystem.file(savedProfilePath); | |
| if (!savedProfile.existsSync()) { | |
| _logger.printError('Unable to find saved provisioning profile $savedProfilePath'); | |
| return null; | |
| } | |
| final ProvisioningProfile? parsedProfile = await parseProvisioningProfile(savedProfile); | |
| if (parsedProfile == null) { | |
| return null; | |
| } | |
| for (final File cert in parsedProfile.developerCertificates) { | |
| final String? identity = await _validateIdentityFromCert(cert, validCodeSigningIdentities); | |
| if (identity != null) { | |
| return parsedProfile; | |
| } | |
| } | |
| _logger.printError( | |
| 'Unable to find a valid certificate matching the provisioning profile $savedProfilePath', | |
| ); | |
| return null; | |
| } | |
| /// Decode and convert a .mobileprovision file to a .plist file and then | |
| /// parse the .plist into [ProvisioningProfile]. | |
| /// | |
| /// This is a public method that can be used by other parts of flutter_tools | |
| /// to parse provisioning profiles without pulling in certificate validation logic. | |
| Future<ProvisioningProfile?> parseProvisioningProfile(File provisioningProfileFile) async { | |
| final Directory profilesDirectory = _fileSystem.systemTempDirectory.childDirectory( | |
| 'provisioning_profiles', | |
| ); | |
| profilesDirectory.createSync(recursive: true); | |
| final File decodedProfile = profilesDirectory.childFile( | |
| 'decoded_profile_${provisioningProfileFile.basename}.plist', | |
| ); | |
| try { | |
| await _processUtils.run(<String>[ | |
| 'security', | |
| 'cms', | |
| '-D', | |
| '-i', | |
| provisioningProfileFile.path, | |
| '-o', | |
| decodedProfile.path, | |
| ], throwOnError: true); | |
| } on ProcessException catch (error) { | |
| _logger.printError('Unexpected failure from security: $error.'); | |
| return null; | |
| } | |
| if (!decodedProfile.existsSync()) { | |
| _logger.printError('Failed to decode ${provisioningProfileFile.basename}'); | |
| return null; | |
| } | |
| try { | |
| final Map<String, Object> contents = _plistParser.parseFile(decodedProfile.path); | |
| return ProvisioningProfile.fromPlist( | |
| provisioningProfileFile.path, | |
| contents, | |
| fileSystem: _fileSystem, | |
| ); | |
| } on Exception catch (e) { | |
| _logger.printError('Failed to parse provisioning profile: $e'); | |
| return null; | |
| } | |
| } | |
| /// Extract the Common Name from the [certificate] and then search for | |
| /// matching identities in [validCodeSigningIdentities]. Return the first | |
| /// matching. | |
| Future<String?> _validateIdentityFromCert( | |
| File certificate, | |
| List<String> validCodeSigningIdentities, | |
| ) async { | |
| final String? commonName = await commonNameForCertificate(certificate); | |
| if (commonName == null) { | |
| _logger.printError('Unable to extract Common Name from certificate.'); | |
| return null; | |
| } | |
| return validCodeSigningIdentities.where((String id) => id.contains(commonName)).firstOrNull; | |
| } | |
| /// Extract the Common Name from the [certificate]. | |
| Future<String?> commonNameForCertificate(File certificate) async { | |
| final String resultsStdOut; | |
| try { | |
| final RunResult results = await _processUtils.run(<String>[ | |
| 'openssl', | |
| 'x509', | |
| '-subject', | |
| '-in', | |
| certificate.path, | |
| '-inform', | |
| 'DER', | |
| ], throwOnError: true); | |
| resultsStdOut = results.stdout; | |
| } on ProcessException catch (error) { | |
| _logger.printError('Unexpected failure from openssl: $error.'); | |
| return null; | |
| } | |
| final String? commonName = _certificateCommonNameExtractionPattern | |
| .firstMatch(resultsStdOut) | |
| ?.group(1); | |
| return commonName; | |
| } | |
| /// Returns [identity] if it is found within [validCodeSigningIdentities] and | |
| /// prints a message that it was found. | |
| String? _validateSavedIdentity( | |
| String identity, | |
| List<String> validCodeSigningIdentities, { | |
| bool printStatus = false, | |
| }) { | |
| if (validCodeSigningIdentities.contains(identity)) { | |
| if (printStatus) { | |
| _logger.printStatus( | |
| 'Found saved certificate choice "$identity". To clear, use "flutter config ' | |
| '--clear-ios-signing-settings".', | |
| ); | |
| } | |
| return identity; | |
| } | |
| return null; | |
| } | |
| /// Find the certificate for the [identity] and extract the development team / | |
| /// organizational unit from the certificate. | |
| Future<String?> getDevelopmentTeamFromIdentity(String identity) async { | |
| final _CertificateTeamInfo? info = await _getCertificateTeamInfo(identity); | |
| return info?.teamId; | |
| } | |
| /// Looks up [_CertificateTeamInfo] (team ID and team name) for the given | |
| /// [identity] by running `security find-certificate` and `openssl x509 -subject`. | |
| /// | |
| /// Returns null if the certificate cannot be found or parsed. | |
| Future<_CertificateTeamInfo?> _getCertificateTeamInfo(String identity) async { | |
| final String? signingCertificateId = _securityFindIdentityCertificateCnExtractionPattern | |
| .firstMatch(identity) | |
| ?.group(1); | |
| // If `security`'s output format changes, we'd have to update the above regex. | |
| if (signingCertificateId == null) { | |
| _logger.printError('Unable to parse common name from code-signing certificate $identity'); | |
| return null; | |
| } | |
| String signingCertificateStdout; | |
| try { | |
| signingCertificateStdout = (await _processUtils.run(<String>[ | |
| 'security', | |
| 'find-certificate', | |
| '-c', | |
| signingCertificateId, | |
| '-p', | |
| ], throwOnError: true)).stdout.trim(); | |
| } on ProcessException catch (error) { | |
| _logger.printError('Unexpected error from security: $error'); | |
| return null; | |
| } | |
| final Process opensslProcess = await _processUtils.start(const <String>[ | |
| 'openssl', | |
| 'x509', | |
| '-subject', | |
| ]); | |
| await ProcessUtils.writeToStdinGuarded( | |
| stdin: opensslProcess.stdin, | |
| content: signingCertificateStdout, | |
| onError: (Object? error, _) { | |
| throw Exception('Unexpected error when writing to openssl: $error'); | |
| }, | |
| ); | |
| await opensslProcess.stdin.close(); | |
| final String opensslOutput = await utf8.decodeStream(opensslProcess.stdout); | |
| // Fire and forget discard of the stderr stream so we don't hold onto resources. | |
| // Don't care about the result. | |
| unawaited(opensslProcess.stderr.drain<String?>()); | |
| if (await opensslProcess.exitCode != 0) { | |
| _logger.printError('Failed to get subject name for code-signing certificate $identity'); | |
| return null; | |
| } | |
| final String? teamId = _certificateOrganizationalUnitExtractionPattern | |
| .firstMatch(opensslOutput) | |
| ?.group(1); | |
| if (teamId == null) { | |
| _logger.printError( | |
| 'Unable to parse development team from code-signing certificate $identity', | |
| ); | |
| return null; | |
| } | |
| final String? teamName = _certificateOrganizationExtractionPattern | |
| .firstMatch(opensslOutput) | |
| ?.group(1); | |
| return (teamId: teamId, teamName: teamName ?? ''); | |
| } | |
| /// Select code-signinging settings and save to config. | |
| /// | |
| /// Available options include automatic signing with a code-signing identity | |
| /// or manual code-signing with a provisioning profile. | |
| Future<void> selectSettings() async { | |
| // If terminal UI can't be used, just attempt with the first valid certificate | |
| // since we can't ask the user. | |
| if (!_terminal.stdinHasTerminal) { | |
| _logger.printError( | |
| 'Unable to detect stdin for the terminal. Code-signing selection requires stdin.', | |
| ); | |
| return; | |
| } | |
| _terminal.usesTerminalUi = true; | |
| final bool toolsValidated = await validateCodeSignSearchTools(printError: true); | |
| if (!toolsValidated) { | |
| return; | |
| } | |
| final savedCertChoice = | |
| _config.getValue(XcodeCodeSigningSettings.kConfigCodeSignCertificate) as String?; | |
| final savedProfile = | |
| _config.getValue(XcodeCodeSigningSettings.kConfigCodeSignProvisioningProfile) as String?; | |
| if (savedCertChoice != null || savedProfile != null) { | |
| _logger.printError( | |
| 'Code-signing settings are already set. To reset them, use "flutter config ' | |
| '--clear-ios-signing-settings"', | |
| ); | |
| return; | |
| } | |
| final _CodeSigningStyle? style = await _selectSigningStyle(); | |
| if (style == null) { | |
| _logger.printWarning(_codeSignSelectionCanceled); | |
| return; | |
| } | |
| if (style == _CodeSigningStyle.automatic) { | |
| final List<String> validCodeSigningIdentities = await getSigningIdentities(); | |
| if (validCodeSigningIdentities.isEmpty) { | |
| _logger.printError(noCertificatesInstruction(), emphasis: true); | |
| _logger.printWarning(_codeSignSelectionCanceled); | |
| return; | |
| } | |
| final String? identity = await _selectSigningIdentity(validCodeSigningIdentities); | |
| if (identity == null) { | |
| _logger.printWarning(_codeSignSelectionCanceled); | |
| return; | |
| } | |
| } else if (style == _CodeSigningStyle.manual) { | |
| final List<ProvisioningProfile> validProvisioningProfiles = await getProvisioningProfiles(); | |
| if (validProvisioningProfiles.isEmpty) { | |
| _logger.printError( | |
| 'No provisioning profiles were found. To learn how to create or download ' | |
| 'a provisioning profile, please see ' | |
| 'https://developer.apple.com/help/account/manage-provisioning-profiles/create-a-development-provisioning-profile', | |
| emphasis: true, | |
| ); | |
| _logger.printWarning(_codeSignSelectionCanceled); | |
| return; | |
| } | |
| final ProvisioningProfile? profile = await _selectProvisioningProfile( | |
| validProvisioningProfiles, | |
| ); | |
| if (profile == null) { | |
| _logger.printWarning(_codeSignSelectionCanceled); | |
| return; | |
| } | |
| _saveProvisioningProfile(profile); | |
| } | |
| } | |
| /// Prompt user to select a code-signing style (Automatic or Manual). | |
| Future<_CodeSigningStyle?> _selectSigningStyle() async { | |
| _logger.printStatus('Code Signing Styles:', emphasis: true); | |
| _logger.printStatus( | |
| ' This setting specifies the method used to acquire and locate signing ' | |
| 'assets. Choose Automatic to let Xcode automatically create and update ' | |
| 'profiles, app IDs, and certificates. Choose Manual to create and update ' | |
| 'these yourself on the developer website.', | |
| ); | |
| _logger.printStatus('[1]: ${_CodeSigningStyle.automatic.label} (recommended)'); | |
| _logger.printStatus('[2]: ${_CodeSigningStyle.manual.label}'); | |
| final String choice = await _terminal.promptForCharInput( | |
| <String>['1', '2', 'q'], | |
| prompt: 'Select a signing style (or "q" to quit)', | |
| defaultChoiceIndex: 0, // Just pressing enter chooses the first one. | |
| logger: _logger, | |
| displayAcceptedCharacters: false, | |
| ); | |
| return switch (choice) { | |
| '1' => _CodeSigningStyle.automatic, | |
| '2' => _CodeSigningStyle.manual, | |
| _ => null, | |
| }; | |
| } | |
| /// Prompts the user to select a code-signing identity from a list of [validCodeSigningIdentities]. | |
| /// Selects the first one found without prompting if there is no stdin or if | |
| /// [autoSelectSingle] is true and only one identity was found. | |
| /// | |
| /// Saves the selected identity to the config. Does not save if auto-selected. | |
| /// | |
| /// Throw an error if [throwOnCancel] is true and the user quits while | |
| /// selecting an identity. | |
| Future<String?> _selectSigningIdentity( | |
| List<String> validCodeSigningIdentities, { | |
| bool autoSelectSingle = false, | |
| bool throwOnCancel = false, | |
| }) async { | |
| if (validCodeSigningIdentities.isEmpty) { | |
| return null; | |
| } | |
| if (autoSelectSingle && validCodeSigningIdentities.length == 1) { | |
| return validCodeSigningIdentities.first; | |
| } | |
| // If terminal UI can't be used, just attempt with the first valid certificate | |
| // since we can't ask the user. | |
| if (!_terminal.stdinHasTerminal) { | |
| return validCodeSigningIdentities.first; | |
| } | |
| _terminal.usesTerminalUi = true; | |
| _logger.printStatus( | |
| '\nValid development certificates available (your choice will be saved):', | |
| emphasis: true, | |
| ); | |
| final int count = validCodeSigningIdentities.length; | |
| // Fetch team info for all identities so we can display it alongside | |
| // the identity name. | |
| final teamInfoMap = <String, _CertificateTeamInfo?>{}; | |
| for (final identity in validCodeSigningIdentities) { | |
| teamInfoMap[identity] = await _getCertificateTeamInfo(identity); | |
| } | |
| for (var i = 0; i < count; i++) { | |
| final String identity = validCodeSigningIdentities[i]; | |
| final _CertificateTeamInfo? info = teamInfoMap[identity]; | |
| if (info != null) { | |
| _logger.printStatus('[${i + 1}] $identity | Team: ${info.teamId} ${info.teamName}'); | |
| } else { | |
| _logger.printStatus('[${i + 1}] $identity'); | |
| } | |
| } | |
| final String choice = await _terminal.promptForCharInput( | |
| List<String>.generate(count, (int number) => '${number + 1}')..add('q'), | |
| prompt: 'Please select a certificate for code signing (or "q" to quit)', | |
| defaultChoiceIndex: 0, // Just pressing enter chooses the first one. | |
| logger: _logger, | |
| displayAcceptedCharacters: false, | |
| ); | |
| if (choice == 'q') { | |
| if (throwOnCancel) { | |
| throwToolExit( | |
| 'No certificate was selected. Code signing is required to build a deployable iOS app.', | |
| ); | |
| } else { | |
| return null; | |
| } | |
| } | |
| final String selectedCert = validCodeSigningIdentities[int.parse(choice) - 1]; | |
| _saveCodeSignIdentity(selectedCert); | |
| return selectedCert; | |
| } | |
| /// Get list of provisioning profiles from `~/Library/Developer/Xcode/UserData/Provisioning\ Profiles`. | |
| /// | |
| /// Only return non-Xcode-managed profiles with matching valid identities. | |
| Future<List<ProvisioningProfile>> getProvisioningProfiles({List<String>? validIdentities}) async { | |
| final Directory? profileDirectory = getProvisioningProfileDirectory( | |
| fileSystemUtils: _fileSystemUtils, | |
| fileSystem: _fileSystem, | |
| ); | |
| if (profileDirectory == null || !profileDirectory.existsSync()) { | |
| return <ProvisioningProfile>[]; | |
| } | |
| final List<String> validCodeSigningIdentities = validIdentities ?? await getSigningIdentities(); | |
| final profiles = <ProvisioningProfile>[]; | |
| for (final FileSystemEntity entity in profileDirectory.listSync()) { | |
| if (entity is! File || _fileSystem.path.extension(entity.path) != '.mobileprovision') { | |
| continue; | |
| } | |
| final ProvisioningProfile? profile = await parseProvisioningProfile(entity); | |
| // Xcode managed profiles can't be used for manual code-signing. | |
| final bool? isXcodeManaged = profile?.isXcodeManaged; | |
| if (profile == null || (isXcodeManaged != null && isXcodeManaged)) { | |
| continue; | |
| } | |
| // Only list profiles with valid identities. | |
| for (final File cert in profile.developerCertificates) { | |
| if (await _validateIdentityFromCert(cert, validCodeSigningIdentities) != null) { | |
| profiles.add(profile); | |
| break; | |
| } | |
| } | |
| } | |
| return profiles; | |
| } | |
| /// Prompt the user to select from list of [validatedProfiles]. | |
| Future<ProvisioningProfile?> _selectProvisioningProfile( | |
| List<ProvisioningProfile> validatedProfiles, | |
| ) async { | |
| if (validatedProfiles.isEmpty) { | |
| return null; | |
| } | |
| _logger.printStatus( | |
| '\nValid provisioning profiles available (your choice will be saved):', | |
| emphasis: true, | |
| ); | |
| var count = 1; | |
| for (final profile in validatedProfiles) { | |
| _logger.printStatus( | |
| '[$count]: ${profile.name} (${profile.teamIdentifier}) | Expires ${profile.expirationDate}', | |
| ); | |
| count++; | |
| } | |
| _logger.printStatus('[$count]: Other (not listed)'); | |
| final String choice = await _terminal.promptForCharInput( | |
| List<String>.generate(validatedProfiles.length + 1, (int number) => '${number + 1}') | |
| ..add('q'), | |
| prompt: 'Select a provisioning profile (or "q" to quit)', | |
| defaultChoiceIndex: 0, // Just pressing enter chooses the first one. | |
| logger: _logger, | |
| displayAcceptedCharacters: false, | |
| ); | |
| if (choice == 'q') { | |
| return null; | |
| } else if (choice == '$count') { | |
| _logger.printError( | |
| 'If you have already downloaded a provisioning profile, double-click it ' | |
| 'in Finder to install it. To learn how to create or download a ' | |
| 'provisioning profile, please see ' | |
| 'https://developer.apple.com/help/account/manage-provisioning-profiles/create-a-development-provisioning-profile', | |
| ); | |
| return null; | |
| } | |
| return validatedProfiles[int.parse(choice) - 1]; | |
| } | |
| } | |
| enum _CodeSigningStyle { | |
| automatic('Automatic'), | |
| manual('Manual'); | |
| const _CodeSigningStyle(this.label); | |
| final String label; | |
| } | |
| class ProvisioningProfile { | |
| ProvisioningProfile({ | |
| required this.filePath, | |
| required this.name, | |
| required this.uuid, | |
| required this.teamIdentifier, | |
| required this.expirationDate, | |
| required this.developerCertificates, | |
| this.isXcodeManaged, | |
| }); | |
| factory ProvisioningProfile.fromPlist( | |
| String filePath, | |
| Map<String, Object> data, { | |
| required FileSystem fileSystem, | |
| }) { | |
| final name = data['Name']?.toString(); | |
| if (name == null) { | |
| throw Exception('Unable to parse Name value for provisioning profile.'); | |
| } | |
| var identifiers = <String>[]; | |
| if (data case {'TeamIdentifier': final List<Object?> values}) { | |
| try { | |
| identifiers = List<String>.from(values); | |
| if (identifiers.isEmpty) { | |
| throw Exception('Unable to parse TeamIdentifier value for provisioning profile.'); | |
| } | |
| } on TypeError { | |
| throw Exception('Error parsing TeamIdentifier value: $values'); | |
| } | |
| } | |
| final uuid = data['UUID']?.toString(); | |
| if (uuid == null) { | |
| throw Exception('Unable to parse UUID value for provisioning profile.'); | |
| } | |
| final certificateFiles = <File>[]; | |
| if (data case {'DeveloperCertificates': final List<Object?> values}) { | |
| for (var i = 0; i < values.length; i++) { | |
| final Object? obj = values[i]; | |
| if (obj != null && obj is List<int>) { | |
| final File certFile = fileSystem.systemTempDirectory | |
| .childDirectory('provisioning_profile_certificates') | |
| .childFile('${uuid}_$i.cer'); | |
| certFile.createSync(recursive: true); | |
| certFile.writeAsBytesSync(obj); | |
| certificateFiles.add(certFile); | |
| } | |
| } | |
| } | |
| if (certificateFiles.isEmpty) { | |
| throw Exception('Unable to parse DeveloperCertificates value for provisioning profile.'); | |
| } | |
| final expirationDateString = data['ExpirationDate']?.toString(); | |
| if (expirationDateString == null) { | |
| throw Exception('Unable to parse ExpirationDate value for provisioning profile.'); | |
| } | |
| final DateTime expirationDate = DateTime.parse(expirationDateString); | |
| return ProvisioningProfile( | |
| filePath: filePath, | |
| name: name, | |
| uuid: uuid, | |
| developerCertificates: certificateFiles, | |
| isXcodeManaged: data['IsXcodeManaged'] is bool? ? data['IsXcodeManaged'] as bool? : null, | |
| expirationDate: expirationDate, | |
| teamIdentifier: identifiers.first, | |
| ); | |
| } | |
| final String filePath; | |
| final String name; | |
| final String uuid; | |
| final String teamIdentifier; | |
| final DateTime expirationDate; | |
| final List<File> developerCertificates; | |
| final bool? isXcodeManaged; | |
| } | |
| /// Returns true if s is a not empty string. | |
| bool _isNotEmpty(String? s) => s != null && s.isNotEmpty; |