Skip to content

Commit b6b0cff

Browse files
authored
VPN-7142: Directly install daemon for macOS<13 (#10662) (#10664)
* Fix double-free in macOS 11/12 XPC auth check * Install the daemon directly instead of using a helper script * Generate codesign for wireguard-go * Add Logger support for CFErrorRef * Fix wireguard-go path lookup on macOS < 13 * Enforce codesign on wireguard-go * Verify that wireguard-go was signed by the same common name
1 parent 5f2fa2a commit b6b0cff

File tree

8 files changed

+169
-32
lines changed

8 files changed

+169
-32
lines changed

macos/pkg/scripts/postinstall.in

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,27 +79,8 @@ fi
7979
# of the SMAppService interface.
8080
if [ ${OSX_MAJOR_VER} -lt 13 ]; then
8181
echo "Installing privileged helper tool"
82-
if [ ! -d /Library/PrivilegedHelperTools ]; then
83-
mkdir -m 755 /Library/PrivilegedHelperTools
84-
chown root:wheel /Library/PrivilegedHelperTools
85-
fi
86-
touch $DAEMON_HELPER_TOOL
87-
chown root:wheel $DAEMON_HELPER_TOOL
88-
chmod 744 $DAEMON_HELPER_TOOL
89-
cat << EOF > $DAEMON_HELPER_TOOL
90-
#!/usr/bin/env bash
91-
CODESIGN_REQS="anchor apple generic and identifier ${CODESIGN_APP_IDENTIFIER}"
92-
CODESIGN_REQS+=" and certificate 1[field.${APPLE_OID_EXT_CA_INTERMEDIATE}] /* exists */"
93-
CODESIGN_REQS+=" and certificate leaf[field.${APPLE_OID_EXT_APP_CODESIGNING}] /* exists */"
94-
CODESIGN_REQS+=" and certificate leaf[subject.OU] = \"${CODESIGN_TEAM_IDENTIFIER}\""
95-
if ! codesign -v --verbose=4 -R="\${CODESIGN_REQS}" "${APP_DIR}"; then
96-
echo "Codesign failed! Aborting."
97-
exit 1
98-
fi
99-
100-
"${APP_DIR}/Contents/Library/LaunchServices/${CODESIGN_APP_IDENTIFIER}.daemon" "\$@"
101-
EOF
102-
chmod u-w $DAEMON_HELPER_TOOL
82+
install -d -m 755 -o root -g wheel /Library/PrivilegedHelperTools
83+
install "${APP_DIR}/Contents/Library/LaunchServices/${CODESIGN_APP_IDENTIFIER}.daemon" /Library/PrivilegedHelperTools
10384

10485
# Modify the SMAppService plist for use as a legacy daemon.
10586
echo "Loading the Daemon at $DAEMON_PLIST_PATH"

src/cmake/macos.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ else()
129129
)
130130
endif()
131131
add_dependencies(mozillavpn build_wireguard_go)
132+
osx_codesign_target(build_wireguard_go FILES ${CMAKE_CURRENT_BINARY_DIR}/wireguard-go)
132133
osx_bundle_files(mozillavpn
133134
FILES ${CMAKE_CURRENT_BINARY_DIR}/wireguard-go
134135
DESTINATION Resources/utils

src/platforms/macos/daemon/wireguardutilsmacos.cpp

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
44

55
#include "wireguardutilsmacos.h"
66

7+
#include <Security/SecCertificate.h>
8+
#include <Security/SecRequirement.h>
9+
#include <Security/SecStaticCode.h>
10+
#include <Security/SecTask.h>
711
#include <errno.h>
812
#include <net/route.h>
913

1014
#include <QByteArray>
1115
#include <QDir>
1216
#include <QFile>
1317
#include <QLocalSocket>
18+
#include <QSysInfo>
1419
#include <QTimer>
20+
#include <QVersionNumber>
1521

1622
#include "leakdetector.h"
1723
#include "logger.h"
@@ -65,6 +71,144 @@ void WireguardUtilsMacos::tunnelFinished(int exitCode,
6571
}
6672
}
6773

