From 279cd6c94a813db9d39cb24e8006914a11e7846f Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 19 May 2026 18:32:46 -0400 Subject: [PATCH] Robust ASC key normalization with diagnostics for TestFlight deploy Replace fragile BEGIN/base64 branch with multi-strategy key parser that handles raw PEM, escaped newlines, base64-encoded PEM, mangled line wrapping, CR/LF issues, and double-encoding. Validates key with OpenSSL::PKey.read before passing to Fastlane via key_filepath (temp file) instead of key_content to bypass Fastlane's own parsing. Prints safe diagnostics (no key material) if all strategies fail. --- mobile/fastlane/Fastfile | 91 ++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/mobile/fastlane/Fastfile b/mobile/fastlane/Fastfile index e3c3bab..f8f72a9 100644 --- a/mobile/fastlane/Fastfile +++ b/mobile/fastlane/Fastfile @@ -1,8 +1,9 @@ require 'openssl' +require 'base64' +require 'tempfile' -# Fastlane 2.234.0 uses OpenSSL::PKey::EC.new which rejects PKCS#8 keys on -# OpenSSL 3.x ("invalid curve name"). Prepend a wrapper that falls back to -# OpenSSL::PKey.read, which handles both PKCS#8 and SEC1 formats. +# OpenSSL 3.x rejects PKCS#8 keys via EC.new ("invalid curve name"). +# Prepend a wrapper that falls back to PKey.read for both formats. module OpenSSLECNewFix def new(arg = nil, *rest) super @@ -13,6 +14,73 @@ module OpenSSLECNewFix end OpenSSL::PKey::EC.singleton_class.prepend(OpenSSLECNewFix) +def normalize_p8_key(raw) + candidates = [] + + cleaned = raw.gsub("\r", "") + with_newlines = cleaned.gsub('\n', "\n") + candidates << ["raw (newlines normalized)", with_newlines] + + if with_newlines.include?("BEGIN") + b64_body = with_newlines.gsub(/-----(?:BEGIN|END)[^-]+-----/, '').gsub(/\s+/, '') + rewrapped = "-----BEGIN PRIVATE KEY-----\n#{b64_body.scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n" + candidates << ["rewrapped PEM", rewrapped] + end + + begin + decoded = Base64.decode64(cleaned.strip) + if decoded.include?("BEGIN") + decoded_clean = decoded.gsub("\r", "").gsub('\n', "\n") + candidates << ["base64→PEM", decoded_clean] + b64_body = decoded_clean.gsub(/-----(?:BEGIN|END)[^-]+-----/, '').gsub(/\s+/, '') + rewrapped = "-----BEGIN PRIVATE KEY-----\n#{b64_body.scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n" + candidates << ["base64→PEM rewrapped", rewrapped] + elsif decoded.length.between?(32, 256) + candidates << ["base64→DER", decoded] + end + rescue StandardError + # not valid base64 + end + + begin + double = Base64.decode64(Base64.decode64(cleaned.strip).strip) + if double.include?("BEGIN") + candidates << ["double-base64→PEM", double.gsub("\r", "")] + end + rescue StandardError + # not double-encoded + end + + candidates.each do |name, content| + begin + OpenSSL::PKey.read(content) + UI.success("ASC key parsed with strategy: #{name}") + return content + rescue StandardError => e + UI.message("Strategy '#{name}' failed: #{e.class} — #{e.message}") + end + end + + UI.error("=== ASC KEY DIAGNOSTIC (no key material shown) ===") + UI.error("Raw byte length: #{raw.bytesize}") + UI.error("Starts with BEGIN: #{raw.strip.start_with?('-----BEGIN')}") + UI.error("Ends with -----: #{raw.strip.end_with?('-----')}") + UI.error("Has real newlines: #{raw.include?("\n")}") + UI.error("Has literal backslash-n: #{raw.include?('\\n')}") + UI.error("Has carriage returns: #{raw.include?("\r")}") + UI.error("Printable ASCII ratio: #{(raw.count(' -~').to_f / raw.bytesize * 100).round(1)}%") + UI.error("Header (first 27 chars): #{raw[0..26]}") + begin + d = Base64.decode64(raw.strip) + UI.error("After base64 decode — length: #{d.bytesize}, header: #{d[0..26]}") + rescue StandardError + UI.error("base64 decode raised an exception") + end + UI.error("=== END DIAGNOSTIC ===") + + raise "Could not parse ASC_KEY_CONTENT in any known format. See diagnostics above." +end + default_platform(:ios) platform :ios do @@ -20,22 +88,19 @@ platform :ios do lane :beta do setup_ci - raw = ENV["ASC_KEY_CONTENT"] - if raw.include?("BEGIN") - key_content = raw.gsub('\n', "\n") - is_base64 = false - else - key_content = raw - is_base64 = true - end + key_pem = normalize_p8_key(ENV["ASC_KEY_CONTENT"]) + + key_path = File.join(Dir.tmpdir, "asc_api_key.p8") + File.write(key_path, key_pem) app_store_connect_api_key( key_id: ENV["ASC_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"], - key_content: key_content, - is_key_content_base64: is_base64 + key_filepath: key_path ) + File.delete(key_path) if File.exist?(key_path) + match(type: "appstore", readonly: true) increment_build_number(