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:
Marcio Barbosa 2024-11-23 06:46:52 -08:00 committed by Andrew Deason
parent 13ef511544
commit 120871f03f
16 changed files with 637 additions and 6 deletions

View File

@ -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 \

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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) {

View File

@ -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)

View File

@ -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;

View File

@ -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:
// -------------------------------------------------------------------------------

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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>

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>org.openafs.privhelper</string>
<key>MachServices</key>
<dict>
<key>org.openafs.privhelper</key>
<true/>
</dict>
</dict>
</plist>

View 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;
}