# 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