VNet daemon: Set code signing requirements (#44639)

* Move Go error definitions to common_darwin.go

Both client_darwin.go and serivce_darwin.go are going to use them,
so it doesn't make sense to keep them in either of those files or scattered
across them.

* Roll protocol_darwin into common_darwin

There's little need for those two files to be kept separate.

* Add function for getting code signing requirement

Both the client and the daemon are going to set a requirement towards
each other, hence why this function is in common.

* Set code signing requirement towards daemon client

* Set code signing requirement towards daemon service

* Add info about mdfind to README

* Keep #cgo declarations in a single file

* Fix typos

* Move const to where it's used
This commit is contained in:
Rafał Cieślak
2024-07-29 14:09:08 +00:00
committed by GitHub
parent 87724d5499
commit d9c645d414
12 changed files with 292 additions and 82 deletions
+9
View File
@@ -181,6 +181,15 @@ launch the daemon with the following error:
After resetting the db and restarting the device, everything seemed to be working again.
In theory, it's possible to list all app bundles with a certain bundle identifier by running the
following command:
```
mdfind kMDItemCFBundleIdentifier = "com.goteleport.tshdev"
```
In practice, getting rid of all but one bundle didn't appear to solve the problem.
### Daemon does not start
List all jobs loaded into launchd. The second column is the status which you can then inspect.
+23 -12
View File
@@ -19,11 +19,9 @@
package daemon
// #cgo CFLAGS: -Wall -xobjective-c -fblocks -fobjc-arc -mmacosx-version-min=10.15
// #cgo LDFLAGS: -framework Foundation -framework ServiceManagement
// #include <stdlib.h>
// #include "client_darwin.h"
// #include "protocol_darwin.h"
// #include "common_darwin.h"
import "C"
import (
@@ -304,6 +302,28 @@ func startByCalling(ctx context.Context, bundlePath string, config Config) error
return
}
if errorDomain == nsCocoaErrorDomain && errorCode == errorCodeNSXPCConnectionInterrupted {
const clientNSXPCConnectionInterruptedDebugMsg = "The connection was interrupted when trying to " +
"reach the XPC service. If there's no clear error logs on the daemon side, it might mean that " +
"the client does not satisfy the code signing requirement enforced by the daemon. " +
"Start capturing logs in Console.app and repeat the scenario. Look for " +
"\"xpc_support_check_token: <private> error: <private> status: -67050\" in the logs to verify " +
"that the connection was interrupted due to the code signing requirement."
log.DebugContext(ctx, clientNSXPCConnectionInterruptedDebugMsg)
errC <- trace.Wrap(errXPCConnectionInterrupted)
return
}
if errorDomain == vnetErrorDomain && errorCode == errorCodeMissingCodeSigningIdentifiers {
errC <- trace.Wrap(errMissingCodeSigningIdentifiers)
return
}
if errorDomain == nsCocoaErrorDomain && errorCode == errorCodeNSXPCConnectionCodeSigningRequirementFailure {
errC <- trace.Wrap(errXPCConnectionCodeSigningRequirementFailure, "the daemon does not appear to be code signed correctly")
return
}
errC <- trace.Errorf("could not start VNet daemon: %v", C.GoString(res.error_description))
return
}
@@ -319,15 +339,6 @@ func startByCalling(ctx context.Context, bundlePath string, config Config) error
}
}
var (
// vnetErrorDomain is a custom error domain used for Objective-C errors that pertain to VNet.
vnetErrorDomain = C.GoString(C.VNEErrorDomain)
// errorCodeAlreadyRunning is returned within [vnetErrorDomain] errors to indicate that the daemon
// received a message to start after it was already running.
errorCodeAlreadyRunning = int(C.VNEAlreadyRunningError)
errAlreadyRunning = errors.New("VNet is already running")
)
func sleepOrDone(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
+7 -3
View File
@@ -2,7 +2,6 @@
#define TELEPORT_LIB_VNET_DAEMON_CLIENT_DARWIN_H_
#include "common_darwin.h"
#include "protocol_darwin.h"
#import <Foundation/Foundation.h>
@@ -44,10 +43,15 @@ typedef struct StartVnetRequest {
typedef struct StartVnetResult {
bool ok;
// error_domain is either VNEErrorDomain, NSOSStatusErrorDomain, or NSCocoaErrorDomain.
const char *error_domain;
// error_code is code taken from an NSError instance encountered during the call to StartVnet.
// If ok is false, error_code is greater than zero and identifies the reason behind the error.
// If error_domain is set to VNEErrorDomain, error_code is one of the VNE codes from common_darwin.h.
// If error_domain is NSOSStatusErrorDomain, error_code comes from OSStatus of Code Signing framework.
// https://developer.apple.com/documentation/security/1574088-code_signing_services_result_cod?language=objc
// If error_domain is NSCocoaErrorDomain, it's likely to be about XPC. It's best to inspect it
// on https://osstatus.com in that case.
int error_code;
// error_description includes the full representation of the error, including domain and code.
const char *error_description;
} StartVnetResult;
+22 -3
View File
@@ -19,7 +19,6 @@
#include "client_darwin.h"
#include "common_darwin.h"
#include "protocol_darwin.h"
#import <Foundation/Foundation.h>
#import <ServiceManagement/ServiceManagement.h>
@@ -78,15 +77,17 @@ void OpenSystemSettingsLoginItems(void) {
@property(nonatomic, strong, readwrite) NSXPCConnection *connection;
@property(nonatomic, strong, readonly) NSString *bundlePath;
@property(nonatomic, strong, readonly) NSString *codeSigningRequirement;
@end
@implementation VNEDaemonClient
- (id)initWithBundlePath:(NSString *)bundlePath {
- (id)initWithBundlePath:(NSString *)bundlePath codeSigningRequirement:(NSString *)codeSigningRequirement {
self = [super init];
if (self) {
_bundlePath = bundlePath;
_codeSigningRequirement = codeSigningRequirement;
}
return self;
}
@@ -102,6 +103,12 @@ void OpenSystemSettingsLoginItems(void) {
self->_connection = nil;
};
// The daemon won't even be started on macOS < 13.0, so we don't have to handle the else branch
// of this condition.
if (@available(macOS 13, *)) {
[_connection setCodeSigningRequirement:_codeSigningRequirement];
}
// New connections always start in a suspended state.
[_connection resume];
}
@@ -134,7 +141,18 @@ static VNEDaemonClient *daemonClient = NULL;
void StartVnet(StartVnetRequest *request, StartVnetResult *outResult) {
if (!daemonClient) {
daemonClient = [[VNEDaemonClient alloc] initWithBundlePath:@(request->bundle_path)];
NSString *requirement = nil;
NSError *error = nil;
bool ok = getCodeSigningRequirement(&requirement, &error);
if (!ok) {
outResult->ok = false;
outResult->error_domain = VNECopyNSString([error domain]);
outResult->error_code = (int)[error code];
outResult->error_description = VNECopyNSString([error description]);
return;
}
daemonClient = [[VNEDaemonClient alloc] initWithBundlePath:@(request->bundle_path) codeSigningRequirement:requirement];
}
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
@@ -142,6 +160,7 @@ void StartVnet(StartVnetRequest *request, StartVnetResult *outResult) {
[daemonClient startVnet:request->vnet_config
completion:^(NSError *error) {
if (error) {
outResult->ok = false;
outResult->error_domain = VNECopyNSString([error domain]);
outResult->error_code = (int)[error code];
outResult->error_description = VNECopyNSString([error description]);
+57
View File
@@ -0,0 +1,57 @@
// Teleport
// Copyright (C) 2024 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:build vnetdaemon
// +build vnetdaemon
package daemon
// #cgo CFLAGS: -Wall -xobjective-c -fblocks -fobjc-arc -mmacosx-version-min=10.15
// #cgo LDFLAGS: -framework Foundation -framework ServiceManagement
// #include "common_darwin.h"
import "C"
import (
"errors"
)
var (
// vnetErrorDomain is a custom error domain used for Objective-C errors that pertain to VNet.
vnetErrorDomain = C.GoString(C.VNEErrorDomain)
// errorCodeAlreadyRunning is returned within [vnetErrorDomain] errors to indicate that the daemon
// received a message to start after it was already running.
errorCodeAlreadyRunning = int(C.VNEAlreadyRunningError)
errAlreadyRunning = errors.New("VNet is already running")
// errorCodeMissingCodeSigningIdentifiers is returned within [vnetErrorDomain] Obj-C errors and
// transformed to [errMissingCodeSigningIdentifiers] in Go.
errorCodeMissingCodeSigningIdentifiers = int(C.VNEMissingCodeSigningIdentifiersError)
errMissingCodeSigningIdentifiers = errors.New("either identifier or team identifier is missing in code signing information; is the binary signed?")
)
var (
// nsCocoaErrorDomain is a generic error domain used in a lot of Apple's Cocoa frameworks.
nsCocoaErrorDomain = "NSCocoaErrorDomain"
// https://developer.apple.com/documentation/foundation/1448136-nserror_codes/nsxpcconnectioninterrupted?changes=latest_major&language=objc
errorCodeNSXPCConnectionInterrupted = int(C.NSXPCConnectionInterrupted)
errXPCConnectionInterrupted = errors.New("XPC connection interrupted")
// https://developer.apple.com/documentation/foundation/1448136-nserror_codes/nsxpcconnectioncodesigningrequirementfailure?language=objc
errorCodeNSXPCConnectionCodeSigningRequirementFailure = int(C.NSXPCConnectionCodeSigningRequirementFailure)
errXPCConnectionCodeSigningRequirementFailure = errors.New("code signing requirement failed")
)
+40
View File
@@ -3,6 +3,35 @@
#import <Foundation/Foundation.h>
// VNEErrorDomain is a custom error domain used for Objective-C errors that pertain to VNet.
extern const char* const VNEErrorDomain;
// VNEAlreadyRunningError indicates that the daemon already received a VNet config.
// It won't accept a new one during its lifetime, instead it's expected to stop, after
// which the client might spawn a new instance of the daemon.
extern const int VNEAlreadyRunningError;
// VNEMissingCodeSigningIdentifiersError indicates that either the identifier or the team identifier are missing.
// This can happen if the binary is unsigned, see the docs for SecCodeCopySigningInformation.
// https://developer.apple.com/documentation/security/1395809-seccodecopysigninginformation?language=objc
extern const int VNEMissingCodeSigningIdentifiersError;
typedef struct VnetConfig {
const char *socket_path;
const char *ipv6_prefix;
const char *dns_addr;
const char *home_path;
} VnetConfig;
@protocol VNEDaemonProtocol
// startVnet passes the config back to Go code (which then starts VNet in a separate thread)
// and returns immediately.
//
// Only the first call to this method starts VNet. Subsequent calls return VNEAlreadyRunningError.
// The daemon process exits after VNet is stopped, after which it can be spawned again by calling
// this method.
- (void)startVnet:(VnetConfig *)vnetConfig completion:(void (^)(NSError *error))completion;
@end
// Returns the label for the daemon by getting the identifier of the bundle
// this executable is shipped in and appending ".vnetd" to it.
//
@@ -16,4 +45,15 @@ NSString *DaemonLabel(NSString *bundlePath);
// The caller is expected to free the returned pointer.
const char *VNECopyNSString(NSString *val);
// getCodeSigningRequirement calculates the requirement that will be matched against
// the designated requirement of the app on the other side of an XPC connection.
// It does so based on the code signing information of the current binary, as it assumes that
// both the VNet client and the VNet daemon use the same binary.
//
// On success, it returns true and sets outRequirement.
// On error, it returns false and sets outError. Returns errors of VNEErrorDomain and
// NSOSStatusErrorDomain. Errors with the latter domain are likely to match Code Signing OSStatus values.
// https://developer.apple.com/documentation/security/1574088-code_signing_services_result_cod?language=objc
bool getCodeSigningRequirement(NSString **outRequirement, NSError **outError);
#endif /* TELEPORT_LIB_VNET_DAEMON_COMMON_DARWIN_H_ */
+77
View File
@@ -17,10 +17,17 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <Security/CodeSigning.h>
#include <string.h>
const char* const VNEErrorDomain = "com.Gravitational.Vnet.ErrorDomain";
const int VNEAlreadyRunningError = 1;
const int VNEMissingCodeSigningIdentifiersError = 2;
NSString *DaemonLabel(NSString *bundlePath) {
NSBundle *main = [NSBundle bundleWithPath:bundlePath];
if (!main) {
@@ -41,3 +48,73 @@ const char *VNECopyNSString(NSString *val) {
}
return strdup("");
}
bool getCodeSigningRequirement(NSString **outRequirement, NSError **outError) {
SecCodeRef codeObj = nil;
OSStatus status = SecCodeCopySelf(kSecCSDefaultFlags, &codeObj);
if (status != errSecSuccess) {
if (outError) {
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
}
return false;
}
CFDictionaryRef cfCodeSignInfo = nil;
// kSecCSSigningInformation must be provided as a flag for the team identifier to be included
// in the returned dictionary.
status = SecCodeCopySigningInformation(codeObj, kSecCSSigningInformation, &cfCodeSignInfo);
// codeObj is no longer needed. Manually release it, as we own it since we got it from a function
// with "Copy" in its name.
// https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/writerid/cfCreateRule
CFRelease(codeObj);
if (status != errSecSuccess) {
if (outError) {
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
}
return false;
}
// Transfer ownership of cfCodeSignInfo to Obj-C, which means we don't have to CFRelease it manually.
// We can transfer the ownership of cfCodeSignInfo because we own it (we got it from a function
// with "Copy" in its name).
// https://developer.apple.com/documentation/foundation/1587932-cfbridgingrelease
NSDictionary *codeSignInfo = (NSDictionary *)CFBridgingRelease(cfCodeSignInfo);
// We don't own kSecCodeInfoIdentifier, so we cannot call CFBridgingRelease on it.
// __bridge transfers a pointer between Obj-C and CoreFoundation with no transfer of ownership.
// Values extracted out of codeSignInfo are cast to toll-free bridged Obj-C types.
// https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFDesignConcepts/Articles/tollFreeBridgedTypes.html#//apple_ref/doc/uid/TP40010677-SW2
// https://stackoverflow.com/questions/18067108/when-should-you-use-bridge-vs-cfbridgingrelease-cfbridgingretain
NSString *identifier = codeSignInfo[(__bridge NSString *)kSecCodeInfoIdentifier];
NSString *teamIdentifier = codeSignInfo[(__bridge NSString *)kSecCodeInfoTeamIdentifier];
if (!identifier || [identifier length] == 0 || !teamIdentifier || [teamIdentifier length] == 0) {
if (outError) {
*outError = [NSError errorWithDomain:@(VNEErrorDomain) code:VNEMissingCodeSigningIdentifiersError userInfo:nil];
}
return false;
}
// The requirement will be matched against the designated requirement of the application on
// the other side of an XPC connection. It is based on the designated requirement of tsh.app.
// To inspect the designated requirement of an app, use the following command:
//
// codesign --display -r - <path to app>
//
// Breakdown of individual parts of the requirement:
// * `identifier "foo"` is satisfied if the code signing identifier matches the provided one.
// It is not the same as the bundle identifier.
// * `anchor apple generic` is satisfied by any code signed with any code signing identity issued
// by Apple.
// * `certificate leaf[field(bunch of specific numbers)]` is satisfied by code signed with
// Developer ID Application certs.
// * `certificate leaf[subject.OU]` is satisfied by certs with a specific Team ID.
//
// Read more at:
// https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements#Designated-requirement
// https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements#Xcode-designated-requirement-for-Developer-ID-code
if (outRequirement) {
*outRequirement = [NSString stringWithFormat:@"identifier \"%@\" and anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = %@", identifier, teamIdentifier];
}
return true;
}
-31
View File
@@ -1,31 +0,0 @@
#ifndef TELEPORT_LIB_VNET_DAEMON_PROTOCOL_DARWIN_H_
#define TELEPORT_LIB_VNET_DAEMON_PROTOCOL_DARWIN_H_
#import <Foundation/Foundation.h>
// VNEErrorDomain is a custom error domain used for Objective-C errors that pertain to VNet.
extern const char* const VNEErrorDomain;
// VNEAlreadyRunningError indicates that the daemon already received a VNet config.
// It won't accept a new one during its lifetime, instead it's expected to stop, after
// which the client might spawn a new instance of the daemon.
extern const int VNEAlreadyRunningError;
typedef struct VnetConfig {
const char *socket_path;
const char *ipv6_prefix;
const char *dns_addr;
const char *home_path;
} VnetConfig;
@protocol VNEDaemonProtocol
// startVnet passes the config back to Go code (which then starts VNet in a separate thread)
// and returns immediately.
//
// Only the first call to this method starts VNet. Subsequent calls return VNEAlreadyRunningError.
// The daemon process exits after VNet is stopped, after which it can be spawned again by calling
// this method.
- (void)startVnet:(VnetConfig *)vnetConfig completion:(void (^)(NSError *error))completion;
@end
#endif /* TELEPORT_LIB_VNET_DAEMON_PROTOCOL_DARWIN_H_ */
-22
View File
@@ -1,22 +0,0 @@
//go:build vnetdaemon
// +build vnetdaemon
// Teleport
// Copyright (C) 2024 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
const char* const VNEErrorDomain = "com.Gravitational.Vnet.ErrorDomain";
const int VNEAlreadyRunningError = 1;
+17 -3
View File
@@ -19,8 +19,6 @@
package daemon
// #cgo CFLAGS: -Wall -xobjective-c -fblocks -fobjc-arc -mmacosx-version-min=10.15
// #cgo LDFLAGS: -framework Foundation
// #include <stdlib.h>
// #include "service_darwin.h"
import "C"
@@ -47,7 +45,23 @@ func Start(ctx context.Context, workFn func(context.Context, Config) error) erro
cBundlePath := C.CString(bundlePath)
defer C.free(unsafe.Pointer(cBundlePath))
C.DaemonStart(cBundlePath)
var result C.DaemonStartResult
defer func() {
C.free(unsafe.Pointer(result.error_domain))
C.free(unsafe.Pointer(result.error_description))
}()
C.DaemonStart(cBundlePath, &result)
if !result.ok {
errorDomain := C.GoString(result.error_domain)
errorCode := int(result.error_code)
if errorDomain == vnetErrorDomain && errorCode == errorCodeMissingCodeSigningIdentifiers {
return trace.Wrap(errMissingCodeSigningIdentifiers)
}
return trace.Errorf("could not start daemon: %s", C.GoString(result.error_description))
}
defer func() {
log.InfoContext(ctx, "Stopping daemon")
C.DaemonStop()
+16 -2
View File
@@ -5,7 +5,7 @@
@interface VNEDaemonService : NSObject
- (id)initWithBundlePath:(NSString *)bundlePath;
- (id)initWithBundlePath:(NSString *)bundlePath codeSigningRequirement:(NSString *)codeSigningRequirement;
// start begins listening for incoming XPC connections.
- (void)start;
@@ -19,9 +19,23 @@
@end
typedef struct DaemonStartResult {
bool ok;
// error_domain is set to either VNEErrorDomain or NSOSStatusErrorDomain if ok is false.
const char *error_domain;
// If error_domain is set to VNEErrorDomain, error_code is one of the VNE codes from common_darwin.h.
// If error_domain is NSOSStatusErrorDomain, error_code comes from OSStatus of Code Signing framework.
// https://developer.apple.com/documentation/security/1574088-code_signing_services_result_cod?language=objc
int error_code;
// error_description includes the full representation of the error, including domain and code.
const char *error_description;
} DaemonStartResult;
// DaemonStart initializes the XPC service and starts listening for new connections.
// It's expected to be called only once, noop if the daemon was already started.
void DaemonStart(const char *bundle_path);
// It might fail if it runs into problems with Code Signing APIs while calucating the code signing
// requirement. In such case, outResult.ok is set to false and the error fields are populated.
void DaemonStart(const char *bundle_path, DaemonStartResult *outResult);
// DaemonStop stops the XPC service. Noop if DaemonStart wasn't called.
void DaemonStop(void);
+24 -6
View File
@@ -18,7 +18,6 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "common_darwin.h"
#include "protocol_darwin.h"
#include "service_darwin.h"
#import <Foundation/Foundation.h>
@@ -44,14 +43,19 @@
@implementation VNEDaemonService
- (id)initWithBundlePath:(NSString *)bundlePath {
- (id)initWithBundlePath:(NSString *)bundlePath codeSigningRequirement:(NSString *)codeSigningRequirement {
self = [super init];
if (self) {
// Launch daemons must configure their listener with the machServiceName
// initializer.
// Launch daemons must configure their listener with the machServiceName initializer.
_listener = [[NSXPCListener alloc] initWithMachServiceName:DaemonLabel(bundlePath)];
_listener.delegate = self;
// The daemon won't even be started on macOS < 13.0, so we don't have to handle the else branch
// of this condition.
if (@available(macOS 13, *)) {
[_listener setConnectionCodeSigningRequirement:codeSigningRequirement];
}
_started = NO;
_gotVnetConfigSema = dispatch_semaphore_create(0);
}
@@ -126,12 +130,26 @@
static VNEDaemonService *daemonService = NULL;
void DaemonStart(const char *bundle_path) {
void DaemonStart(const char *bundle_path, DaemonStartResult *outResult) {
if (daemonService) {
outResult->ok = true;
return;
}
daemonService = [[VNEDaemonService alloc] initWithBundlePath:@(bundle_path)];
NSString *requirement = nil;
NSError *error = nil;
bool ok = getCodeSigningRequirement(&requirement, &error);
if (!ok) {
outResult->ok = false;
outResult->error_domain = VNECopyNSString([error domain]);
outResult->error_code = (int)[error code];
outResult->error_description = VNECopyNSString([error description]);
return;
}
daemonService = [[VNEDaemonService alloc] initWithBundlePath:@(bundle_path) codeSigningRequirement:requirement];
[daemonService start];
outResult->ok = true;
}
void DaemonStop(void) {