mirror of
https://git.openafs.org/openafs.git
synced 2025-01-18 15:00:12 +00:00
DARWIN: Add 'privhelper' tool for PrefPane
The prefpane for macOS runs as the logged-in user, but needs root access for some operations: starting/stopping the client, editing various configuration files like CellServDB, etc. We currently use functions like AuthorizationExecuteWithPrivileges() to run commands with root privileges directly, but this approach no longer works as of macOS 10.8 (Mountain Lion); the relevant functions have been removed. Instead, a new approach exists as of macOS 10.6 (Snow Leopard). The prefpane application itself cannot gain root privileges, but we can provide another daemon process that runs as root, and the PrefPane sends requests to that process to perform the privileged operations we need. In this commit, create a separate helper program called PrivilegedHelper (privhelper for short) that serves this purpose. Define the executePrivTask() method in TaskUtil to handle communicating with privhelper over XPC. This commit does not define any of the tasks that privhelper will actually perform; this just implements privhelper itself. Later commits will add and use various privileged tasks in privhelper. In order for privhelper to be able to run as root, both privhelper and the prefpane itself must be code signed and the relevant apple team id must be specified in their Info.plist when they are built, as well as inside privhelper.c. Currently, we have no way of specifying code signatures info during the build, since all code signing is done when generating packages (via pkgbuild.sh) after binaries are built. For now, just put a commented-out section in src/platform/DARWIN/AFSPreference/Info.plist and src/platform/DARWIN/PrivilegedHelper/privhelper-info.plist and a placeholder in src/platform/DARWIN/PrivilegedHelper/privhelper.c to show how to add this information. The package builder must add their own team id to these before privhelper can work properly. The privhelper tool checks that the calling user has authorization to run commands as root (via AuthorizationCopyRights()), and that the calling process is either our AFSBackgrounder menu bar or the prefpane. We use xpc_connection_set_peer_code_signing_requirement() for this where available, but fallback to using SecCodeCheckValidity() with SecCodeCreateWithXPCMessage() or xpc_dictionary_get_audit_token()/SecCodeCopyGuestWithAttributes() if needed. Change-Id: I724b6d486ee5397c89c79e589ddcb2a5987a895b Reviewed-on: https://gerrit.openafs.org/15956 Tested-by: BuildBot <buildbot@rampaginggeek.com> Reviewed-by: Michael Meffie <mmeffie@sinenomine.net> Reviewed-by: Cheyenne Wills <cwills@sinenomine.net> Reviewed-by: Mark Vitale <mvitale@sinenomine.net> Reviewed-by: Andrew Deason <adeason@sinenomine.net>
This commit is contained in:
parent
13ef511544
commit
120871f03f
@ -879,6 +879,7 @@ distclean: clean
|
||||
src/platform/Makefile \
|
||||
src/platform/${MKAFS_OSTYPE}/Makefile \
|
||||
src/platform/DARWIN/growlagent/Makefile \
|
||||
src/platform/DARWIN/PrivilegedHelper/Makefile \
|
||||
src/procmgmt/Makefile \
|
||||
src/procmgmt/test/Makefile \
|
||||
src/ptserver/Makefile \
|
||||
|
@ -111,6 +111,7 @@ AC_CONFIG_FILES([
|
||||
src/platform/Makefile
|
||||
src/platform/${MKAFS_OSTYPE}/Makefile
|
||||
src/platform/DARWIN/growlagent/Makefile
|
||||
src/platform/DARWIN/PrivilegedHelper/Makefile
|
||||
src/procmgmt/Makefile
|
||||
src/procmgmt/test/Makefile
|
||||
src/ptserver/Makefile
|
||||
|
@ -156,6 +156,43 @@ fi
|
||||
#start the new launchd daemon
|
||||
launchctl load -w /Library/LaunchDaemons/org.openafs.filesystems.afs.plist
|
||||
|
||||
# Install and start our privileged helper
|
||||
PRIVHELPER="/Library/PreferencePanes/OpenAFS.prefPane/Contents/Library/LaunchServices/org.openafs.privhelper"
|
||||
if [[ -f "$PRIVHELPER" ]]; then
|
||||
if [[ ! -d "/Library/PrivilegedHelperTools" ]]; then
|
||||
mkdir -p "/Library/PrivilegedHelperTools"
|
||||
chmod 755 "/Library/PrivilegedHelperTools"
|
||||
chown -R root:wheel "/Library/PrivilegedHelperTools"
|
||||
fi
|
||||
|
||||
cp -f "$PRIVHELPER" "/Library/PrivilegedHelperTools"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
chmod 755 "/Library/PrivilegedHelperTools/org.openafs.privhelper"
|
||||
PRIVDAEMON="/Library/LaunchDaemons/privhelper-launchd.plist"
|
||||
cat > "$PRIVDAEMON" << EOF
|
||||
<?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>org.openafs.privhelper</string>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>org.openafs.privhelper</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Library/PrivilegedHelperTools/org.openafs.privhelper</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
chmod 644 "$PRIVDAEMON"
|
||||
launchctl load -w "$PRIVDAEMON"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $majorvers -ge 19 ]; then
|
||||
# Assume that, if /afs is not present, either OpenAFS is being installed for
|
||||
# the first time or the system was not rebooted after the installation.
|
||||
|
@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
DAEMON=/private/var/db/openafs/etc/launchafs.sh
|
||||
DAEMON_LAUNCHD_SCRIPT=/Library/LaunchDaemons/org.openafs.filesystems.afs.plist
|
||||
PRIVHELPER_LAUNCHD_SCRIPT=/Library/LaunchDaemons/privhelper-launchd.plist
|
||||
DAEMON_UP=$(ps -ef | grep "$DAEMON_NAME" | grep -v grep | wc -l)
|
||||
PREFERENCE_PANE=/Library/PreferencePanes/OpenAFS.prefPane
|
||||
|
||||
@ -41,6 +42,10 @@ else
|
||||
|
||||
launchctl unload -w $DAEMON_LAUNCHD_SCRIPT
|
||||
fi
|
||||
|
||||
if [ -f $PRIVHELPER_LAUNCHD_SCRIPT ]; then
|
||||
launchctl unload -w $PRIVHELPER_LAUNCHD_SCRIPT
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -d /Library/PreferencePanes/OpenAFS.prefPane ]; then
|
||||
|
@ -12,7 +12,7 @@ use File::Basename;
|
||||
|
||||
use vars qw ($do_nothing $print_donothing_removals $receipts_dir $verbose $noisy_warnings);
|
||||
use vars qw ($suppress_spin $spin_counter $spin_state $spin_slower_downer);
|
||||
use vars qw (%exception_list $gen_dirs @gen_files @rmfiles @rmdirs @rmpkg);
|
||||
use vars qw (%exception_list $gen_dirs @gen_files @rmfiles @rmdirs @rmpkg $privhelper);
|
||||
|
||||
#----------------------------------------------------------------------------------------
|
||||
|
||||
@ -24,6 +24,8 @@ $noisy_warnings = 0;
|
||||
# One of rm -rf in this script uses $receipts_dir -- change with care.
|
||||
$receipts_dir = "/Library/Receipts";
|
||||
|
||||
$privhelper="/Library/LaunchDaemons/privhelper-launchd.plist";
|
||||
|
||||
%exception_list = (
|
||||
);
|
||||
|
||||
@ -38,6 +40,8 @@ $gen_dirs = { };
|
||||
"/var/db/openafs/etc/CellServDB.master.last",
|
||||
"/var/db/openafs/etc/CellServDB",
|
||||
"/var/db/openafs/etc/config/settings.plist",
|
||||
"/Library/LaunchDaemons/privhelper-launchd.plist",
|
||||
"/Library/PrivilegedHelperTools/org.openafs.privhelper",
|
||||
);
|
||||
|
||||
#----------------------------------------------------------------------------------------
|
||||
@ -73,10 +77,10 @@ sub main {
|
||||
# earlier output. The final 'tr' in the pipeline here turns them back
|
||||
# into \n newlines. pkgutil --forget at least will print output like
|
||||
# "Forgot package 'foo'".
|
||||
my $rmcmd = "osascript -e 'do shell script \"/bin/rm -f @rmfiles; " .
|
||||
"/bin/rmdir @rmdirs; echo @rmpkg | xargs -n 1 " .
|
||||
"/usr/sbin/pkgutil --forget\" with administrator " .
|
||||
"privileges' | tr '\\r' '\\n'";
|
||||
my $rmcmd = "osascript -e 'do shell script \"/bin/launchctl unload -w $privhelper; " .
|
||||
"/bin/rm -f @rmfiles; /bin/rmdir @rmdirs; " .
|
||||
"echo @rmpkg | xargs -n 1 /usr/sbin/pkgutil --forget\" " .
|
||||
"with administrator privileges' | tr '\\r' '\\n'";
|
||||
system $rmcmd;
|
||||
my $retcode = $? >> 8;
|
||||
if ($retcode != 0) {
|
||||
|
@ -1043,6 +1043,17 @@
|
||||
[checkButtonAfsAtBootTime setEnabled:[self isUnlocked]];
|
||||
[installKRB5AuthAtLoginButton setEnabled:[self isUnlocked]];
|
||||
}
|
||||
|
||||
// These are called when the user clicks on the lock icon in the prefpane to
|
||||
// authenticate to make system changes. Store a reference to the authorization
|
||||
// context that we can use for privileged operations.
|
||||
- (void)authorizationViewCreatedAuthorization:(SFAuthorizationView *)view {
|
||||
[[AuthUtil shared] setAuthorization:[[authView authorization] authorizationRef]];
|
||||
}
|
||||
|
||||
- (void)authorizationViewReleasedAuthorization:(SFAuthorizationView *)view {
|
||||
[[AuthUtil shared] setAuthorization:nil];
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AFSCommanderPref (NSTableDataSource)
|
||||
|
@ -12,11 +12,13 @@
|
||||
|
||||
@interface AuthUtil : NSObject {
|
||||
AuthorizationRef authorizationRef;
|
||||
BOOL isAuthorizationRefOwned;
|
||||
}
|
||||
-(id) init;
|
||||
-(OSStatus) autorize;
|
||||
-(BOOL) deautorize;
|
||||
-(AuthorizationRef) authorization;
|
||||
-(void)setAuthorization:(AuthorizationRef)authRef;
|
||||
-(NSData*) extFormAuth;
|
||||
-(OSStatus) execUnixCommand:(const char*) commandPath args:(const char*[])args output:(NSMutableString*)output;
|
||||
+(AuthUtil*) shared;
|
||||
|
@ -76,6 +76,7 @@ static AuthUtil *sharedAuthUtil = nil;
|
||||
if (status != errAuthorizationSuccess) {
|
||||
return status;
|
||||
}
|
||||
isAuthorizationRefOwned = YES;
|
||||
}
|
||||
|
||||
flags = kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights;
|
||||
@ -100,6 +101,7 @@ static AuthUtil *sharedAuthUtil = nil;
|
||||
if(authorizationRef){
|
||||
status = AuthorizationFree (authorizationRef, kAuthorizationFlagDefaults);
|
||||
authorizationRef = 0L;
|
||||
isAuthorizationRefOwned = NO;
|
||||
}
|
||||
return status == noErr;
|
||||
}
|
||||
@ -113,6 +115,14 @@ static AuthUtil *sharedAuthUtil = nil;
|
||||
return authorizationRef;
|
||||
}
|
||||
|
||||
-(void)setAuthorization:(AuthorizationRef)authRef
|
||||
{
|
||||
if (isAuthorizationRefOwned) {
|
||||
[self deautorize];
|
||||
}
|
||||
authorizationRef = authRef;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// authorization:
|
||||
// -------------------------------------------------------------------------------
|
||||
|
@ -30,5 +30,13 @@
|
||||
<string>OpenAFS</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>AFSCommanderPref</string>
|
||||
<!-- Uncomment the following block and add your signature (XXXXXXXXXX) -->
|
||||
<!--
|
||||
<key>SMPrivilegedExecutables</key>
|
||||
<dict>
|
||||
<key>org.openafs.privhelper</key>
|
||||
<string>identifier "org.openafs.privhelper" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = XXXXXXXXXX</string>
|
||||
</dict>
|
||||
-->
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -10,6 +10,8 @@
|
||||
#include <Security/Authorization.h>
|
||||
#include <Security/AuthorizationTags.h>
|
||||
|
||||
#define PRIVHELPER_ID "org.openafs.privhelper"
|
||||
|
||||
@interface TaskUtil : NSObject {
|
||||
}
|
||||
+(NSString*) searchExecutablePath:(NSString*)unixCommand;
|
||||
@ -17,4 +19,5 @@
|
||||
+(NSString*) executeTask:(NSString*) taskName arguments:(NSArray *)args;
|
||||
+(int) executeTaskWithAuth:(NSString*) taskName arguments:(NSArray *)args authExtForm:(NSData*)auth;
|
||||
+(int) executeTaskWithAuth:(NSString*) taskName arguments:(NSArray *)args helper:(NSString *)helper withAuthRef:(AuthorizationRef)authRef;
|
||||
+(int) executePrivTask:(const char *)task;
|
||||
@end
|
||||
|
@ -6,6 +6,9 @@
|
||||
// Copyright 2007 INFN - National Institute of Nuclear Physics. All rights reserved.
|
||||
//
|
||||
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <Security/Authorization.h>
|
||||
|
||||
#import "TaskUtil.h"
|
||||
#import "AuthUtil.h"
|
||||
|
||||
@ -146,4 +149,89 @@
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a task that requires root access via privhelper.
|
||||
*
|
||||
* This performs a task that requires root access, like starting/stopping the
|
||||
* client, or updating configuration files like CellServDB. This function sends
|
||||
* the request to the privhelper daemon, which performs the actual actions for
|
||||
* the task, and waits for a response.
|
||||
*
|
||||
* @param[in] task The name of the task to perform (e.g. "startup_enable").
|
||||
* See privhelper.c:ProcessRequest for what tasks we define.
|
||||
*
|
||||
* @returns Return status of the task
|
||||
* @retval 0 success
|
||||
* @retval -1 internal error
|
||||
* @retval nonzero The return value of system() of a failed command for the
|
||||
* task.
|
||||
*/
|
||||
+(int) executePrivTask:(const char *)task {
|
||||
int status = -1;
|
||||
|
||||
OSErr autherr = [[AuthUtil shared] autorize];
|
||||
if (autherr != noErr) {
|
||||
NSLog(@"executePrivTask: Could not get authorization: %d.", autherr);
|
||||
return status;
|
||||
}
|
||||
|
||||
AuthorizationExternalForm extForm;
|
||||
AuthorizationRef authRef = [[AuthUtil shared] authorization];
|
||||
if (authRef == NULL) {
|
||||
NSLog(@"executePrivTask: Authorization reference is null.");
|
||||
return status;
|
||||
}
|
||||
|
||||
OSStatus code = AuthorizationMakeExternalForm(authRef, &extForm);
|
||||
if (code != errAuthorizationSuccess) {
|
||||
NSLog(@"executePrivTask: Could not serialize authorization: %d.", code);
|
||||
return status;
|
||||
}
|
||||
|
||||
const uint64_t flags = XPC_CONNECTION_MACH_SERVICE_PRIVILEGED;
|
||||
xpc_connection_t conn = xpc_connection_create_mach_service(PRIVHELPER_ID, NULL, flags);
|
||||
if (conn == NULL) {
|
||||
NSLog(@"executePrivTask: Could not create service connection.");
|
||||
return status;
|
||||
}
|
||||
|
||||
xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
|
||||
NSLog(@"executePrivTask: Received unexpected event.");
|
||||
});
|
||||
xpc_connection_resume(conn);
|
||||
|
||||
xpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0);
|
||||
if (msg == NULL) {
|
||||
NSLog(@"executePrivTask: Could not create XPC message.");
|
||||
xpc_connection_cancel(conn);
|
||||
xpc_release(conn);
|
||||
return status;
|
||||
}
|
||||
xpc_dictionary_set_string(msg, "task", task);
|
||||
xpc_dictionary_set_data(msg, "auth", &extForm, sizeof(extForm));
|
||||
|
||||
xpc_object_t reply = xpc_connection_send_message_with_reply_sync(conn, msg);
|
||||
if (reply == NULL) {
|
||||
NSLog(@"executePrivTask: No reply received for task: %s", task);
|
||||
xpc_release(msg);
|
||||
xpc_connection_cancel(conn);
|
||||
xpc_release(conn);
|
||||
return status;
|
||||
}
|
||||
|
||||
xpc_object_t status_obj = xpc_dictionary_get_value(reply, "status");
|
||||
if (status_obj != NULL && xpc_get_type(status_obj) == XPC_TYPE_INT64) {
|
||||
status = (int)xpc_int64_get_value(status_obj);
|
||||
} else {
|
||||
NSLog(@"executePrivTask: Received invalid reply status for task: %s", task);
|
||||
}
|
||||
|
||||
xpc_release(msg);
|
||||
xpc_release(reply);
|
||||
xpc_connection_cancel(conn);
|
||||
xpc_release(conn);
|
||||
|
||||
return status;
|
||||
}
|
||||
@end
|
||||
|
@ -14,7 +14,7 @@ OSXSDK = @OSXSDK@
|
||||
|
||||
.PHONY: all afscell
|
||||
|
||||
all: OpenAFS.prefPane afssettings afscell growlagent aklog.bundle
|
||||
all: OpenAFS.prefPane afssettings afscell growlagent aklog.bundle PrivilegedHelper
|
||||
|
||||
AFSPreference/build/Release/OpenAFS.prefPane: OpenAFS.prefPane
|
||||
afscell/build/Release/afscell.bundle: afscell
|
||||
@ -39,6 +39,9 @@ aklog.bundle:
|
||||
growlagent:
|
||||
cd growlagent && $(MAKE) all
|
||||
|
||||
PrivilegedHelper: OpenAFS.prefPane
|
||||
cd PrivilegedHelper && $(MAKE) all
|
||||
|
||||
afscell:
|
||||
@case ${OSXSDK} in \
|
||||
macosx* ) \
|
||||
@ -53,6 +56,7 @@ afssettings: afssettings.m
|
||||
install: \
|
||||
${DESTDIR}${sbindir}/afssettings
|
||||
cd growlagent && $(MAKE) install
|
||||
cd PrivilegedHelper && $(MAKE) install
|
||||
|
||||
dest: \
|
||||
${DEST}/etc/afssettings \
|
||||
@ -61,6 +65,7 @@ dest: \
|
||||
${DEST}/tools/OpenAFS.prefPane \
|
||||
${DEST}/tools/aklog.bundle
|
||||
cd growlagent && $(MAKE) dest
|
||||
cd PrivilegedHelper && $(MAKE) dest
|
||||
|
||||
${DESTDIR}${sbindir}/afssettings: afssettings
|
||||
${INSTALL} $? $@
|
||||
@ -93,3 +98,4 @@ clean:
|
||||
$(RM) -rf afscell/build
|
||||
$(RM) -f *.o core afssettings AFS_component_version_number.c
|
||||
cd growlagent && $(MAKE) clean
|
||||
cd PrivilegedHelper && $(MAKE) clean
|
||||
|
29
src/platform/DARWIN/PrivilegedHelper/Makefile.in
Normal file
29
src/platform/DARWIN/PrivilegedHelper/Makefile.in
Normal file
@ -0,0 +1,29 @@
|
||||
srcdir=@srcdir@
|
||||
include @TOP_OBJDIR@/src/config/Makefile.config
|
||||
include @TOP_OBJDIR@/src/config/Makefile.pthread
|
||||
|
||||
all: org.openafs.privhelper
|
||||
|
||||
org.openafs.privhelper: privhelper.c
|
||||
$(CC) -O2 -mmacosx-version-min=10.6 \
|
||||
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker privhelper-info.plist \
|
||||
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __launchd_plist -Xlinker privhelper-launchd.plist \
|
||||
-o org.openafs.privhelper privhelper.c -framework Security -framework CoreFoundation
|
||||
|
||||
clean:
|
||||
$(RM) -f *.o org.openafs.privhelper
|
||||
|
||||
PREFPANE_DIR = ${DEST}/tools/OpenAFS.prefPane
|
||||
LAUNCHSERVICES_DIR = $(PREFPANE_DIR)/Contents/Library/LaunchServices
|
||||
PRIVHELPER_TARGET = $(LAUNCHSERVICES_DIR)/org.openafs.privhelper
|
||||
|
||||
install:
|
||||
|
||||
dest: \
|
||||
$(PRIVHELPER_TARGET)
|
||||
|
||||
$(PRIVHELPER_TARGET): org.openafs.privhelper
|
||||
-mkdir -p $(LAUNCHSERVICES_DIR)
|
||||
${INSTALL} org.openafs.privhelper $(LAUNCHSERVICES_DIR)
|
||||
|
||||
include ../../../config/Makefile.version
|
21
src/platform/DARWIN/PrivilegedHelper/privhelper-info.plist
Normal file
21
src/platform/DARWIN/PrivilegedHelper/privhelper-info.plist
Normal file
@ -0,0 +1,21 @@
|
||||
<?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>org.openafs.privhelper</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>privhelper</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<!-- Uncomment the following block and add your signature (XXXXXXXXXX) -->
|
||||
<!--
|
||||
<key>SMAuthorizedClients</key>
|
||||
<array>
|
||||
<string>identifier "it.infn.lnf.network.openafs" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = XXXXXXXXXX</string>
|
||||
</array>
|
||||
-->
|
||||
</dict>
|
||||
</plist>
|
@ -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>org.openafs.privhelper</string>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>org.openafs.privhelper</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
392
src/platform/DARWIN/PrivilegedHelper/privhelper.c
Normal file
392
src/platform/DARWIN/PrivilegedHelper/privhelper.c
Normal file
@ -0,0 +1,392 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Sine Nomine Associates. All Rights Reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR `AS IS'' AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Tool that performs privileged operations on behalf of Prefpane.
|
||||
*/
|
||||
|
||||
#include <syslog.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <xpc/xpc.h>
|
||||
#include <Security/Security.h>
|
||||
#include <Security/Authorization.h>
|
||||
|
||||
#include <mach/message.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
#include <xpc/xpc.h>
|
||||
|
||||
#define PRIVHELPER_ID "org.openafs.privhelper"
|
||||
|
||||
/*
|
||||
* This is the code signing requirement imposed on anyone that connects to our
|
||||
* XPC service.
|
||||
*
|
||||
* com.apple.systempreferences.legacyLoader.{arm64, arm64} and
|
||||
* com.apple.systempreferences.legacyLoader refer to the "legacyloader"
|
||||
* program that runs our prefpane code (signed by apple).
|
||||
*
|
||||
* it.infn.lnf.network.{openafs, AFSMenuExtra, AFSBackgrounder} refers to our
|
||||
* AFSBackgrounder; the menubar provided by it may talk to privhelper.
|
||||
*
|
||||
* "certificate 1[field.1.2.840.113635.100.6.2.6]" means the code signature is
|
||||
* signed by the Apple Developer ID cert authority.
|
||||
*
|
||||
* "certificate leaf[field.1.2.840.113635.100.6.1.13]" means the code is a
|
||||
* Developer ID application.
|
||||
*
|
||||
* "certificate leaf[subject.OU] = @MACOS_TEAM_ID@" means the code was signed
|
||||
* by us.
|
||||
*
|
||||
* Replace @MACOS_TEAM_ID@ by your team ID. For example:
|
||||
* "certificate leaf[subject.OU] = SKMME9E2Y8"
|
||||
*/
|
||||
#define CLI_SIGNATURES "((identifier \"com.apple.systempreferences.legacyLoader.x86_64\" and anchor apple) or " \
|
||||
"(identifier \"com.apple.systempreferences.legacyLoader.arm64\" and anchor apple) or " \
|
||||
"(identifier \"com.apple.systempreferences.legacyLoader\" and anchor apple) or " \
|
||||
"(identifier \"it.infn.lnf.network.openafs\" and anchor apple generic and " \
|
||||
"certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and " \
|
||||
"certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and " \
|
||||
"certificate leaf[subject.OU] = @MACOS_TEAM_ID@) or " \
|
||||
"(identifier \"it.infn.lnf.network.AFSMenuExtra\" and anchor apple generic and " \
|
||||
"certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and " \
|
||||
"certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and " \
|
||||
"certificate leaf[subject.OU] = @MACOS_TEAM_ID@) or " \
|
||||
"(identifier \"it.infn.lnf.network.AFSBackgrounder\" and anchor apple generic and " \
|
||||
"certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and " \
|
||||
"certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and " \
|
||||
"certificate leaf[subject.OU] = @MACOS_TEAM_ID@))"
|
||||
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 120000
|
||||
/* macOS 12.0 introduced xpc_connection_set_peer_code_signing_requirement(). */
|
||||
# define HAVE_XPC_CONNECTION_SET_PEER_CODE_SIGNING_REQUIREMENT
|
||||
#endif
|
||||
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
|
||||
/* macOS 11.0 introduced SecCodeCreateWithXPCMessage(). */
|
||||
# define HAVE_SECCODECREATEWITHXPCMESSAGE
|
||||
#endif
|
||||
|
||||
static void
|
||||
mem_error(void)
|
||||
{
|
||||
syslog(LOG_ERR, "%s: Memory alloc failed", PRIVHELPER_ID);
|
||||
}
|
||||
|
||||
#if !defined(HAVE_SECCODECREATEWITHXPCMESSAGE)
|
||||
/*
|
||||
* We need xpc_dictionary_get_audit_token() to implement our own version of
|
||||
* SecCodeCreateWithXPCMessage(). This isn't declared in public headers, but we
|
||||
* can use it if we declare it ourselves.
|
||||
*/
|
||||
extern void xpc_dictionary_get_audit_token(xpc_object_t, audit_token_t *);
|
||||
|
||||
/*
|
||||
* Our own version of SecCodeCreateWithXPCMessage(), implemented in terms of
|
||||
* xpc_dictionary_get_audit_token() and SecCodeCopyGuestWithAttributes().
|
||||
*
|
||||
* This gets the SecCodeRef of the peer that sent us the XPC message 'event',
|
||||
* and returns it in *a_codeRef on success.
|
||||
*/
|
||||
static OSStatus
|
||||
SecCodeCreateWithXPCMessage(xpc_object_t event, SecCSFlags secflags, SecCodeRef *a_codeRef)
|
||||
{
|
||||
OSStatus status;
|
||||
audit_token_t auditToken;
|
||||
CFDataRef tokenData = NULL;
|
||||
CFDictionaryRef attr = NULL;
|
||||
|
||||
xpc_dictionary_get_audit_token(event, &auditToken);
|
||||
|
||||
tokenData = CFDataCreate(NULL, (const UInt8 *)&auditToken, sizeof(auditToken));
|
||||
if (tokenData == NULL) {
|
||||
mem_error();
|
||||
status = errSecAllocate;
|
||||
goto done;
|
||||
}
|
||||
|
||||
const void *keys[] = {
|
||||
kSecGuestAttributeAudit
|
||||
};
|
||||
const void *values[] = {
|
||||
tokenData
|
||||
};
|
||||
attr = CFDictionaryCreate(NULL, keys, values, 1,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks);
|
||||
if (attr == NULL) {
|
||||
mem_error();
|
||||
status = errSecAllocate;
|
||||
goto done;
|
||||
}
|
||||
|
||||
status = SecCodeCopyGuestWithAttributes(NULL, attr, secflags, a_codeRef);
|
||||
|
||||
done:
|
||||
if (attr != NULL) {
|
||||
CFRelease(attr);
|
||||
}
|
||||
if (tokenData != NULL) {
|
||||
CFRelease(tokenData);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
#endif /* !HAVE_SECCODECREATEWITHXPCMESSAGE */
|
||||
|
||||
static int
|
||||
ProcessRequest(const char *task, xpc_object_t event)
|
||||
{
|
||||
/* Tasks we understand will be added here as they are implemented. */
|
||||
|
||||
syslog(LOG_WARNING, "%s: Received unknown task '%s'", PRIVHELPER_ID, task);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Is the task for the given event authorized to run commands as root? Check
|
||||
* the AuthorizationExternalForm in event["auth"], and see if it is authorized
|
||||
* for running commands as root.
|
||||
*
|
||||
* Returns 1 if the task is authorized, 0 otherwise.
|
||||
*/
|
||||
static int
|
||||
IsEventAuthorized(xpc_object_t event)
|
||||
{
|
||||
OSStatus status;
|
||||
int authorized = 0;
|
||||
|
||||
const void *auth_data;
|
||||
size_t data_size = 0;
|
||||
|
||||
AuthorizationRef authRef;
|
||||
AuthorizationExternalForm extForm;
|
||||
|
||||
/*
|
||||
* Check that the given authRef has the kAuthorizationRightExecute right
|
||||
* (the right to execute commands as root).
|
||||
*/
|
||||
AuthorizationItem authItems[] = {
|
||||
{ kAuthorizationRightExecute, 0, NULL, 0 }
|
||||
};
|
||||
AuthorizationRights rights = {
|
||||
1, authItems
|
||||
};
|
||||
|
||||
auth_data = xpc_dictionary_get_data(event, "auth", &data_size);
|
||||
if (auth_data == NULL || data_size != sizeof(AuthorizationExternalForm)) {
|
||||
syslog(LOG_WARNING, "%s: Authorization not found.", PRIVHELPER_ID);
|
||||
return authorized;
|
||||
}
|
||||
|
||||
memcpy(&extForm, auth_data, sizeof(extForm));
|
||||
|
||||
status = AuthorizationCreateFromExternalForm(&extForm, &authRef);
|
||||
if (status != errAuthorizationSuccess) {
|
||||
syslog(LOG_ERR, "%s: Deserialization failed.\n", PRIVHELPER_ID);
|
||||
return authorized;
|
||||
}
|
||||
|
||||
status = AuthorizationCopyRights(authRef, &rights,
|
||||
kAuthorizationEmptyEnvironment,
|
||||
kAuthorizationFlagDefaults, NULL);
|
||||
AuthorizationFree(authRef, kAuthorizationFlagDefaults);
|
||||
if (status == errAuthorizationSuccess) {
|
||||
authorized = 1;
|
||||
}
|
||||
return authorized;
|
||||
}
|
||||
|
||||
#if defined(HAVE_XPC_CONNECTION_SET_PEER_CODE_SIGNING_REQUIREMENT)
|
||||
|
||||
/*
|
||||
* Set the given code signing requirements for the given service.
|
||||
*
|
||||
* 'req_cf' is the requirement string as a CFStringRef.
|
||||
*/
|
||||
static int
|
||||
SetCodeReq(xpc_connection_t service, CFStringRef req_cf)
|
||||
{
|
||||
const char *req_c = CFStringGetCStringPtr(req_cf, kCFStringEncodingUTF8);
|
||||
if (req_c == NULL) {
|
||||
syslog(LOG_ERR, "%s: Could not convert CFStringRef.", PRIVHELPER_ID);
|
||||
return -1;
|
||||
}
|
||||
if (xpc_connection_set_peer_code_signing_requirement(service, req_c) != 0) {
|
||||
syslog(LOG_ERR, "%s: Could not set peer code signing requirement.", PRIVHELPER_ID);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Is the xpc conn for the given event authorized to connect at all? This
|
||||
* checks whether the peer satisfies the code signing requirements given in
|
||||
* SetCodeReq().
|
||||
*
|
||||
* Returns 1 if the connection is authorized, 0 otherwise.
|
||||
*/
|
||||
static int
|
||||
IsConnAuthorized(xpc_object_t event)
|
||||
{
|
||||
/*
|
||||
* xpc_connection_set_peer_code_signing_requirement() ensures that all
|
||||
* incoming connections and messages are validated against the given
|
||||
* code-signing requirement. So we don't need to do any checks ourselves.
|
||||
*/
|
||||
return 1;
|
||||
}
|
||||
|
||||
#else /* HAVE_XPC_CONNECTION_SET_PEER_CODE_SIGNING_REQUIREMENT */
|
||||
|
||||
/* Our compiled code security requirements, which we use to check every
|
||||
* connection that comes in. */
|
||||
static SecRequirementRef secRequirementRef;
|
||||
|
||||
static int
|
||||
SetCodeReq(xpc_connection_t service, CFStringRef req_cf)
|
||||
{
|
||||
OSStatus code;
|
||||
code = SecRequirementCreateWithString(req_cf,
|
||||
kSecCSDefaultFlags,
|
||||
&secRequirementRef);
|
||||
if (code != errSecSuccess || secRequirementRef == NULL) {
|
||||
syslog(LOG_ERR, "%s: Could not compile code sec requirement.", PRIVHELPER_ID);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
IsConnAuthorized(xpc_object_t event)
|
||||
{
|
||||
SecCodeRef codeRef = NULL;
|
||||
OSStatus status;
|
||||
|
||||
status = SecCodeCreateWithXPCMessage(event, kSecCSDefaultFlags, &codeRef);
|
||||
if (status != errSecSuccess || codeRef == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
status = SecCodeCheckValidity(codeRef, kSecCSDefaultFlags, secRequirementRef);
|
||||
CFRelease(codeRef);
|
||||
if (status != errSecSuccess) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif /* HAVE_XPC_CONNECTION_SET_PEER_CODE_SIGNING_REQUIREMENT */
|
||||
|
||||
static void
|
||||
XPCEventHandler(xpc_connection_t conn, xpc_object_t event)
|
||||
{
|
||||
int status;
|
||||
xpc_type_t type;
|
||||
xpc_object_t reply;
|
||||
xpc_connection_t client;
|
||||
const char *task;
|
||||
|
||||
type = xpc_get_type(event);
|
||||
if (type == XPC_TYPE_ERROR) {
|
||||
syslog(LOG_WARNING, "%s: Could not get event type.", PRIVHELPER_ID);
|
||||
return;
|
||||
}
|
||||
if (!IsConnAuthorized(event)) {
|
||||
syslog(LOG_WARNING, "%s: Connection not authorized.", PRIVHELPER_ID);
|
||||
return;
|
||||
}
|
||||
if (!IsEventAuthorized(event)) {
|
||||
syslog(LOG_WARNING, "%s: Event not authorized.", PRIVHELPER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
task = xpc_dictionary_get_string(event, "task");
|
||||
if (task == NULL) {
|
||||
syslog(LOG_WARNING, "%s: Could not get event task.", PRIVHELPER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
status = ProcessRequest(task, event);
|
||||
|
||||
reply = xpc_dictionary_create_reply(event);
|
||||
if (reply == NULL) {
|
||||
syslog(LOG_ERR, "%s: Could not create reply for event.", PRIVHELPER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
xpc_dictionary_set_int64(reply, "status", status);
|
||||
|
||||
client = xpc_dictionary_get_remote_connection(event);
|
||||
if (client == NULL) {
|
||||
syslog(LOG_ERR, "%s: No remote connection for event.", PRIVHELPER_ID);
|
||||
xpc_release(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
xpc_connection_send_message(client, reply);
|
||||
xpc_release(reply);
|
||||
}
|
||||
|
||||
static void
|
||||
XPCConnectionHandler(xpc_connection_t conn)
|
||||
{
|
||||
xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
|
||||
XPCEventHandler(conn, event);
|
||||
});
|
||||
xpc_connection_resume(conn);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
xpc_connection_t service;
|
||||
dispatch_queue_t targetq = dispatch_get_main_queue();
|
||||
uint64_t flags = XPC_CONNECTION_MACH_SERVICE_LISTENER;
|
||||
|
||||
service = xpc_connection_create_mach_service(PRIVHELPER_ID, targetq, flags);
|
||||
if (service == NULL) {
|
||||
syslog(LOG_ERR, "%s: Could not create service.", PRIVHELPER_ID);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (SetCodeReq(service, CFSTR(CLI_SIGNATURES)) != 0) {
|
||||
xpc_release(service);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
xpc_connection_set_event_handler(service, ^(xpc_object_t conn) {
|
||||
XPCConnectionHandler(conn);
|
||||
});
|
||||
xpc_connection_resume(service);
|
||||
|
||||
dispatch_main();
|
||||
xpc_release(service);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
Loading…
Reference in New Issue
Block a user