diff --git a/lib/devicetrust/native/device_darwin.go b/lib/devicetrust/native/device_darwin.go index b16f02988ee09..64f9700573bc3 100644 --- a/lib/devicetrust/native/device_darwin.go +++ b/lib/devicetrust/native/device_darwin.go @@ -22,12 +22,21 @@ package native import "C" import ( + "bytes" "crypto/sha256" "crypto/x509" + "errors" + "fmt" + "io/fs" + "os/exec" + "os/user" + "strings" + "sync" "unsafe" "github.com/google/uuid" "github.com/gravitational/trace" + log "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/timestamppb" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" @@ -91,21 +100,107 @@ func pubKeyToCredential(id string, pubKeyRaw []byte) (*devicepb.DeviceCredential func collectDeviceData() (*devicepb.DeviceCollectedData, error) { var dd C.DeviceData - defer func() { C.free(unsafe.Pointer(dd.serial_number)) }() + defer func() { + C.free(unsafe.Pointer(dd.serial_number)) + C.free(unsafe.Pointer(dd.model)) + C.free(unsafe.Pointer(dd.os_version_string)) + }() if res := C.DeviceCollectData(&dd); res != 0 { return nil, trace.Wrap(statusErrorFromC(res)) } + osUser, err := user.Current() + if err != nil { + return nil, trace.Wrap(err, "reading current user") + } + + // Run exec-ed commands concurrently. + var wg sync.WaitGroup + // Note: We could read the OS build from dd.os_version_string, but this + // requires no string parsing. + var osBuild, jamfVersion, macosEnrollmentProfiles string + for _, spec := range []struct { + fn func() (string, error) + out *string + desc string + }{ + {fn: getOSBuild, out: &osBuild, desc: "macOS build"}, + {fn: getJamfBinaryVersion, out: &jamfVersion, desc: "Jamf version"}, + {fn: getMacosEnrollmentProfiles, out: &macosEnrollmentProfiles, desc: "macOs enrollment profiles"}, + } { + spec := spec + wg.Add(1) + go func() { + defer wg.Done() + out, err := spec.fn() + if err != nil { + log.WithError(err).Warnf("Device Trust: Failed to get %v", spec.desc) + return + } + *spec.out = out + }() + } + wg.Wait() + sn := C.GoString(dd.serial_number) return &devicepb.DeviceCollectedData{ - CollectTime: timestamppb.Now(), - OsType: devicepb.OSType_OS_TYPE_MACOS, - SerialNumber: sn, - SystemSerialNumber: sn, + CollectTime: timestamppb.Now(), + OsType: devicepb.OSType_OS_TYPE_MACOS, + SerialNumber: sn, + ModelIdentifier: C.GoString(dd.model), + OsVersion: fmt.Sprintf("%v.%v.%v", dd.os_major, dd.os_minor, dd.os_patch), + OsBuild: osBuild, + OsUsername: osUser.Username, + JamfBinaryVersion: jamfVersion, + MacosEnrollmentProfiles: macosEnrollmentProfiles, + SystemSerialNumber: sn, }, nil } +func getOSBuild() (string, error) { + cmd := exec.Command("/usr/bin/sw_vers", "-buildVersion") + out, err := cmd.Output() + if err != nil { + return "", trace.Wrap(err, "running sw_vers -buildVersion") + } + return string(bytes.TrimSpace(out)), nil +} + +func getJamfBinaryVersion() (string, error) { + // See https://learn.jamf.com/bundle/jamf-pro-documentation-current/page/Components_Installed_on_Managed_Computers.html + cmd := exec.Command("/usr/local/bin/jamf", "version") + out, err := cmd.Output() + if err != nil { + // Jamf binary may not exist. This is alright. + pathErr := &fs.PathError{} + if errors.As(err, &pathErr) { + log.Debugf("Device Trust: Jamf binary not found: %q", pathErr.Path) + return "", nil + } + + return "", trace.Wrap(err, "running jamf version") + } + + // Eg: "version=10.46.1-t1683911857" + s := string(bytes.TrimSpace(out)) + tmp := strings.Split(s, "=") + if len(tmp) != 2 { + return "", fmt.Errorf("unexpected jamf version string: %q", s) + } + + return string(tmp[1]), nil +} + +func getMacosEnrollmentProfiles() (string, error) { + cmd := exec.Command("/usr/bin/profiles", "status", "-type", "enrollment") + out, err := cmd.Output() + if err != nil { + return "", trace.Wrap(err, "running /usr/bin/profiles status -type enrollment") + } + return string(bytes.TrimSpace(out)), nil +} + func signChallenge(chal []byte) (sig []byte, err error) { h := sha256.Sum256(chal) digC := C.Digest{ diff --git a/lib/devicetrust/native/device_darwin.h b/lib/devicetrust/native/device_darwin.h index 3bb1774142b0a..23302c3b51946 100644 --- a/lib/devicetrust/native/device_darwin.h +++ b/lib/devicetrust/native/device_darwin.h @@ -43,7 +43,19 @@ int32_t DeviceKeySign(Digest digest, Signature *sigOut); // DeviceData contains collected data for the device in use. typedef struct _DeviceData { + // Mac system serial number. + // Example: "C02FP3EXXXXX". const char *serial_number; + // Mac device model. + // See https://support.apple.com/en-us/HT201608. + // Example: "MacBookPro16,1". + const char *model; + // OS version "string", as acquired from NSProcessInfo. + // Example: "Version 13.4 (Build 22F66)". + const char *os_version_string; + int64_t os_major; + int64_t os_minor; + int64_t os_patch; } DeviceData; // DeviceCollectData collects data for the device in use. diff --git a/lib/devicetrust/native/device_darwin.m b/lib/devicetrust/native/device_darwin.m index fec6dcfda65df..f4316d17ac07f 100644 --- a/lib/devicetrust/native/device_darwin.m +++ b/lib/devicetrust/native/device_darwin.m @@ -237,9 +237,33 @@ int32_t DeviceKeySign(Digest digest, Signature *sigOut) { return res; } +// Duplicate a CFString or CFData `ref` as a C string. +const char *refToCString(CFTypeRef ref) { + NSData *data = NULL; // managed by ARC. + NSString *str = NULL; // managed by ARC. + CFTypeID id; + + if (!ref) { + return NULL; + } + + id = CFGetTypeID(ref); + if (id == CFStringGetTypeID()) { + str = (__bridge NSString *)ref; + } else if (id == CFDataGetTypeID()) { + data = (__bridge NSData *)ref; + str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + } else { + return NULL; + } + + return strdup([str UTF8String]); +} + int32_t DeviceCollectData(DeviceData *out) { - CFStringRef cfSerialNumber = NULL; // managed via bridge - NSString *serialNumber = NULL; // managed by ARC + CFMutableDictionaryRef cfIODict = NULL; // manually released + NSProcessInfo *info = NULL; // managed by ARC + NSOperatingSystemVersion osVersion; int32_t res = 0; io_service_t platformExpert = IOServiceGetMatchingService( @@ -249,17 +273,32 @@ int32_t DeviceCollectData(DeviceData *out) { goto end; } - cfSerialNumber = IORegistryEntryCreateCFProperty( - platformExpert, CFSTR(kIOPlatformSerialNumberKey), kCFAllocatorDefault, - 0 /* options */); - if (!cfSerialNumber) { + // For a quick reference, see `ioreg -c IOPlatformExpertDevice -d 2`. + IORegistryEntryCreateCFProperties(platformExpert, &cfIODict, + kCFAllocatorDefault, 0 /* options */); + if (!cfIODict) { res = kErrIORegistryEntryFailed; goto end; } - serialNumber = (__bridge_transfer NSString *)cfSerialNumber; - out->serial_number = strdup([serialNumber UTF8String]); + + // Serial number and model from IORegistry. + out->serial_number = refToCString( + CFDictionaryGetValue(cfIODict, CFSTR(kIOPlatformSerialNumberKey))); + out->model = refToCString(CFDictionaryGetValue(cfIODict, CFSTR("model"))); + + // OS version numbers. + info = [NSProcessInfo processInfo]; + osVersion = [info operatingSystemVersion]; + out->os_version_string = + strdup([[info operatingSystemVersionString] UTF8String]); + out->os_major = osVersion.majorVersion; + out->os_minor = osVersion.minorVersion; + out->os_patch = osVersion.patchVersion; end: + if (cfIODict) { + CFRelease(cfIODict); + } if (platformExpert) { IOObjectRelease(platformExpert); }