74+
// static
75+
QString WireguardUtilsMacos::wireguardGoPath() {
76+
QString osVersion = QSysInfo::productVersion();
77+
if (QVersionNumber::fromString(osVersion) >= QVersionNumber(13, 0)) {
78+
// For macOS 13 and later this can be a relative path to the daemon.
79+
QDir appPath(QCoreApplication::applicationDirPath());
80+
appPath.cdUp();
81+
appPath.cdUp();
82+
appPath.cd("Resources");
83+
appPath.cd("utils");
84+
return appPath.filePath("wireguard-go");
85+
}
86+
87+
// For earlier versions of macOS - this must be a fixed path
88+
return "/Applications/Mozilla VPN.app/Contents/Resources/utils/wireguard-go";
89+
}
90+
91+
// static
92+
QString WireguardUtilsMacos::wireguardGoRequirements() {
93+
static QString requirements;
94+
if (!requirements.isEmpty()) {
95+
return requirements;
96+
}
97+
98+
OSStatus status = errSecSuccess;
99+
SecCodeRef code = nullptr;
100+
CFDictionaryRef dict = nullptr;
101+
auto guard = qScopeGuard([&]() {
102+
if (status != errSecSuccess) {
103+
CFStringRef msg = SecCopyErrorMessageString(status, nullptr);
104+
logger.warning() << "Requirements failed:" << msg;
105+
CFRelease(msg);
106+
}
107+
CFRelease(code);
108+
CFRelease(dict);
109+
});
110+
111+
status = SecCodeCopySelf(kSecCSDefaultFlags, &code);
112+
if (status != errSecSuccess) {
113+
return QString();
114+
}
115+
status = SecCodeCopySigningInformation(code, kSecCSSigningInformation, &dict);
116+
if (status != errSecSuccess) {
117+
return QString();
118+
}
119+
120+
// Build the signing requirements.
121+
QStringList reqList("anchor apple generic");
122+
CFTypeRef value;
123+
value = CFDictionaryGetValue(dict, kSecCodeInfoTeamIdentifier);
124+
if ((value != nullptr) && (CFGetTypeID(value) == CFStringGetTypeID())) {
125+
QString team = QString::fromCFString(static_cast<CFStringRef>(value));
126+
reqList << QString("certificate leaf[subject.OU] = \"%1\"").arg(team);
127+
}
128+
129+
value = CFDictionaryGetValue(dict, kSecCodeInfoCertificates);
130+
if ((value != nullptr) && (CFGetTypeID(value) == CFArrayGetTypeID()) &&
131+
(CFArrayGetCount(static_cast<CFArrayRef>(value)) != 0)) {
132+
CFTypeRef leaf = CFArrayGetValueAtIndex(static_cast<CFArrayRef>(value), 0);
133+
if ((leaf != nullptr) && (CFGetTypeID(leaf) == SecCertificateGetTypeID())) {
134+
CFStringRef name;
135+
QString nameReqTemplate = "certificate leaf[subject.CN] = \"%1\"";
136+
SecCertificateCopyCommonName((SecCertificateRef)leaf, &name);
137+
reqList << nameReqTemplate.arg(QString::fromCFString(name));
138+
CFRelease(name);
139+
}
140+
}
141+
142+
requirements = reqList.join(" and ");
143+
return requirements;
144+
}
145+
146+
// static
147+
bool WireguardUtilsMacos::wireguardGoCodesign(const QProcess& process) {
148+
// No need to verify the codesign on macOS >= 13.0 as the daemon is running
149+
// as a part of the bundle, so we ought to get codesign verification for free
150+
QString osVersion = QSysInfo::productVersion();
151+
if (QVersionNumber::fromString(osVersion) >= QVersionNumber(13, 0)) {
152+
return true;
153+
}
154+
155+
QString requirements = wireguardGoRequirements();
156+
if (requirements.isEmpty()) {
157+
// If the daemon is not signed, then we shouldn't expect wireguard-go to be.
158+
return true;
159+
}
160+
161+
OSStatus status = errSecSuccess;
162+
CFErrorRef err = nullptr;
163+
CFURLRef url = nullptr;
164+
SecRequirementRef req = nullptr;
165+
SecStaticCodeRef code = nullptr;
166+
auto guard = qScopeGuard([&]() {
167+
if (err != nullptr) {
168+
logger.warning() << "Codesign failed:" << err;
169+
CFRelease(err);
170+
} else if (status != errSecSuccess) {
171+
CFStringRef msg = SecCopyErrorMessageString(status, nullptr);
172+
logger.warning() << "Codesign failed:" << msg;
173+
CFRelease(msg);
174+
}
175+
CFRelease(url);
176+
CFRelease(req);
177+
CFRelease(code);
178+
});
179+
180+
// Get the URL to the QProcess's program
181+
CFStringRef urlString = process.program().toCFString();
182+
url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, urlString,
183+
kCFURLPOSIXPathStyle, false);
184+
CFRelease(urlString);
185+
if (!url) {
186+
logger.warning() << "Unable to generate URL for" << process.program();
187+
return false;
188+
}
189+
logger.debug() << "Codesign verifying:" << CFURLGetString(url);
190+
logger.debug() << "Codesign requirements:" << requirements;
191+
192+
// Prepare the codesign requirements.
193+
CFStringRef reqString = requirements.toCFString();
194+
status = SecRequirementCreateWithString(reqString, kSecCSDefaultFlags, &req);
195+
CFRelease(reqString);
196+
if (status != errSecSuccess) {
197+
return false;
198+
}
199+
200+
// Validate the codesign.
201+
logger.debug() << "Codesign get code object";
202+
status = SecStaticCodeCreateWithPath(url, kSecCSDefaultFlags, &code);
203+
if (status != errSecSuccess) {
204+
return false;
205+
}
206+
logger.debug() << "Codesign verify code object";
207+
status =
208+
SecStaticCodeCheckValidityWithErrors(code, kSecCSDefaultFlags, req, &err);
209+
return (status == errSecSuccess);
210+
}
211+
68212
bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
69213
Q_UNUSED(config);
70214
if (m_tunnel.state() != QProcess::NotRunning) {
@@ -84,14 +228,14 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
84228
pe.insert("LOG_LEVEL", "debug");
85229
#endif
86230
m_tunnel.setProcessEnvironment(pe);
231+
m_tunnel.setProgram(wireguardGoPath());
232+
m_tunnel.setArguments(QStringList({"-f", "utun"}));
233+
if (!wireguardGoCodesign(m_tunnel)) {
234+
logger.error() << "Unable to validate tunnel process code signature";
235+
return false;
236+
}
87237

88-
QDir appPath(QCoreApplication::applicationDirPath());
89-
appPath.cdUp();
90-
appPath.cdUp();
91-
appPath.cd("Resources");
92-
appPath.cd("utils");
93-
QStringList wgArgs = {"-f", "utun"};
94-
m_tunnel.start(appPath.filePath("wireguard-go"), wgArgs);
238+
m_tunnel.start();
95239
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
96240
logger.error() << "Unable to start tunnel process due to timeout";
97241
m_tunnel.kill();

src/platforms/macos/daemon/wireguardutilsmacos.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ class WireguardUtilsMacos final : public WireguardUtils {
4242
QString uapiCommand(const QString& command);
4343
static int uapiErrno(const QString& command);
4444
QString waitForTunnelName(const QString& filename);
45+
static QString wireguardGoPath();
46+
static bool wireguardGoCodesign(const QProcess& process);
47+
static QString wireguardGoRequirements();
4548

4649
QString m_ifname;
4750
QProcess m_tunnel;

src/platforms/macos/daemon/xpcdaemonserver.mm

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ + (NSString*) getTeamIdentifier:(SecTaskRef)task {
141141
CFRelease(error);
142142
return nil;
143143
}
144-
auto guard = qScopeGuard([&]() { CFRelease(result); });
145144

146145
if (CFGetTypeID(result) == CFStringGetTypeID()) {
147146
return static_cast<NSString*>(result);

src/platforms/macos/macoscryptosettings.mm

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,7 @@
156156
CFErrorRef error = nullptr;
157157
CFTypeRef result = SecTaskCopyValueForEntitlement(task, cfName, &error);
158158
if (error != nullptr) {
159-
CFStringRef desc = CFErrorCopyDescription(error);
160-
logger.error() << "Failed to check entitlements:" << desc;
161-
CFRelease(desc);
159+
logger.error() << "Failed to check entitlements:" << error;
162160
CFRelease(error);
163161
}
164162
if (result != nullptr) {

src/utils/logger.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ Logger::Log& Logger::Log::operator<<(CFStringRef t) {
6363
m_data->m_ts << QString::fromCFString(t);
6464
return *this;
6565
}
66+
Logger::Log& Logger::Log::operator<<(CFErrorRef t) {
67+
CFStringRef ref = CFErrorCopyDescription(t);
68+
m_data->m_ts << QString::fromCFString(ref);
69+
CFRelease(ref);
70+
return *this;
71+
}
6672
#endif
6773

6874
QString Logger::sensitive(const QString& input) {

src/utils/logger.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
#include "loglevel.h"
1414

15+
#ifdef Q_OS_APPLE
16+
# include <CoreFoundation/CoreFoundation.h>
17+
#endif
18+
1519
class QJsonObject;
1620

1721
class Logger {
@@ -36,6 +40,7 @@ class Logger {
3640
#ifdef Q_OS_APPLE
3741
Log& operator<<(const NSString* t);
3842
Log& operator<<(CFStringRef t);
43+
Log& operator<<(CFErrorRef t);
3944
#endif
4045

4146
// Q_ENUM

0 commit comments

Comments
 (0)