133 lines
4.9 KiB
Swift
133 lines
4.9 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
enum PluginTrustValidatorError: LocalizedError, Equatable {
|
|
case signatureCheckFailed(String)
|
|
case teamIdentifierUnavailable(URL)
|
|
case teamIdentifierMismatch(expected: String, actual: String)
|
|
case hostTeamIdentifierUnavailable
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case let .signatureCheckFailed(reason):
|
|
return AppL10n.pluginsFormat("plugin.error.trust.signatureCheckFailedFormat", defaultValue: "插件签名校验失败:%@", reason)
|
|
case let .teamIdentifierUnavailable(url):
|
|
return AppL10n.pluginsFormat("plugin.error.trust.teamUnavailableFormat", defaultValue: "无法读取插件签名团队:%@", url.path)
|
|
case let .teamIdentifierMismatch(expected, actual):
|
|
return AppL10n.pluginsFormat(
|
|
"plugin.error.trust.teamMismatchFormat",
|
|
defaultValue: "插件签名团队不匹配,期望 %@,实际 %@。",
|
|
expected,
|
|
actual
|
|
)
|
|
case .hostTeamIdentifierUnavailable:
|
|
return AppL10n.plugins("plugin.error.trust.hostTeamUnavailable", defaultValue: "无法确定宿主签名团队,已拒绝加载插件。")
|
|
}
|
|
}
|
|
}
|
|
|
|
protocol PluginTrustValidating {
|
|
func validatePluginBundle(at bundleURL: URL) throws
|
|
}
|
|
|
|
struct SameTeamPluginTrustValidator: PluginTrustValidating {
|
|
private let hostTeamIdentifier: String?
|
|
private let codeSignatureInfoProvider: CodeSignatureInfoProviding
|
|
private let allowsUntrustedHostTeam: Bool
|
|
|
|
init(
|
|
hostBundleURL: URL = Bundle.main.bundleURL,
|
|
codeSignatureInfoProvider: CodeSignatureInfoProviding = SecurityCodeSignatureInfoProvider(),
|
|
allowsUntrustedHostTeam: Bool = SameTeamPluginTrustValidator.defaultAllowsUntrustedHostTeam
|
|
) {
|
|
self.codeSignatureInfoProvider = codeSignatureInfoProvider
|
|
self.hostTeamIdentifier = try? codeSignatureInfoProvider.teamIdentifier(for: hostBundleURL)
|
|
self.allowsUntrustedHostTeam = allowsUntrustedHostTeam
|
|
}
|
|
|
|
// DEBUG host builds are often ad-hoc/unsigned and report no team identifier;
|
|
// default to permissive there. Release builds default to enforcing same-team.
|
|
static var defaultAllowsUntrustedHostTeam: Bool {
|
|
#if DEBUG
|
|
true
|
|
#else
|
|
false
|
|
#endif
|
|
}
|
|
|
|
func validatePluginBundle(at bundleURL: URL) throws {
|
|
try codeSignatureInfoProvider.validateCodeSignature(at: bundleURL)
|
|
|
|
guard let expectedTeamID = hostTeamIdentifier, !expectedTeamID.isEmpty else {
|
|
// Fail closed: an unreadable host team must not silently bypass the
|
|
// same-team check (which would otherwise accept any validly-signed bundle).
|
|
if allowsUntrustedHostTeam {
|
|
return
|
|
}
|
|
throw PluginTrustValidatorError.hostTeamIdentifierUnavailable
|
|
}
|
|
|
|
guard let actualTeamID = try codeSignatureInfoProvider.teamIdentifier(for: bundleURL), !actualTeamID.isEmpty else {
|
|
throw PluginTrustValidatorError.teamIdentifierUnavailable(bundleURL)
|
|
}
|
|
|
|
guard actualTeamID == expectedTeamID else {
|
|
throw PluginTrustValidatorError.teamIdentifierMismatch(
|
|
expected: expectedTeamID,
|
|
actual: actualTeamID
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
protocol CodeSignatureInfoProviding {
|
|
func validateCodeSignature(at url: URL) throws
|
|
func teamIdentifier(for url: URL) throws -> String?
|
|
}
|
|
|
|
struct SecurityCodeSignatureInfoProvider: CodeSignatureInfoProviding {
|
|
func validateCodeSignature(at url: URL) throws {
|
|
let code = try staticCode(for: url)
|
|
let status = SecStaticCodeCheckValidity(code, SecCSFlags(), nil)
|
|
|
|
guard status == errSecSuccess else {
|
|
throw PluginTrustValidatorError.signatureCheckFailed(secErrorMessage(status))
|
|
}
|
|
}
|
|
|
|
func teamIdentifier(for url: URL) throws -> String? {
|
|
let code = try staticCode(for: url)
|
|
var info: CFDictionary?
|
|
let status = SecCodeCopySigningInformation(
|
|
code,
|
|
SecCSFlags(rawValue: kSecCSSigningInformation),
|
|
&info
|
|
)
|
|
|
|
guard status == errSecSuccess else {
|
|
throw PluginTrustValidatorError.signatureCheckFailed(secErrorMessage(status))
|
|
}
|
|
|
|
guard let dictionary = info as? [String: Any] else {
|
|
return nil
|
|
}
|
|
|
|
return dictionary[kSecCodeInfoTeamIdentifier as String] as? String
|
|
}
|
|
|
|
private func staticCode(for url: URL) throws -> SecStaticCode {
|
|
var code: SecStaticCode?
|
|
let status = SecStaticCodeCreateWithPath(url as CFURL, SecCSFlags(), &code)
|
|
|
|
guard status == errSecSuccess, let code else {
|
|
throw PluginTrustValidatorError.signatureCheckFailed(secErrorMessage(status))
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
private func secErrorMessage(_ status: OSStatus) -> String {
|
|
SecCopyErrorMessageString(status, nil) as String? ?? "OSStatus \(status)"
|
|
}
|
|
}
|