Mac: secure XPC and register mach service via launchd
This commit is contained in:
@@ -2,6 +2,9 @@ import AppKit
|
|||||||
import Foundation
|
import Foundation
|
||||||
import MenuBarExtraAccess
|
import MenuBarExtraAccess
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Security
|
||||||
|
import OSLog
|
||||||
|
import Darwin
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct ClawdisApp: App {
|
struct ClawdisApp: App {
|
||||||
@@ -386,6 +389,8 @@ enum CritterIconRenderer {
|
|||||||
final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate {
|
final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate {
|
||||||
private var listener: NSXPCListener?
|
private var listener: NSXPCListener?
|
||||||
private var state: AppState?
|
private var state: AppState?
|
||||||
|
private let xpcLogger = Logger(subsystem: "com.steipete.clawdis", category: "xpc")
|
||||||
|
private let allowedTeamIDs: Set<String> = ["Y5PE65HELJ"]
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
@@ -426,6 +431,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
|
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
|
||||||
|
guard self.isAllowed(connection: connection) else {
|
||||||
|
self.xpcLogger.error("Rejecting XPC connection: team ID mismatch or invalid audit token")
|
||||||
|
connection.invalidate()
|
||||||
|
return false
|
||||||
|
}
|
||||||
let interface = NSXPCInterface(with: ClawdisXPCProtocol.self)
|
let interface = NSXPCInterface(with: ClawdisXPCProtocol.self)
|
||||||
connection.exportedInterface = interface
|
connection.exportedInterface = interface
|
||||||
connection.exportedObject = ClawdisXPCService()
|
connection.exportedObject = ClawdisXPCService()
|
||||||
@@ -438,4 +448,55 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
|
|||||||
let running = NSWorkspace.shared.runningApplications.filter { $0.bundleIdentifier == bundleID }
|
let running = NSWorkspace.shared.runningApplications.filter { $0.bundleIdentifier == bundleID }
|
||||||
return running.count > 1
|
return running.count > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func isAllowed(connection: NSXPCConnection) -> Bool {
|
||||||
|
// Prefer audit token (available via KVC); fall back to pid-based lookup.
|
||||||
|
if let tokenData = connection.value(forKey: "auditToken") as? Data,
|
||||||
|
tokenData.count == MemoryLayout<audit_token_t>.size {
|
||||||
|
var token = audit_token_t()
|
||||||
|
_ = withUnsafeMutableBytes(of: &token) { tokenData.copyBytes(to: $0) }
|
||||||
|
let attrs: NSDictionary = [kSecGuestAttributeAudit: tokenData]
|
||||||
|
if self.teamIDMatches(attrs: attrs) { return true }
|
||||||
|
}
|
||||||
|
|
||||||
|
let pid = connection.processIdentifier
|
||||||
|
guard pid > 0 else { return false }
|
||||||
|
let attrs: NSDictionary = [kSecGuestAttributePid: pid]
|
||||||
|
if self.teamIDMatches(attrs: attrs) { return true }
|
||||||
|
|
||||||
|
// Fallback: allow same-user processes (still local-only).
|
||||||
|
var pidInfo = kinfo_proc()
|
||||||
|
var size = MemoryLayout.size(ofValue: pidInfo)
|
||||||
|
var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]
|
||||||
|
let result = name.withUnsafeMutableBufferPointer { namePtr -> Bool in
|
||||||
|
return sysctl(namePtr.baseAddress, u_int(namePtr.count), &pidInfo, &size, nil, 0) == 0
|
||||||
|
}
|
||||||
|
if result, pidInfo.kp_eproc.e_ucred.cr_uid == getuid() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func teamIDMatches(attrs: NSDictionary) -> Bool {
|
||||||
|
var secCode: SecCode?
|
||||||
|
guard SecCodeCopyGuestWithAttributes(nil, attrs, SecCSFlags(), &secCode) == errSecSuccess,
|
||||||
|
let code = secCode else { return false }
|
||||||
|
|
||||||
|
var staticCode: SecStaticCode?
|
||||||
|
guard SecCodeCopyStaticCode(code, SecCSFlags(), &staticCode) == errSecSuccess,
|
||||||
|
let sCode = staticCode else { return false }
|
||||||
|
|
||||||
|
var infoCF: CFDictionary?
|
||||||
|
guard SecCodeCopySigningInformation(sCode, SecCSFlags(), &infoCF) == errSecSuccess,
|
||||||
|
let info = infoCF as? [String: Any],
|
||||||
|
let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.allowedTeamIDs.contains(teamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func writeEndpoint(_ endpoint: NSXPCListenerEndpoint) {}
|
||||||
|
@MainActor private func writeEndpointIfAvailable() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,3 @@ echo "🔏 Signing bundle (ad-hoc by default; set SIGN_IDENTITY for real cert)"
|
|||||||
SIGN_IDENTITY="${SIGN_IDENTITY:--}" "$ROOT_DIR/scripts/codesign-mac-app.sh" "$APP_ROOT"
|
SIGN_IDENTITY="${SIGN_IDENTITY:--}" "$ROOT_DIR/scripts/codesign-mac-app.sh" "$APP_ROOT"
|
||||||
|
|
||||||
echo "✅ Bundle ready at $APP_ROOT"
|
echo "✅ Bundle ready at $APP_ROOT"
|
||||||
|
|
||||||
echo "🚀 Launching app"
|
|
||||||
open "$APP_ROOT"
|
|
||||||
|
|||||||
@@ -56,7 +56,42 @@ run_step "swift build" bash -lc "cd '${ROOT_DIR}/apps/macos' && swift build -q -
|
|||||||
# 3) Package + relaunch the app (script also stops any stragglers).
|
# 3) Package + relaunch the app (script also stops any stragglers).
|
||||||
run_step "package app" "${ROOT_DIR}/scripts/package-mac-app.sh"
|
run_step "package app" "${ROOT_DIR}/scripts/package-mac-app.sh"
|
||||||
|
|
||||||
# 4) Verify the packaged app is alive.
|
# 4) Install launch agent with Mach service and bootstrap it (no KeepAlive).
|
||||||
|
cat > "${LAUNCH_AGENT}" <<PLIST
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.steipete.clawdis</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>${APP_BUNDLE}/Contents/MacOS/Clawdis</string>
|
||||||
|
</array>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>${ROOT_DIR}</string>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<false/>
|
||||||
|
<key>MachServices</key>
|
||||||
|
<dict>
|
||||||
|
<key>com.steipete.clawdis.xpc</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/tmp/clawdis.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/tmp/clawdis.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
PLIST
|
||||||
|
|
||||||
|
stop_launch_agent
|
||||||
|
run_step "bootstrap launch agent" launchctl bootstrap gui/"$UID" "${LAUNCH_AGENT}"
|
||||||
|
run_step "kickstart" launchctl kickstart -k gui/"$UID"/com.steipete.clawdis
|
||||||
|
|
||||||
|
# 5) Verify the packaged app is alive.
|
||||||
sleep 1
|
sleep 1
|
||||||
if pgrep -f "${APP_PROCESS_PATTERN}" >/dev/null 2>&1; then
|
if pgrep -f "${APP_PROCESS_PATTERN}" >/dev/null 2>&1; then
|
||||||
log "OK: Clawdis is running."
|
log "OK: Clawdis is running."
|
||||||
|
|||||||
Reference in New Issue
Block a user