Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

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)"
}
}