Skip to content

Commit

Permalink
[match] Provisioning Profiles Import and Windows Support (#16188)
Browse files Browse the repository at this point in the history
* 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 <piotrowski+github@gmail.com>

* docs: accept a PR suggestion

Co-Authored-By: Jan Piotrowski <piotrowski+github@gmail.com>

Co-authored-by: Jan Piotrowski <piotrowski+github@gmail.com>
  • Loading branch information
Dimitar Tachev and janpio committed Mar 23, 2020
1 parent 63be9d3 commit ebcf197
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 48 deletions.
4 changes: 2 additions & 2 deletions fastlane/lib/fastlane/actions/docs/sync_code_signing.md
Expand Up @@ -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

Expand Down
17 changes: 15 additions & 2 deletions fastlane_core/lib/fastlane_core/provisioning_profile.rb
Expand Up @@ -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

Expand Down
51 changes: 33 additions & 18 deletions match/lib/match/importer.rb
Expand Up @@ -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], {
Expand Down Expand Up @@ -61,32 +54,54 @@ 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

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
Binary file added match/spec/fixtures/test.provisionprofile
Binary file not shown.
95 changes: 69 additions & 26 deletions 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)
Expand All @@ -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",
Expand All @@ -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

0 comments on commit ebcf197

Please sign in to comment.