Merge pull request #21 from RobotsAndPencils/helper

Add privileged helper
This commit is contained in:
Brandon Evans 2020-12-28 10:40:18 -07:00 committed by GitHub
commit e617fd2689
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 872 additions and 7 deletions

View file

@ -29,3 +29,23 @@ xcodes used Point Free's Environment type, and I'm happy with how that turned ou
## State Management
While I'm curious and eager to try Point Free's [Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture), I'm going to avoid it at first in favour of a simpler AppState ObservableObject. My motivation for this is to try to have something more familiar to a contributor that was also new to SwiftUI, so that the codebase doesn't have too many new or unfamiliar things. If we run into performance or correctness issues in the future I think TCA should be a candidate to reconsider.
## Privilege Escalation
Unlike [xcodes](https://github.com/RobotsAndPencils/xcodes/blob/master/DECISIONS.md#privilege-escalation), there is a better option than running sudo in a Process when we need to escalate privileges in Xcodes.app, namely a privileged helper.
A separate, bundle executable is installed as a privileged helper using SMJobBless and communicates with the main app (the client) over XPC. This helper performs the post-install and xcode-select tasks that would require sudo from the command line. The helper and main app validate each other's bundle ID, version and code signing certificate chain. Validation of the connection is done using the private audit token API. An alternative is to validate the code signature of the client based on the PID from a first "handshake" message. DTS [seems to say](https://developer.apple.com/forums/thread/72881#420409022) that this would also be safe against an attacker PID-wrapping. Because the SMJobBless + XPC examples I found online all use the audit token instead, I decided to go with that. The tradeoff is that this is private API.
Uninstallation is not provided yet. I had this partially implemented (one attempt was based on [DoNotDisturb's approach](https://github.com/objective-see/DoNotDisturb/blob/237b19800fa356f830d1c02715a9a75be08b8924/configure/Helper/HelperInterface.m#L123)) but an issue that I kept hitting was that despite the helper not being installed or running I was able to get a remote object proxy over the connection. Adding a timeout to getVersion might be sufficient as a workaround, as it should return the string immediately.
- [Apple Developer: Creating XPC Services](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html)
- [Objective Development: The Story Behind CVE-2019-13013](https://blog.obdev.at/what-we-have-learned-from-a-vulnerability/)
- [Apple Developer Forums: How to and When to uninstall a privileged helper](https://developer.apple.com/forums/thread/66821)
- [Apple Developer Forums: XPC restricted to processes with the same code signing?](https://developer.apple.com/forums/thread/72881#419817)
- [Wojciech Reguła: Learn XPC exploitation - Part 1: Broken cryptography](https://wojciechregula.blog/post/learn-xpc-exploitation-part-1-broken-cryptography/)
- [Wojciech Reguła: Learn XPC exploitation - Part 2: Say no to the PID!](https://wojciechregula.blog/post/learn-xpc-exploitation-part-2-say-no-to-the-pid/)
- [Wojciech Reguła: Learn XPC exploitation - Part 3: Code injections](https://wojciechregula.blog/post/learn-xpc-exploitation-part-3-code-injections/)
- [Apple Developer: EvenBetterAuthorizationSample](https://developer.apple.com/library/archive/samplecode/EvenBetterAuthorizationSample/Introduction/Intro.html)
- [erikberglund/SwiftPrivilegedHelper](https://github.com/erikberglund/SwiftPrivilegedHelper)
- [aronskaya/smjobbless](https://github.com/aronskaya/smjobbless)
- [securing/SimpleXPCApp](https://github.com/securing/SimpleXPCApp)

View file

@ -0,0 +1,11 @@
import Foundation
let machServiceName = "com.robotsandpencils.XcodesApp.Helper"
let clientBundleID = "com.robotsandpencils.XcodesApp"
let subjectOrganizationalUnit = Bundle.main.infoDictionary!["CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT"] as! String
@objc(HelperXPCProtocol)
protocol HelperXPCProtocol {
func getVersion(completion: @escaping (String) -> Void)
func xcodeSelect(absolutePath: String, completion: @escaping (Error?) -> Void)
}

View file

@ -14,6 +14,8 @@ Xcodes.app is currently only provided as source code that must be built using Xc
You'll need macOS 11 Big Sur and Xcode 12 in order to build and run Xcodes.app.
If you aren't a Robots and Pencils employee you'll need to change the CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT build setting to your Apple Developer team ID in order for code signing validation to succeed between the main app and the privileged helper.
Notable design decisions are recorded in [DECISIONS.md](./DECISIONS.md). The Apple authentication flow is described in [Apple.paw](./Apple.paw), which will allow you to play with the API endpoints that are involved using the [Paw](https://paw.cloud) app.
[`xcode-install`](https://github.com/xcpretty/xcode-install) and [fastlane/spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) both deserve credit for figuring out the hard parts of what makes this possible.

View file

@ -0,0 +1,18 @@
#!/bin/bash
PRIVILEGED_HELPER_LABEL=com.robotsandpencils.XcodesApp.Helper
sudo rm /Library/PrivilegedHelperTools/$PRIVILEGED_HELPER_LABEL
sudo rm /Library/LaunchDaemons/$PRIVILEGED_HELPER_LABEL.plist
sudo launchctl bootout system/$PRIVILEGED_HELPER_LABEL #'Boot-out failed: 36: Operation now in progress' is OK output
echo "Querying launchd..."
LAUNCHD_OUTPUT=$(sudo launchctl list | grep $PRIVILEGED_HELPER_LABEL)
if [ -z "$LAUNCHD_OUTPUT" ]
then
echo "Finished successfully."
else
echo "WARNING: $PRIVILEGED_HELPER_LABEL is not removed"
fi

View file

@ -25,6 +25,15 @@
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF87A2595293E00E47BAF /* DataSource.swift */; };
CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF88025955C7000E47BAF /* AvailableXcode.swift */; };
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8862595607900E47BAF /* InstalledXcode.swift */; };
CA9FF8B12595967A00E47BAF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8B02595967A00E47BAF /* main.swift */; };
CA9FF8BC259596CB00E47BAF /* com.robotsandpencils.XcodesApp.Helper in Copy Helper */ = {isa = PBXBuildFile; fileRef = CA9FF8AE2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
CA9FF8CF25959A9700E47BAF /* HelperXPCShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */; };
CA9FF8D025959A9700E47BAF /* HelperXPCShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */; };
CA9FF8DB25959B4000E47BAF /* XPCDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8DA25959B4000E47BAF /* XPCDelegate.swift */; };
CA9FF8E025959BAA00E47BAF /* ConnectionVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8DF25959BAA00E47BAF /* ConnectionVerifier.swift */; };
CA9FF8E625959BB800E47BAF /* AuditTokenHack.m in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8E525959BB800E47BAF /* AuditTokenHack.m */; };
CA9FF8F525959CE000E47BAF /* HelperInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */; };
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF9352595B44700E47BAF /* HelperClient.swift */; };
CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2C255A5262003FD669 /* AppleAPI */; };
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */; };
CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */; };
@ -65,6 +74,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
CA9FF8B9259596A000E47BAF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = CAD2E7962449574E00113D76 /* Project object */;
proxyType = 1;
remoteGlobalIDString = CA9FF8AD2595967A00E47BAF;
remoteInfo = com.robotsandpencils.XcodesApp.Helper;
};
CAD2E7B42449575100113D76 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = CAD2E7962449574E00113D76 /* Project object */;
@ -74,6 +90,29 @@
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
CA9FF8AC2595967A00E47BAF /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
CA9FF8BB259596B500E47BAF /* Copy Helper */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = Contents/Library/LaunchServices;
dstSubfolderSpec = 1;
files = (
CA9FF8BC259596CB00E47BAF /* com.robotsandpencils.XcodesApp.Helper in Copy Helper */,
);
name = "Copy Helper";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
63EAA4EA259944450046AB8F /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = "<group>"; };
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = "<group>"; };
@ -97,6 +136,20 @@
CA9FF87A2595293E00E47BAF /* DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = "<group>"; };
CA9FF88025955C7000E47BAF /* AvailableXcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableXcode.swift; sourceTree = "<group>"; };
CA9FF8862595607900E47BAF /* InstalledXcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledXcode.swift; sourceTree = "<group>"; };
CA9FF8AE2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = com.robotsandpencils.XcodesApp.Helper; sourceTree = BUILT_PRODUCTS_DIR; };
CA9FF8B02595967A00E47BAF /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
CA9FF8C22595988B00E47BAF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CA9FF8C32595989800E47BAF /* launchd.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = launchd.plist; sourceTree = "<group>"; };
CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperXPCShared.swift; sourceTree = "<group>"; };
CA9FF8DA25959B4000E47BAF /* XPCDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCDelegate.swift; sourceTree = "<group>"; };
CA9FF8DF25959BAA00E47BAF /* ConnectionVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionVerifier.swift; sourceTree = "<group>"; };
CA9FF8E425959BB800E47BAF /* AuditTokenHack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AuditTokenHack.h; sourceTree = "<group>"; };
CA9FF8E525959BB800E47BAF /* AuditTokenHack.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AuditTokenHack.m; sourceTree = "<group>"; };
CA9FF8EA25959BDD00E47BAF /* com.robotsandpencils.XcodesApp.Helper-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "com.robotsandpencils.XcodesApp.Helper-Bridging-Header.h"; sourceTree = "<group>"; };
CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperInstaller.swift; sourceTree = "<group>"; };
CA9FF9052595A28400E47BAF /* com.robotsandpencils.XcodesApp.HelperTest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = com.robotsandpencils.XcodesApp.HelperTest.entitlements; sourceTree = "<group>"; };
CA9FF9252595A7EB00E47BAF /* Scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Scripts; sourceTree = "<group>"; };
CA9FF9352595B44700E47BAF /* HelperClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperClient.swift; sourceTree = "<group>"; };
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInCredentialsView.swift; sourceTree = "<group>"; };
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignIn2FAView.swift; sourceTree = "<group>"; };
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSMSView.swift; sourceTree = "<group>"; };
@ -135,9 +188,17 @@
CAD2E7B92449575100113D76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedXcode.swift; sourceTree = "<group>"; };
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
CAFBDBA525990C76003DCC5A /* SimpleXPCApp.LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = SimpleXPCApp.LICENSE; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
CA9FF8AB2595967A00E47BAF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
CAD2E79B2449574E00113D76 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -189,6 +250,31 @@
path = About;
sourceTree = "<group>";
};
CA9FF8AF2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */ = {
isa = PBXGroup;
children = (
CA9FF8E425959BB800E47BAF /* AuditTokenHack.h */,
CA9FF8E525959BB800E47BAF /* AuditTokenHack.m */,
CA9FF8EA25959BDD00E47BAF /* com.robotsandpencils.XcodesApp.Helper-Bridging-Header.h */,
CA9FF8DF25959BAA00E47BAF /* ConnectionVerifier.swift */,
CA9FF8B02595967A00E47BAF /* main.swift */,
CA9FF8DA25959B4000E47BAF /* XPCDelegate.swift */,
CA9FF8C22595988B00E47BAF /* Info.plist */,
CA9FF8C32595989800E47BAF /* launchd.plist */,
CA9FF9052595A28400E47BAF /* com.robotsandpencils.XcodesApp.HelperTest.entitlements */,
CAFBDBA525990C76003DCC5A /* SimpleXPCApp.LICENSE */,
);
path = com.robotsandpencils.XcodesApp.Helper;
sourceTree = "<group>";
};
CA9FF8CD25959A7600E47BAF /* HelperXPCShared */ = {
isa = PBXGroup;
children = (
CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */,
);
path = HelperXPCShared;
sourceTree = "<group>";
};
CAA1CB50255A5D16003FD669 /* SignIn */ = {
isa = PBXGroup;
children = (
@ -229,6 +315,8 @@
CABFA9B82592EEEA00380FEE /* FileManager+.swift */,
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */,
CABFA9AC2592EEE900380FEE /* Foundation.swift */,
CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */,
CA9FF9352595B44700E47BAF /* HelperClient.swift */,
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
CABFA9AE2592EEE900380FEE /* Path+.swift */,
CABFA9B42592EEEA00380FEE /* Process.swift */,
@ -279,10 +367,13 @@
CABFA9A32592ED5700380FEE /* Apple.paw */,
CABFA9A12592EAFB00380FEE /* LICENSE */,
CA8FB61C256E115700469DA5 /* .github */,
CA9FF9252595A7EB00E47BAF /* Scripts */,
CA9FF8242594F10700E47BAF /* AcknowledgementsGenerator */,
CA538A0C255A4F1A00E64DD7 /* AppleAPI */,
CAD2E7A02449574E00113D76 /* Xcodes */,
CAD2E7B62449575100113D76 /* XcodesTests */,
CA9FF8AF2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */,
CA9FF8CD25959A7600E47BAF /* HelperXPCShared */,
CAD2E79F2449574E00113D76 /* Products */,
CA538A12255A4F7C00E64DD7 /* Frameworks */,
);
@ -293,6 +384,7 @@
children = (
CAD2E79E2449574E00113D76 /* Xcodes.app */,
CAD2E7B32449575100113D76 /* XcodesTests.xctest */,
CA9FF8AE2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */,
);
name = Products;
sourceTree = "<group>";
@ -329,6 +421,23 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
CA9FF8AD2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */ = {
isa = PBXNativeTarget;
buildConfigurationList = CA9FF8B52595967A00E47BAF /* Build configuration list for PBXNativeTarget "com.robotsandpencils.XcodesApp.Helper" */;
buildPhases = (
CA9FF8AA2595967A00E47BAF /* Sources */,
CA9FF8AB2595967A00E47BAF /* Frameworks */,
CA9FF8AC2595967A00E47BAF /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = com.robotsandpencils.XcodesApp.Helper;
productName = com.robotsandpencils.XcodesApp.Helper;
productReference = CA9FF8AE2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */;
productType = "com.apple.product-type.tool";
};
CAD2E79D2449574E00113D76 /* Xcodes */ = {
isa = PBXNativeTarget;
buildConfigurationList = CAD2E7BC2449575100113D76 /* Build configuration list for PBXNativeTarget "Xcodes" */;
@ -337,10 +446,12 @@
CAD2E79B2449574E00113D76 /* Frameworks */,
CA9FF8292594F33200E47BAF /* Generate Acknowledgements */,
CAD2E79C2449574E00113D76 /* Resources */,
CA9FF8BB259596B500E47BAF /* Copy Helper */,
);
buildRules = (
);
dependencies = (
CA9FF8BA259596A000E47BAF /* PBXTargetDependency */,
);
name = Xcodes;
packageProductDependencies = (
@ -382,10 +493,13 @@
CAD2E7962449574E00113D76 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1140;
LastSwiftUpdateCheck = 1220;
LastUpgradeCheck = 1140;
ORGANIZATIONNAME = "Robots and Pencils";
TargetAttributes = {
CA9FF8AD2595967A00E47BAF = {
CreatedOnToolsVersion = 12.2;
};
CAD2E79D2449574E00113D76 = {
CreatedOnToolsVersion = 11.4;
};
@ -420,6 +534,7 @@
targets = (
CAD2E79D2449574E00113D76 /* Xcodes */,
CAD2E7B22449575100113D76 /* XcodesTests */,
CA9FF8AD2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */,
);
};
/* End PBXProject section */
@ -466,14 +581,28 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
CA9FF8AA2595967A00E47BAF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CA9FF8D025959A9700E47BAF /* HelperXPCShared.swift in Sources */,
CA9FF8DB25959B4000E47BAF /* XPCDelegate.swift in Sources */,
CA9FF8E625959BB800E47BAF /* AuditTokenHack.m in Sources */,
CA9FF8B12595967A00E47BAF /* main.swift in Sources */,
CA9FF8E025959BAA00E47BAF /* ConnectionVerifier.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
CAD2E79A2449574E00113D76 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CA9FF8CF25959A9700E47BAF /* HelperXPCShared.swift in Sources */,
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */,
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */,
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
CA44901F2463AD34003D8213 /* Tag.swift in Sources */,
CABFA9BF2592EEEA00380FEE /* URLSession+Promise.swift in Sources */,
@ -496,6 +625,7 @@
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
CA9FF8F525959CE000E47BAF /* HelperInstaller.swift in Sources */,
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */,
@ -526,6 +656,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
CA9FF8BA259596A000E47BAF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = CA9FF8AD2595967A00E47BAF /* com.robotsandpencils.XcodesApp.Helper */;
targetProxy = CA9FF8B9259596A000E47BAF /* PBXContainerItemProxy */;
};
CAD2E7B52449575100113D76 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = CAD2E79D2449574E00113D76 /* Xcodes */;
@ -566,6 +701,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = PBH8V487HB;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -641,6 +777,74 @@
};
name = Test;
};
CA9FF8B22595967A00E47BAF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
DEVELOPMENT_TEAM = PBH8V487HB;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
OTHER_LDFLAGS = (
"-sectcreate",
__TEXT,
__launchd_plist,
"$(SRCROOT)/${TARGET_NAME}/launchd.plist",
);
PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp.Helper;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "com.robotsandpencils.XcodesApp.Helper/com.robotsandpencils.XcodesApp.Helper-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
CA9FF8B32595967A00E47BAF /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_ENTITLEMENTS = com.robotsandpencils.XcodesApp.Helper/com.robotsandpencils.XcodesApp.HelperTest.entitlements;
CODE_SIGN_STYLE = Manual;
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
OTHER_LDFLAGS = (
"-sectcreate",
__TEXT,
__launchd_plist,
"$(SRCROOT)/${TARGET_NAME}/launchd.plist",
);
PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp.Helper;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "com.robotsandpencils.XcodesApp.Helper/com.robotsandpencils.XcodesApp.Helper-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
name = Test;
};
CA9FF8B42595967A00E47BAF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
DEVELOPMENT_TEAM = PBH8V487HB;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
OTHER_LDFLAGS = (
"-sectcreate",
__TEXT,
__launchd_plist,
"$(SRCROOT)/${TARGET_NAME}/launchd.plist",
);
PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp.Helper;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "com.robotsandpencils.XcodesApp.Helper/com.robotsandpencils.XcodesApp.Helper-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
name = Release;
};
CAD2E7BA2449575100113D76 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -673,6 +877,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = PBH8V487HB;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -733,6 +938,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = PBH8V487HB;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
@ -843,6 +1049,16 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
CA9FF8B52595967A00E47BAF /* Build configuration list for PBXNativeTarget "com.robotsandpencils.XcodesApp.Helper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CA9FF8B22595967A00E47BAF /* Debug */,
CA9FF8B32595967A00E47BAF /* Test */,
CA9FF8B42595967A00E47BAF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
CAD2E7992449574E00113D76 /* Build configuration list for PBXProject "Xcodes" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View file

@ -73,7 +73,7 @@ guard let currentProject = projects.first(where: ({ $0.workspacePath == projectP
let checkouts = currentProject.url.deletingLastPathComponent().appendingPathComponent("SourcePackages/checkouts")
let checkedDependencies = try fileManager.contentsOfDirectory(at: checkouts, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
let licences: [Xcode.Project.License] = checkedDependencies.compactMap {
let spmLicences: [Xcode.Project.License] = checkedDependencies.compactMap {
let supportedFilenames = ["LICENSE", "LICENSE.txt", "LICENSE.md"]
for filename in supportedFilenames {
let licenseURL = $0.appendingPathComponent(filename)
@ -84,6 +84,19 @@ let licences: [Xcode.Project.License] = checkedDependencies.compactMap {
return nil
}
var manualLicenses: [Xcode.Project.License] = []
let enumerator = fileManager.enumerator(at: projectURL.deletingLastPathComponent(), includingPropertiesForKeys: [URLResourceKey.nameKey], options: .skipsHiddenFiles)!
for case let url as URL in enumerator where url.lastPathComponent.hasSuffix(".LICENSE") {
manualLicenses.append(
Xcode.Project.License(
url: url,
name: url.lastPathComponent.replacingOccurrences(of: ".LICENSE", with: "")
)
)
}
let licences = spmLicences + manualLicenses
let acknowledgementsAttributedString = NSMutableAttributedString()
for licence in licences {
acknowledgementsAttributedString.append(NSAttributedString(string: licence.name + "\n\n", attributes: [.font: NSFont.preferredFont(forTextStyle: .title2)]))

View file

@ -1,7 +1,6 @@
MIT License
Copyright (c) 2019 MacPaw
Copyright (c) 2020 Robots and Pencils
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -9,7 +9,9 @@ import Version
class AppState: ObservableObject {
private let client = AppleAPI.Client()
private let helperClient = HelperClient()
private var cancellables = Set<AnyCancellable>()
private var selectPublisher: AnyCancellable?
@Published var authenticationState: AuthenticationState = .unauthenticated
@Published var availableXcodes: [AvailableXcode] = [] {
@ -30,9 +32,11 @@ class AppState: ObservableObject {
// but we need it here instead so that it can be a focusedValue at the top level in XcodesApp instead of in a list row. The latter seems more like how the focusedValue API is supposed to work, but currently doesn't.
@Published var selectedXcodeID: Xcode.ID?
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
@Published var helperInstallState: HelperInstallState = .notInstalled
init() {
try? loadCachedAvailableXcodes()
checkIfHelperIsInstalled()
}
// MARK: - Authentication
@ -175,6 +179,26 @@ class AppState: ObservableObject {
authenticationState = .unauthenticated
}
// MARK: - Helper
func installHelper() {
HelperInstaller.install()
checkIfHelperIsInstalled()
}
private func checkIfHelperIsInstalled() {
helperInstallState = .unknown
helperClient.checkIfLatestHelperIsInstalled()
.receive(on: DispatchQueue.main)
.sink(
receiveValue: { installed in
self.helperInstallState = installed ? .installed : .notInstalled
}
)
.store(in: &cancellables)
}
// MARK: -
func install(id: Xcode.ID) {
@ -192,7 +216,25 @@ class AppState: ObservableObject {
}
func select(id: Xcode.ID) {
// TODO:
if helperInstallState == .notInstalled {
installHelper()
}
guard
let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }),
selectPublisher == nil
else { return }
selectPublisher = HelperClient().switchXcodePath(installedXcode.path.string)
.sink(
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = AlertContent(title: "Error selecting Xcode", message: error.legibleLocalizedDescription)
}
self.selectPublisher = nil
},
receiveValue: { _ in }
)
}
func launch(id: Xcode.ID) {
@ -245,6 +287,12 @@ class AppState: ObservableObject {
// MARK: - Nested Types
enum HelperInstallState: Equatable {
case unknown
case notInstalled
case installed
}
struct AlertContent: Identifiable {
var title: String
var message: String

View file

@ -12,4 +12,8 @@ extension Bundle {
var version: String? {
infoDictionary?["CFBundleVersion"] as? String
}
var humanReadableCopyright: String? {
infoDictionary?["NSHumanReadableCopyright"] as? String
}
}

View file

@ -0,0 +1,106 @@
import Combine
import Foundation
final class HelperClient {
private var connection: NSXPCConnection?
func currentConnection() -> NSXPCConnection? {
guard self.connection == nil else {
return self.connection
}
let connection = NSXPCConnection(machServiceName: machServiceName, options: .privileged)
connection.remoteObjectInterface = NSXPCInterface(with: HelperXPCProtocol.self)
connection.invalidationHandler = {
self.connection?.invalidationHandler = nil
DispatchQueue.main.async {
self.connection = nil
}
}
self.connection = connection
connection.resume()
return self.connection
}
func helper(errorSubject: PassthroughSubject<String, Error>) -> HelperXPCProtocol? {
guard
let helper = self.currentConnection()?.remoteObjectProxyWithErrorHandler({ error in
errorSubject.send(completion: .failure(error))
}) as? HelperXPCProtocol
else { return nil }
return helper
}
func checkIfLatestHelperIsInstalled() -> AnyPublisher<Bool, Never> {
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + machServiceName)
guard
let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any],
let bundledHelperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String
else {
return Just(false).eraseToAnyPublisher()
}
return getVersion()
.map { installedHelperVersion in installedHelperVersion == bundledHelperVersion }
.catch { _ in Just(false) }
.eraseToAnyPublisher()
}
func getVersion() -> AnyPublisher<String, Error> {
let connectionErrorSubject = PassthroughSubject<String, Error>()
guard
let helper = self.helper(errorSubject: connectionErrorSubject)
else {
return Fail(error: NSError())
.eraseToAnyPublisher()
}
return Deferred {
Future { promise in
helper.getVersion { version in
promise(.success(version))
}
}
}
// Take values, but fail when connectionErrorSubject fails
.zip(
connectionErrorSubject
.prepend("")
.map { _ in Void() }
)
.map { $0.0 }
.eraseToAnyPublisher()
}
func switchXcodePath(_ absolutePath: String) -> AnyPublisher<Void, Error> {
let connectionErrorSubject = PassthroughSubject<String, Error>()
guard
let helper = self.helper(errorSubject: connectionErrorSubject)
else {
return Fail(error: NSError())
.eraseToAnyPublisher()
}
return Deferred {
Future { promise in
helper.xcodeSelect(absolutePath: absolutePath, completion: { (possibleError) in
if let error = possibleError {
promise(.failure(error))
} else {
promise(.success(()))
}
})
}
}
// Take values, but fail when connectionErrorSubject fails
.zip(
connectionErrorSubject
.prepend("")
.map { _ in Void() }
)
.map { $0.0 }
.eraseToAnyPublisher()
}
}

View file

@ -0,0 +1,42 @@
// From https://github.com/securing/SimpleXPCApp/
import Foundation
import ServiceManagement
enum HelperAuthorizationError: Error {
case message(String)
}
class HelperInstaller {
private static func executeAuthorizationFunction(_ authorizationFunction: () -> (OSStatus) ) throws {
let osStatus = authorizationFunction()
guard osStatus == errAuthorizationSuccess else {
throw HelperAuthorizationError.message(String(describing: SecCopyErrorMessageString(osStatus, nil)))
}
}
static func authorizationRef(_ rights: UnsafePointer<AuthorizationRights>?,
_ environment: UnsafePointer<AuthorizationEnvironment>?,
_ flags: AuthorizationFlags) throws -> AuthorizationRef? {
var authRef: AuthorizationRef?
try executeAuthorizationFunction { AuthorizationCreate(rights, environment, flags, &authRef) }
return authRef
}
static func install() {
var authItem = kSMRightBlessPrivilegedHelper.withCString { name in
AuthorizationItem(name: name, valueLength: 0, value:UnsafeMutableRawPointer(bitPattern: 0), flags: 0)
}
var authRights = withUnsafeMutablePointer(to: &authItem) { authItem in
AuthorizationRights(count: 1, items: authItem)
}
do {
let authRef = try authorizationRef(&authRights, nil, [.interactionAllowed, .extendRights, .preAuthorize])
var cfError: Unmanaged<CFError>?
SMJobBless(kSMDomainSystemLaunchd, machServiceName as CFString, authRef, &cfError)
} catch let err {
print("Error in installing the helper -> \(err.localizedDescription)")
}
}
}

View file

@ -30,7 +30,7 @@ struct AboutView: View {
.buttonStyle(LinkButtonStyle())
}
Text("Copyright © 2020 Robots and Pencils")
Text(Bundle.main.humanReadableCopyright!)
.font(.footnote)
}
}

View file

@ -17,18 +17,25 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2020 Robots and Pencils. All rights reserved.</string>
<string>Copyright © 2020 Robots and Pencils.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>
<true/>
<key>NSSupportsSuddenTermination</key>
<true/>
<key>SMPrivilegedExecutables</key>
<dict>
<key>com.robotsandpencils.XcodesApp.Helper</key>
<string>identifier &quot;com.robotsandpencils.XcodesApp.Helper&quot; and info [CFBundleShortVersionString] &gt;= &quot;1.0.0&quot; and anchor apple generic and certificate leaf[subject.OU] = &quot;$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)&quot;</string>
</dict>
<key>CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT</key>
<string>$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)</string>
</dict>
</plist>

View file

@ -419,4 +419,58 @@ OTHER DEALINGS IN THE SOFTWARE.\
For more information, please refer to &lt;<http://unlicense.org/>&gt;\
\
\
\fs34 spm-licenses\
\
\fs26 MIT License\
\
Copyright (c) 2019 MacPaw\
\
Permission is hereby granted, free of charge, to any person obtaining a copy\
of this software and associated documentation files (the "Software"), to deal\
in the Software without restriction, including without limitation the rights\
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
copies of the Software, and to permit persons to whom the Software is\
furnished to do so, subject to the following conditions:\
\
The above copyright notice and this permission notice shall be included in all\
copies or substantial portions of the Software.\
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
SOFTWARE.\
\
\
\fs34 SimpleXPCApp\
\
\fs26 MIT License\
\
Copyright (c) 2020 securing\
\
Permission is hereby granted, free of charge, to any person obtaining a copy\
of this software and associated documentation files (the "Software"), to deal\
in the Software without restriction, including without limitation the rights\
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
copies of the Software, and to permit persons to whom the Software is\
furnished to do so, subject to the following conditions:\
\
The above copyright notice and this permission notice shall be included in all\
copies or substantial portions of the Software.\
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
SOFTWARE.\
\
\
}

View file

@ -38,6 +38,28 @@ struct SettingsView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
}
GroupBox(label: Text("Privileged Helper")) {
VStack(alignment: .leading, spacing: 8) {
switch appState.helperInstallState {
case .unknown:
ProgressView()
.scaleEffect(0.5, anchor: .center)
case .installed:
Text("Helper is installed")
case .notInstalled:
HStack {
Text("Helper is not installed")
Button("Install helper") {
appState.installHelper()
}
}
}
Text("Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.")
.font(.footnote)
}
}
Spacer()
}
.padding()

View file

@ -0,0 +1,16 @@
// From https://github.com/securing/SimpleXPCApp/
#import <Foundation/Foundation.h>
@interface NSXPCConnection(PrivateAuditToken)
@property (nonatomic, readonly) audit_token_t auditToken;
@end
@interface AuditTokenHack : NSObject
+(NSData *)getAuditTokenDataFromNSXPCConnection:(NSXPCConnection *)connection;
@end

View file

@ -0,0 +1,12 @@
// From https://github.com/securing/SimpleXPCApp/
#import "AuditTokenHack.h"
@implementation AuditTokenHack
+ (NSData *)getAuditTokenDataFromNSXPCConnection:(NSXPCConnection *)connection {
audit_token_t auditToken = connection.auditToken;
return [NSData dataWithBytes:&auditToken length:sizeof(audit_token_t)];
}
@end

View file

@ -0,0 +1,123 @@
// From https://github.com/securing/SimpleXPCApp/
import Foundation
class ConnectionVerifier {
private static func prepareCodeReferencesFromAuditToken(connection: NSXPCConnection, secCodeOptional: inout SecCode?, secStaticCodeOptional: inout SecStaticCode?) -> Bool {
let auditTokenData = AuditTokenHack.getAuditTokenData(from: connection)
let attributesDictrionary = [
kSecGuestAttributeAudit : auditTokenData
]
if SecCodeCopyGuestWithAttributes(nil, attributesDictrionary as CFDictionary, SecCSFlags(rawValue: 0), &secCodeOptional) != errSecSuccess {
NSLog("Couldn't get SecCode with the audit token")
return false
}
guard let secCode = secCodeOptional else {
NSLog("Couldn't unwrap the secCode")
return false
}
SecCodeCopyStaticCode(secCode, SecCSFlags(rawValue: 0), &secStaticCodeOptional)
guard let _ = secStaticCodeOptional else {
NSLog("Couldn't unwrap the secStaticCode")
return false
}
return true
}
private static func verifyHardenedRuntimeAndProblematicEntitlements(secStaticCode: SecStaticCode) -> Bool {
var signingInformationOptional: CFDictionary? = nil
if SecCodeCopySigningInformation(secStaticCode, SecCSFlags(rawValue: kSecCSDynamicInformation), &signingInformationOptional) != errSecSuccess {
NSLog("Couldn't obtain signing information")
return false
}
guard let signingInformation = signingInformationOptional else {
return false
}
let signingInformationDict = signingInformation as NSDictionary
let signingFlagsOptional = signingInformationDict.object(forKey: "flags") as? UInt32
if let signingFlags = signingFlagsOptional {
let hardenedRuntimeFlag: UInt32 = 0x10000
if (signingFlags & hardenedRuntimeFlag) != hardenedRuntimeFlag {
NSLog("Hardened runtime is not set for the sender")
return false
}
} else {
return false
}
let entitlementsOptional = signingInformationDict.object(forKey: "entitlements-dict") as? NSDictionary
guard let entitlements = entitlementsOptional else {
return false
}
NSLog("Entitlements are \(entitlements)")
let problematicEntitlements = [
"com.apple.security.get-task-allow",
"com.apple.security.cs.disable-library-validation",
"com.apple.security.cs.allow-dyld-environment-variables"
]
// Skip this check for debug builds because they'll have the get-task-allow entitlement
#if !DEBUG
for problematicEntitlement in problematicEntitlements {
if let presentEntitlement = entitlements.object(forKey: problematicEntitlement) {
if presentEntitlement as! Int == 1 {
NSLog("The sender has \(problematicEntitlement) entitlement set to true")
return false
}
}
}
#endif
return true
}
private static func verifyWithRequirementString(secCode: SecCode) -> Bool {
// Code Signing Requirement Language
// https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html#//apple_ref/doc/uid/TP40005929-CH5-SW1
let requirementString = "identifier \"\(clientBundleID)\" and info [CFBundleShortVersionString] >= \"1.0.0\" and anchor apple generic and certificate leaf[subject.OU] = \"\(subjectOrganizationalUnit)\"" as NSString
var secRequirement: SecRequirement? = nil
if SecRequirementCreateWithString(requirementString as CFString, SecCSFlags(rawValue: 0), &secRequirement) != errSecSuccess {
NSLog("Couldn't create the requirement string")
return false
}
if SecCodeCheckValidity(secCode, SecCSFlags(rawValue: 0), secRequirement) != errSecSuccess {
NSLog("NSXPC client does not meet the requirements")
return false
}
return true
}
public static func isValid(connection: NSXPCConnection) -> Bool {
var secCodeOptional: SecCode? = nil
var secStaticCodeOptional: SecStaticCode? = nil
if !prepareCodeReferencesFromAuditToken(connection: connection, secCodeOptional: &secCodeOptional, secStaticCodeOptional: &secStaticCodeOptional) {
return false
}
if !verifyHardenedRuntimeAndProblematicEntitlements(secStaticCode: secStaticCodeOptional!) {
return false
}
if !verifyWithRequirementString(secCode: secCodeOptional!) {
return false
}
return true
}
}

View file

@ -0,0 +1,18 @@
<?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>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>SMAuthorizedClients</key>
<array>
<string>identifier "com.robotsandpencils.XcodesApp" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)"</string>
</array>
<key>CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT</key>
<string>$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)</string>
</dict>
</plist>

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 securing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,85 @@
import Foundation
class XPCDelegate: NSObject, NSXPCListenerDelegate, HelperXPCProtocol {
// MARK: - NSXPCListenerDelegate
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
guard ConnectionVerifier.isValid(connection: newConnection) else { return false }
newConnection.exportedInterface = NSXPCInterface(with: HelperXPCProtocol.self)
newConnection.exportedObject = self
newConnection.resume()
return true
}
// MARK: - HelperXPCProtocol
func getVersion(completion: @escaping (String) -> Void) {
NSLog("XPCDelegate: \(#function)")
completion(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
}
func xcodeSelect(absolutePath: String, completion: @escaping (Error?) -> Void) {
NSLog("XPCDelegate: \(#function)")
guard URL(fileURLWithPath: absolutePath).hasDirectoryPath else {
completion(XPCDelegateError(.invalidXcodePath))
return
}
run(
url: URL(fileURLWithPath: "/usr/bin/xcode-select"),
arguments: ["-s", absolutePath],
completion: completion
)
}
}
// MARK: - Run
private func run(url: URL, arguments: [String], completion: @escaping (Error?) -> Void) {
NSLog("XPCDelegate: run \(url) \(arguments)")
let process = Process()
process.executableURL = url
process.arguments = arguments
do {
try process.run()
process.waitUntilExit()
completion(nil)
} catch {
completion(error)
}
}
// MARK: - Errors
struct XPCDelegateError: CustomNSError {
enum Code: Int {
case invalidXcodePath
}
let code: Code
init(_ code: Code) {
self.code = code
}
// MARK: - CustomNSError
static var errorDomain: String { "XPCDelegateError" }
var errorCode: Int { code.rawValue }
var errorUserInfo: [String : Any] {
switch code {
case .invalidXcodePath:
return [
NSLocalizedDescriptionKey: "Invalid Xcode path.",
NSLocalizedFailureReasonErrorKey: "Xcode path must be absolute."
]
}
}
}

View file

@ -0,0 +1 @@
#import "AuditTokenHack.h"

View file

@ -0,0 +1,5 @@
<?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/>
</plist>

View file

@ -0,0 +1,13 @@
<?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.robotsandpencils.XcodesApp.Helper</string>
<key>MachServices</key>
<dict>
<key>com.robotsandpencils.XcodesApp.Helper</key>
<true/>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,9 @@
import Foundation
let listener = NSXPCListener.init(machServiceName: machServiceName)
let delegate = XPCDelegate()
listener.delegate = delegate
listener.resume()
RunLoop.main.run()