Skip to content

如何使用 fastlane 读取 iOS 证书信息

轩辕十四
Published date:

在 iOS 开发中,证书管理一直是一个重要且复杂的话题。随着 fastlane 的更新,我们处理证书信息的方式也在不断演进。今天,我想分享我在使用 fastlane 读取和加密 iOS 证书信息时的经历,以及如何应对 fastlane 更新带来的变化。

Table of contents

Open Table of contents

初始方法:直接读取证书文件

最初,我创建了一个自定义的 fastlane Action 来直接读取 .cer 文件,这里使用的是 OpenSSL 进行解密:

class ReadSpecificFileAction < Action
  def self.decrypt_specific_file(path: nil, password: nil, hash_algorithm: "MD5")
    stored_data = Base64.decode64(File.read(path))
    salt = stored_data[8..15]
    data_to_decrypt = stored_data[16..-1]

    decipher = ::OpenSSL::Cipher.new('AES-256-CBC')
    decipher.decrypt
    decipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm)

    decrypted_data = decipher.update(data_to_decrypt) + decipher.final

    File.binwrite(path, decrypted_data)
  rescue => error
    fallback_hash_algorithm = "SHA256"
    if hash_algorithm != fallback_hash_algorithm
      decrypt_specific_file(path: path, password: password, hash_algorithm: fallback_hash_algorithm)
    else
      UI.error(error.to_s)
      UI.crash!("Error decrypting '#{path}'")
    end
  end
end

这种方法简单直接,但随着 fastlane 的更新,fastlane 应该是修改了加密解密的逻辑,上面的方案已经无法使用,我们需要寻找新的实现方案。

寻找新的实现方案

Step 1: 查阅 fastlane match 插件的官方文档

在阅读 match 文档的过程中我看到一个手动解密的信息,原文如下:

Manual Decrypt

If you want to manually decrypt or encrypt a file, you can use the companion script match_file:

match_file encrypt "<fileYouWantToEncryptPath>" ["<encryptedFilePath>"]

match_file decrypt "<fileYouWantToDecryptPath>" ["<decryptedFilePath>"]

The password will be asked interactively.

Note: You may need to swap double quotes " for single quotes ' if your match password contains an exclamation mark !.

Step 2: 查看 match_file 命令的源码实现

既然我们知道了这里是使用 match_file 命令进行手动解密的,那么我们可以去 github 看一下他是如何实现的。match_file

begin
  Match::Encryption::MatchFileEncryption.new.send(method_name, file_path: input_file, password: password, output_path: output_file)
rescue => e
  puts("ERROR #{method_name}ing. [#{e}]. Check your password")
  usage
end

这是脚本的核心部分,使用 Match::Encryption::MatchFileEncryption 类来执行加密或解密操作。如果发生错误(例如密码错误),会捕获异常并显示错误信息。

我们来看一下这个方法的参数都是什么:

method_name

file_path: input_file

password: password

output_path: output_file

新方法:基于 match_file 的实现

我创建了一个新的 Action,名为 MatchFileEncryptionAction

class MatchFileEncryptionAction < Action
  def self.run(params)
    method_name = params[:method]
    input_file = params[:input_file]
    output_file = params[:output_file] || input_file
    password = params[:password]

    begin
      Match::Encryption::MatchFileEncryption.new.send(method_name, file_path: input_file, password: password, output_path: output_file)
    rescue => e
      UI.user_error!("ERROR #{method_name}ing. [#{e}]. Check your password")
    end
  end
end

可以看到,这个方法只是做了加密解密相关的事情,没有对证书信息进行读取,我将对证书的解析封装到了另一个 ReadCertificateInfoAction 中。

class ReadCertificateInfoAction < Action
  def self.run(params)
    cert_path = params[:cert_path]

    UI.user_error!("Certificate file not found at path: #{cert_path}") unless File.exist?(cert_path)

    begin
      cert = OpenSSL::X509::Certificate.new(File.read(cert_path))

      subject = parse_subject(cert.subject)

      info = {
        subject: cert.subject.to_s,
        issuer: cert.issuer.to_s,
        serial: cert.serial.to_s,
        not_before: cert.not_before,
        not_after: cert.not_after,
        public_key: cert.public_key.to_pem,
        team_id: extract_team_id(subject),
        description: extract_description(subject)
      }

      # Extract Subject Alternative Names if present
      ext = cert.extensions.find { |e| e.oid == 'subjectAltName' }
      info[:subject_alt_names] = ext.value if ext

      return info
    rescue OpenSSL::X509::CertificateError => e
      UI.user_error!("Failed to read certificate: #{e.message}")
    rescue => e
      UI.user_error!("An error occurred: #{e.message}")
    end
  end

  def self.parse_subject(subject)
    subject.to_a.each_with_object({}) do |field, hash|
      hash[field[0]] = field[1]
    end
  end

  def self.extract_team_id(subject)
    subject['OU'] || "Not found"
  end

  def self.extract_description(subject)
    cn = subject['CN'] || ""
    o = subject['O'] || ""
    "#{cn}#{o.empty? ? '' : " (#{o})"}"
  end
end

至此,我们已经能够在新版 fastlane 中进行对证书的解密和对证书的解析。

最后我们来看一下,iOS 证书里面我解析出了哪些信息

Previous
Ruby 魔法:用 Monkey Patching 解决 Fastlane Gym 的清理困境
Next
常用 Android Debug Bridge (ADB) 命令指南