76 lines
3.8 KiB
Ruby
76 lines
3.8 KiB
Ruby
# One app-scoped bootstrap for the already-regenerated Sybil distribution profile.
|
|
# Invoked inside the existing CI job so MATCH_PASSWORD never leaves that job.
|
|
require 'match'
|
|
require 'match/encryption'
|
|
require 'cfpropertylist'
|
|
require 'digest'
|
|
require 'fileutils'
|
|
require 'open3'
|
|
require 'tmpdir'
|
|
|
|
module SybilPushProfile
|
|
PROFILE = File.expand_path('~/.config/sybil-release-signing/Sybil_AppStore_CI.mobileprovision')
|
|
REPOSITORY = 'https://code.buzzert.dev/buzzert/fastlane-match.git'
|
|
TARGET = 'profiles/appstore/AppStore_net.buzzert.sybil2.mobileprovision'
|
|
UUID = '6ca8c49d-ca89-4b9f-8ae8-a0e33fafd19c'
|
|
CERTIFICATE = '6B74B268C4761720FB2051D01D8BB3E47B55D9F5'
|
|
|
|
def self.run
|
|
return unless File.exist?(PROFILE)
|
|
stage = 'profile validation'
|
|
file = File.lstat(PROFILE)
|
|
parent = File.stat(File.dirname(PROFILE))
|
|
raise unless file.file? && !file.symlink? && file.uid == Process.uid && (file.mode & 0o077).zero?
|
|
raise unless parent.uid == Process.uid && (parent.mode & 0o077).zero?
|
|
xml, _, status = Open3.capture3('/usr/bin/security', 'cms', '-D', '-i', PROFILE)
|
|
raise unless status.success?
|
|
profile = CFPropertyList.native_types(CFPropertyList::List.new(data: xml).value)
|
|
entitlements = profile.fetch('Entitlements')
|
|
raise unless profile['UUID'] == UUID && profile['Name'] == 'Sybil AppStore CI'
|
|
raise unless profile['TeamIdentifier'] == ['DQQH5H6GBD']
|
|
raise unless entitlements['application-identifier'] == 'DQQH5H6GBD.net.buzzert.sybil2'
|
|
raise unless entitlements['aps-environment'] == 'production' && entitlements['get-task-allow'] == false
|
|
raise unless profile.fetch('DeveloperCertificates').any? { |c| Digest::SHA1.hexdigest(c).upcase == CERTIFICATE }
|
|
raise unless profile.fetch('ExpirationDate') > Time.now
|
|
plaintext = File.binread(PROFILE)
|
|
password = ENV.fetch('MATCH_PASSWORD')
|
|
authorization = ENV.fetch('MATCH_GIT_BASIC_AUTHORIZATION')
|
|
raise if password.empty? || authorization.empty? || authorization.match?(/[\r\n]/)
|
|
git_env = { 'GIT_TERMINAL_PROMPT' => '0', 'GIT_CONFIG_COUNT' => '1',
|
|
'GIT_CONFIG_KEY_0' => 'http.https://code.buzzert.dev/.extraheader',
|
|
'GIT_CONFIG_VALUE_0' => "Authorization: Basic #{authorization}" }
|
|
git = lambda do |*args|
|
|
out, _, result = Open3.capture3(git_env, 'git', *args)
|
|
raise unless result.success?
|
|
out
|
|
end
|
|
stage = 'encrypted profile refresh'
|
|
Dir.mktmpdir('sybil-profile-') do |temp|
|
|
File.chmod(0o700, temp)
|
|
repo = File.join(temp, 'match')
|
|
git.call('clone', '--quiet', '--single-branch', REPOSITORY, repo)
|
|
target = File.join(repo, TARGET)
|
|
raise unless File.file?(target) && !File.symlink?(target)
|
|
crypto = Match::Encryption::MatchDataEncryption.new
|
|
current = crypto.decrypt(base64encoded_encrypted: File.read(target), password: password)
|
|
if current == plaintext
|
|
puts 'Sybil production Push profile is already current in match.'
|
|
return
|
|
end
|
|
encrypted = crypto.encrypt(data: plaintext, password: password, version: 2)
|
|
raise unless crypto.decrypt(base64encoded_encrypted: encrypted, password: password) == plaintext
|
|
File.write(target, encrypted)
|
|
changed = git.call('-C', repo, 'diff', '--name-only').lines.map(&:strip)
|
|
raise unless changed == [TARGET]
|
|
git.call('-C', repo, 'add', '--', TARGET)
|
|
git.call('-C', repo, '-c', 'user.name=Sybil CI', '-c', 'user.email=james@magahern.com',
|
|
'commit', '--quiet', '-m', 'signing: refresh Sybil production Push profile')
|
|
stage = 'normal signing-repository push'
|
|
git.call('-C', repo, 'push', 'origin', 'HEAD')
|
|
puts 'Updated only the encrypted Sybil production Push profile in match.'
|
|
end
|
|
rescue StandardError
|
|
abort "Sybil signing profile refresh failed at #{stage}; credential details withheld."
|
|
end
|
|
end
|