Files
firewall/macos/App/TrustStore.swift
ian-at-strix b212bb92ee Uninstall through the bundled CLI (#24)
* Remove the CA from the app, not the extension

* Uninstall through the bundled CLI

* Log uninstall only when something was removed

* Rename the Installer app to StrixFirewall (#28)

* Rename the Installer app to Firewall

* Name the app Strix Firewall

* Name the app StrixFirewall

* Show why the extension failed to start (#31)

* Delete the CA secret without reading it

* Treat a missing extension as already deactivated

* Leave installing the app to the package
2026-10-21 16:01:25 -04:00

80 lines
3.3 KiB
Swift

import CryptoKit
import Foundation
import Security
private enum TrustStoreError: Error, LocalizedError {
case malformedPEM
case malformedCertificate
case security(String, OSStatus)
var errorDescription: String? {
switch self {
case .malformedPEM:
return "The extension did not return a PEM certificate"
case .malformedCertificate:
return "Certificate data was not valid DER"
case let .security(call, status):
let detail = SecCopyErrorMessageString(status, nil) as String? ?? "status \(status)"
return "\(call) failed: \(detail)"
}
}
}
enum TrustStore {
static func certificate(pem: String) throws -> SecCertificate {
guard let begin = pem.range(of: "-----BEGIN CERTIFICATE-----"),
let end = pem.range(of: "-----END CERTIFICATE-----", range: begin.upperBound ..< pem.endIndex),
let der = Data(base64Encoded: String(pem[begin.upperBound ..< end.lowerBound]),
options: .ignoreUnknownCharacters)
else { throw TrustStoreError.malformedPEM }
guard let certificate = SecCertificateCreateWithData(nil, der as CFData) else {
throw TrustStoreError.malformedCertificate
}
return certificate
}
static func isTrusted(_ certificate: SecCertificate) -> Bool {
var settings: CFArray?
return SecTrustSettingsCopyTrustSettings(certificate, .admin, &settings) == errSecSuccess
}
/// Scoped to SSL rather than "any usage", so a leaked key cannot sign code or email.
static func trust(_ certificate: SecCertificate) throws {
let settings: [[String: Any]] = [[
kSecTrustSettingsPolicy as String: SecPolicyCreateSSL(true, nil),
kSecTrustSettingsResult as String: SecTrustSettingsResult.trustRoot.rawValue,
]]
let status = SecTrustSettingsSetTrustSettings(certificate, .admin, settings as CFArray)
guard status == errSecSuccess else {
throw TrustStoreError.security("SecTrustSettingsSetTrustSettings", status)
}
}
static func untrust(_ certificate: SecCertificate) throws {
let status = SecTrustSettingsRemoveTrustSettings(certificate, .admin)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw TrustStoreError.security("SecTrustSettingsRemoveTrustSettings", status)
}
}
/// Matched by name because trust settings outlive the CA: a re-mint would otherwise leave the old root trusted.
static func trustedAuthorities(commonName: String) -> [SecCertificate] {
var copied: CFArray?
guard SecTrustSettingsCopyCertificates(.admin, &copied) == errSecSuccess,
let certificates = copied as? [SecCertificate]
else { return [] }
return certificates.filter { certificate in
var name: CFString?
guard SecCertificateCopyCommonName(certificate, &name) == errSecSuccess else { return false }
return name as String? == commonName
}
}
static func fingerprint(_ certificate: SecCertificate) -> String {
let digest = SHA256.hash(data: SecCertificateCopyData(certificate) as Data)
return digest.map { String(format: "%02X", $0) }.joined(separator: ":")
}
}