From ebcf1973bfd58e73008ef842aaab7c62a0dbf9b6 Mon Sep 17 00:00:00 2001 From: Dimitar Tachev Date: Mon, 23 Mar 2020 13:47:03 +0200 Subject: [PATCH] [match] Provisioning Profiles Import and Windows Support (#16188) * feat: support match import on Windows When a binary file is opened in Text Mode on Windows, the 1a (ASCII-26 or ctrl + Z) character is assumed as End Of File and the provided certificate is not read properly. In this way, the `fastlane match import` command is always failing with "This certificate cannot be imported - the certificate contents did not match with any available on the Developer Portal" because it is searching for a partial certificate content. When reading the file in binary more, the 1a (ASCII-26 or ctrl + Z) is properly read and the whole file is properly base64 encodded leading to a working certificate import on Windows. More details: http://www.justskins.com/forums/trouble-with-binary-files-105116.html * feat: support provisioning profiles import along with the certificate and private key into the match repo * docs: update the match import docs after the Provisioning Profiles support * chore: fix linting errors * feat: support macOS provisioning profiles (.provisionprofile) in the `match import` command * docs: accept a PR suggestion Co-Authored-By: Jan Piotrowski * docs: accept a PR suggestion Co-Authored-By: Jan Piotrowski Co-authored-by: Jan Piotrowski --- .../actions/docs/sync_code_signing.md | 4 +- .../lib/fastlane_core/provisioning_profile.rb | 17 +++- match/lib/match/importer.rb | 51 ++++++---- match/spec/fixtures/test.provisionprofile | Bin 0 -> 7417 bytes match/spec/importer_spec.rb | 95 +++++++++++++----- 5 files changed, 119 insertions(+), 48 deletions(-) create mode 100644 match/spec/fixtures/test.provisionprofile diff --git a/fastlane/lib/fastlane/actions/docs/sync_code_signing.md b/fastlane/lib/fastlane/actions/docs/sync_code_signing.md index 2916f41a5dd..596bf928f49 100644 --- a/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +++ b/fastlane/lib/fastlane/actions/docs/sync_code_signing.md @@ -465,13 +465,13 @@ You'll be asked for the new password on all your machines on the next run. ### Import -To import and encrypt a certificate (`.cer`) and the private key (`.p12`) into the _match_ repo run: +To import and encrypt a certificate (`.cer`), the private key (`.p12`) and the provisioning profiles (`.mobileprovision` or `.provisionprofile`) into the _match_ repo run: ```no-highlight fastlane match import ``` -You'll be prompted for the certificate (`.cer`) and the private key (`.p12`) paths. _match_ will first validate the certificate (`.cer`) against the Developer Portal before importing the certificate (`.cer`) and the private key (`.p12`). +You'll be prompted for the certificate (`.cer`), the private key (`.p12`) and the provisioning profiles (`.mobileprovision` or `.provisionprofile`) paths. _match_ will first validate the certificate (`.cer`) against the Developer Portal before importing the certificate, the private key and the provisioning profiles into the specified _match_ repository. ### Manual Decrypt diff --git a/fastlane_core/lib/fastlane_core/provisioning_profile.rb b/fastlane_core/lib/fastlane_core/provisioning_profile.rb index 41c3e47fdc0..a67898ab545 100644 --- a/fastlane_core/lib/fastlane_core/provisioning_profile.rb +++ b/fastlane_core/lib/fastlane_core/provisioning_profile.rb @@ -44,16 +44,29 @@ def name(path, keychain_path = nil) parse(path, keychain_path).fetch("Name") end + def bundle_id(path, keychain_path = nil) + profile = parse(path, keychain_path) + app_id_prefix = profile["ApplicationIdentifierPrefix"].first + bundle_id = profile["Entitlements"]["application-identifier"].gsub("#{app_id_prefix}.", "") + bundle_id + rescue + UI.error("Unable to extract the Bundle Id from the provided provisioning profile '#{path}'.") + end + def mac?(path, keychain_path = nil) parse(path, keychain_path).fetch("Platform", []).include?('OSX') end def profile_filename(path, keychain_path = nil) basename = uuid(path, keychain_path) + basename + profile_extension(path, keychain_path) + end + + def profile_extension(path, keychain_path = nil) if mac?(path, keychain_path) - basename + ".provisionprofile" + ".provisionprofile" else - basename + ".mobileprovision" + ".mobileprovision" end end diff --git a/match/lib/match/importer.rb b/match/lib/match/importer.rb index 9b06d5b8c41..49a94e13527 100644 --- a/match/lib/match/importer.rb +++ b/match/lib/match/importer.rb @@ -2,23 +2,16 @@ require_relative 'encryption' require_relative 'storage' require_relative 'module' +require 'fastlane_core/provisioning_profile' require 'fileutils' module Match class Importer - def import_cert(params, cert_path: nil, p12_path: nil) - # Get and verify cert and p12 path - cert_path ||= UI.input("Certificate (.cer) path:") - p12_path ||= UI.input("Private key (.p12) path:") - - cert_path = File.absolute_path(cert_path) - p12_path = File.absolute_path(p12_path) - - UI.user_error!("Certificate does not exist at path: #{cert_path}") unless File.exist?(cert_path) - UI.user_error!("Private key does not exist at path: #{p12_path}") unless File.exist?(p12_path) - - # Base64 encode contents to find match from API to find a cert ID - cert_contents_base_64 = Base64.strict_encode64(File.open(cert_path).read) + def import_cert(params, cert_path: nil, p12_path: nil, profile_path: nil) + # Get and verify cert, p12 and profiles path + cert_path = ensure_valid_file_path(cert_path, "Certificate", ".cer") + p12_path = ensure_valid_file_path(p12_path, "Private key", ".p12") + profile_path = ensure_valid_file_path(profile_path, "Provisioning profile", ".mobileprovision or .provisionprofile", optional: true) # Storage storage = Storage.for_mode(params[:storage_mode], { @@ -61,13 +54,16 @@ def import_cert(params, cert_path: nil, p12_path: nil) UI.user_error!("Cert type '#{cert_type}' is not supported") end - output_dir = File.join(storage.prefixed_working_directory, "certs", cert_type.to_s) + output_dir_certs = File.join(storage.prefixed_working_directory, "certs", cert_type.to_s) + output_dir_profiles = File.join(storage.prefixed_working_directory, "profiles", cert_type.to_s) # Need to get the cert id by comparing base64 encoded cert content with certificate content from the API responses Spaceship::Portal.login(params[:username]) Spaceship::Portal.select_team(team_id: params[:team_id], team_name: params[:team_name]) certs = Spaceship::ConnectAPI::Certificate.all(filter: { certificateType: certificate_type }) + # Base64 encode contents to find match from API to find a cert ID + cert_contents_base_64 = Base64.strict_encode64(File.binread(cert_path)) matching_cert = certs.find do |cert| cert.certificate_content == cert_contents_base_64 end @@ -75,18 +71,37 @@ def import_cert(params, cert_path: nil, p12_path: nil) UI.user_error!("This certificate cannot be imported - the certificate contents did not match with any available on the Developer Portal") if matching_cert.nil? # Make dir if doesn't exist - FileUtils.mkdir_p(output_dir) - dest_cert_path = File.join(output_dir, "#{matching_cert.id}.cer") - dest_p12_path = File.join(output_dir, "#{matching_cert.id}.p12") + FileUtils.mkdir_p(output_dir_certs) + dest_cert_path = File.join(output_dir_certs, "#{matching_cert.id}.cer") + dest_p12_path = File.join(output_dir_certs, "#{matching_cert.id}.p12") + + files_to_commit = [dest_cert_path, dest_p12_path] # Copy files IO.copy_stream(cert_path, dest_cert_path) IO.copy_stream(p12_path, dest_p12_path) - files_to_commit = [dest_cert_path, dest_p12_path] + unless profile_path.nil? + FileUtils.mkdir_p(output_dir_profiles) + bundle_id = FastlaneCore::ProvisioningProfile.bundle_id(profile_path) + profile_extension = FastlaneCore::ProvisioningProfile.profile_extension(profile_path) + dest_profile_path = File.join(output_dir_profiles, "#{cert_type.to_s.capitalize}_#{bundle_id}#{profile_extension}") + files_to_commit.push(dest_profile_path) + IO.copy_stream(profile_path, dest_profile_path) + end # Encrypt and commit encryption.encrypt_files if encryption storage.save_changes!(files_to_commit: files_to_commit) end + + def ensure_valid_file_path(file_path, file_description, file_extension, optional: false) + optional_file_message = optional ? " or leave empty to skip this file" : "" + file_path ||= UI.input("#{file_description} (#{file_extension}) path#{optional_file_message}:") + + file_path = File.absolute_path(file_path) unless file_path == "" + file_path = File.exist?(file_path) ? file_path : nil + UI.user_error!("#{file_description} does not exist at path: #{file_path}") unless !file_path.nil? || optional + file_path + end end end diff --git a/match/spec/fixtures/test.provisionprofile b/match/spec/fixtures/test.provisionprofile new file mode 100644 index 0000000000000000000000000000000000000000..1a35989cbe2f76cceb322677cd26c331c56f5c9e GIT binary patch literal 7417 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 literal 0 HcmV?d00001 diff --git a/match/spec/importer_spec.rb b/match/spec/importer_spec.rb index 410fac28211..ab82685c885 100644 --- a/match/spec/importer_spec.rb +++ b/match/spec/importer_spec.rb @@ -1,13 +1,28 @@ describe Match do describe Match::Runner do + let(:fake_storage) { "fake_storage" } let(:keychain) { 'login.keychain' } let(:mock_cert) { double } let(:cert_path) { "./match/spec/fixtures/test.cer" } let(:p12_path) { "./match/spec/fixtures/test.p12" } + let(:ios_profile_path) { "./match/spec/fixtures/test.mobileprovision" } + let(:osx_profile_path) { "./match/spec/fixtures/test.provisionprofile" } + let(:values) { test_values } + let(:config) { FastlaneCore::Configuration.create(Match::Options.available_options, values) } + + def test_values + { + app_identifier: "tools.fastlane.app", + type: "appstore", + git_url: "https://github.com/fastlane/fastlane/tree/master/certificates", + shallow_clone: true, + username: "flapple@something.com" + } + end before do allow(mock_cert).to receive(:id).and_return("123456789") - allow(mock_cert).to receive(:certificate_content).and_return(Base64.strict_encode64(File.open(cert_path).read)) + allow(mock_cert).to receive(:certificate_content).and_return(Base64.strict_encode64(File.binread(cert_path))) allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with('MATCH_KEYCHAIN_NAME').and_return(keychain) @@ -19,22 +34,63 @@ ENV.delete('FASTLANE_TEAM_NAME') end - it "imports a .cert and .p12 into the match repo", requires_security: true do - git_url = "https://github.com/fastlane/fastlane/tree/master/certificates" - values = { - app_identifier: "tools.fastlane.app", - type: "appstore", - git_url: git_url, - shallow_clone: true, - username: "flapple@something.com" - } + it "imports a .cert, .p12 and .mobileprovision (iOS provision) into the match repo" do + repo_dir = Dir.mktmpdir + setup_fake_storage(repo_dir) + + expect(Spaceship::Portal).to receive(:login) + expect(Spaceship::Portal).to receive(:select_team) + expect(Spaceship::ConnectAPI::Certificate).to receive(:all).and_return([mock_cert]) + expect(fake_storage).to receive(:save_changes!).with( + files_to_commit: [ + File.join(repo_dir, "certs", "distribution", "#{mock_cert.id}.cer"), + File.join(repo_dir, "certs", "distribution", "#{mock_cert.id}.p12"), + File.join(repo_dir, "profiles", "distribution", "Distribution_tools.fastlane.app.mobileprovision") + ] + ) + + Match::Importer.new.import_cert(config, cert_path: cert_path, p12_path: p12_path, profile_path: ios_profile_path) + end - config = FastlaneCore::Configuration.create(Match::Options.available_options, values) + it "imports a .cert, .p12 and .provisionprofile (osx provision) into the match repo" do repo_dir = Dir.mktmpdir + setup_fake_storage(repo_dir) - fake_storage = "fake_storage" + expect(Spaceship::Portal).to receive(:login) + expect(Spaceship::Portal).to receive(:select_team) + expect(Spaceship::ConnectAPI::Certificate).to receive(:all).and_return([mock_cert]) + expect(fake_storage).to receive(:save_changes!).with( + files_to_commit: [ + File.join(repo_dir, "certs", "distribution", "#{mock_cert.id}.cer"), + File.join(repo_dir, "certs", "distribution", "#{mock_cert.id}.p12"), + File.join(repo_dir, "profiles", "distribution", "Distribution_tools.fastlane.app.provisionprofile") + ] + ) + + Match::Importer.new.import_cert(config, cert_path: cert_path, p12_path: p12_path, profile_path: osx_profile_path) + end + + it "imports a .cert and .p12 without profile into the match repo (backwards compatibility)" do + repo_dir = Dir.mktmpdir + setup_fake_storage(repo_dir) + + expect(UI).to receive(:input).and_return("") + expect(Spaceship::Portal).to receive(:login) + expect(Spaceship::Portal).to receive(:select_team) + expect(Spaceship::ConnectAPI::Certificate).to receive(:all).and_return([mock_cert]) + expect(fake_storage).to receive(:save_changes!).with( + files_to_commit: [ + File.join(repo_dir, "certs", "distribution", "#{mock_cert.id}.cer"), + File.join(repo_dir, "certs", "distribution", "#{mock_cert.id}.p12") + ] + ) + + Match::Importer.new.import_cert(config, cert_path: cert_path, p12_path: p12_path) + end + + def setup_fake_storage(repo_dir) expect(Match::Storage::GitStorage).to receive(:configure).with( - git_url: git_url, + git_url: values[:git_url], shallow_clone: true, skip_docs: false, git_branch: "master", @@ -55,19 +111,6 @@ expect(fake_storage).to receive(:download).and_return(nil) allow(fake_storage).to receive(:working_directory).and_return(repo_dir) allow(fake_storage).to receive(:prefixed_working_directory).and_return(repo_dir) - - expect(Spaceship::Portal).to receive(:login) - expect(Spaceship::Portal).to receive(:select_team) - expect(Spaceship::ConnectAPI::Certificate).to receive(:all).and_return([mock_cert]) - - expect(fake_storage).to receive(:save_changes!).with( - files_to_commit: [ - File.join(repo_dir, "certs", "distribution", "#{mock_cert.id}.cer"), - File.join(repo_dir, "certs", "distribution", "#{mock_cert.id}.p12") - ] - ) - - Match::Importer.new.import_cert(config, cert_path: cert_path, p12_path: p12_path) end end end