Skip to content

Commit 6571d15

Browse files
committed
MOLCodesignChecker: extract active-slice identity for universal binaries
Threads cputype/cpusubtype from es_event_exec_t through SNTFileInfo into a new MOLCodesignChecker initializer so signing identity (cdhash, teamID, signingID, certificates) reflects the slice the kernel actually loaded. Without the hint, behavior is unchanged - the new path is opt-in via the new SNTFileInfo exec-event initializer. For per-arch-inconsistent universals, the active slice's identity now populates regardless of cross-slice consistency. The cross-slice check remains as an informational error signal.
1 parent b8ab0d1 commit 6571d15

11 files changed

Lines changed: 412 additions & 14 deletions

Source/common/BUILD

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ package(
1111

1212
licenses(["notice"])
1313

14+
# Expose individual test fixtures for use by tests in other packages.
15+
exports_files(["testdata/cal-yikes-universal_signed"])
16+
17+
filegroup(
18+
name = "testdata_cal_yikes_universal_signed",
19+
srcs = ["testdata/cal-yikes-universal_signed"],
20+
)
21+
1422
proto_library(
1523
name = "santa_proto",
1624
srcs = ["santa.proto"],
@@ -1045,7 +1053,10 @@ santa_unit_test(
10451053
"testdata/BundleExample.app/**",
10461054
"testdata/DirectoryBundle/**",
10471055
]),
1048-
deps = [":SNTFileInfo"],
1056+
deps = [
1057+
":SNTFileInfo",
1058+
":TestUtils",
1059+
],
10491060
)
10501061

10511062
santa_unit_test(

Source/common/MOLCodesignChecker.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
@class MOLCertificate;
1717

1818
#import <Foundation/Foundation.h>
19+
#import <mach/machine.h>
1920

2021
/**
2122
`MOLCodesignChecker` validates a binary (either on-disk or in memory) has been signed
@@ -174,6 +175,30 @@
174175
*/
175176
- (instancetype)initWithBinaryPath:(NSString*)binaryPath fileDescriptor:(int)fileDescriptor;
176177

178+
/**
179+
Initialize with a binary on disk via a caller-supplied file descriptor, with an
180+
active-slice hint for universal binaries.
181+
182+
When `cpuType` is non-sentinel and the binary is fat, the per-slice signing dict
183+
for the matching arch is used to populate `_signingInformation` and `_certificates`,
184+
regardless of cross-slice signing consistency. The per-arch consistency check still
185+
runs and still surfaces `errSecCSSignatureInvalid` on the error param informationally.
186+
187+
When `cpuType == CPU_TYPE_ANY` (and/or for thin binaries) this method behaves
188+
identically to `initWithBinaryPath:fileDescriptor:error:`.
189+
190+
@param binaryPath Path to a binary file on disk (display only when fd is provided).
191+
@param fileDescriptor Open fd to the binary, or `-1` to re-resolve `binaryPath`.
192+
@param cpuType Active-slice CPU type, or `CPU_TYPE_ANY` for no hint.
193+
@param cpuSubtype Active-slice CPU subtype, or `CPU_SUBTYPE_ANY` for no hint.
194+
@param error NSError to be filled in if validation fails.
195+
*/
196+
- (instancetype)initWithBinaryPath:(NSString*)binaryPath
197+
fileDescriptor:(int)fileDescriptor
198+
cpuType:(cpu_type_t)cpuType
199+
cpuSubtype:(cpu_subtype_t)cpuSubtype
200+
error:(NSError**)error;
201+
177202
/**
178203
Initialize with a running binary using its process ID.
179204

Source/common/MOLCodesignChecker.mm

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ @interface MOLCodesignChecker ()
7575

7676
// Cached on-disk binary file descriptor
7777
@property int binaryFileDescriptor;
78+
79+
// Active-slice hint; values <= 0 mean no hint (CPU_TYPE_ANY is -1; ivar zero-default
80+
// from non-fd init paths also fails the > 0 gate in -initWithSecStaticCodeRef:error:).
81+
@property cpu_type_t cpuTypeHint;
82+
@property cpu_subtype_t cpuSubtypeHint;
7883
@end
7984

8085
@implementation MOLCodesignChecker
@@ -98,6 +103,12 @@ - (instancetype)initWithSecStaticCodeRef:(SecStaticCodeRef)codeRef error:(NSErro
98103
}
99104
});
100105

106+
// hintActiveForFat is YES when the caller passed a real arch hint and we're
107+
// looking at a fat binary. In that mode the per-slice dict is the authority
108+
// for _signingInformation/_certificates; the whole-binary fallback below is
109+
// suppressed regardless of whether a matching slice was found.
110+
BOOL hintActiveForFat = NO;
111+
101112
// For static code checks perform additional checks across all slices
102113
if (CFGetTypeID(codeRef) == SecStaticCodeGetTypeID()) {
103114
// Ensure signing is consistent for all architectures.
@@ -106,6 +117,26 @@ - (instancetype)initWithSecStaticCodeRef:(SecStaticCodeRef)codeRef error:(NSErro
106117
NSArray* infos = [self universalSigningInformationForBinaryPath:_binaryPath
107118
fileDescriptor:_binaryFileDescriptor];
108119
if (infos) _universalSigningInformation = infos;
120+
121+
// When the caller provided an active-slice hint and this is a fat binary,
122+
// populate _signingInformation/_certificates from the matching slice.
123+
// The cross-slice consistency check below still runs and still surfaces
124+
// its error informationally; the per-arch identity is already locked in
125+
// via the active-slice extraction. If no slice matches the hint (e.g. a
126+
// PowerPC hint against an i386/x86_64 binary), _signingInformation stays
127+
// nil and hintActiveForFat suppresses the whole-binary fallback below so
128+
// callers see an empty identity for the mismatched arch request.
129+
hintActiveForFat = (_cpuTypeHint > 0 && _universalSigningInformation.count > 0);
130+
if (hintActiveForFat) {
131+
NSDictionary* sliceDict = [self perSliceDictForCpuType:_cpuTypeHint
132+
cpuSubtype:_cpuSubtypeHint];
133+
if (sliceDict.count > 0) {
134+
_signingInformation = sliceDict;
135+
NSArray* certs = sliceDict[(__bridge id)kSecCodeInfoCertificates];
136+
_certificates = [MOLCertificate certificatesFromArray:certs];
137+
}
138+
}
139+
109140
if (infos && ![self allSigningInformationMatches:infos]) {
110141
status = errSecCSSignatureInvalid;
111142
scopedError = ScopedCFError::BridgeRetain([self
@@ -114,9 +145,14 @@ - (instancetype)initWithSecStaticCodeRef:(SecStaticCodeRef)codeRef error:(NSErro
114145
}
115146
}
116147

117-
// Do not set _signingInformation or _certificates for universal binaries with signing issues.
148+
// Do not set _signingInformation or _certificates for universal binaries with
149+
// signing issues EXCEPT when the cputype hint already populated them from the
150+
// active slice (above). Also suppress when hint mode is active but no slice
151+
// matched — callers asked for a specific arch and should not receive the
152+
// whole-binary aggregate identity.
118153
NSError* err = scopedError.BridgeRelease<NSError*>();
119-
if (!([err.domain isEqualToString:kMOLCodesignCheckerErrorDomain] &&
154+
if (!_signingInformation && !hintActiveForFat &&
155+
!([err.domain isEqualToString:kMOLCodesignCheckerErrorDomain] &&
120156
status == errSecCSSignatureInvalid)) {
121157
// Get CFDictionary of signing information for binary
122158
CFDictionaryRef signingDict = NULL;
@@ -154,6 +190,18 @@ - (instancetype)initWithBinaryPath:(NSString*)binaryPath {
154190
- (instancetype)initWithBinaryPath:(NSString*)binaryPath
155191
fileDescriptor:(int)fileDescriptor
156192
error:(NSError**)error {
193+
return [self initWithBinaryPath:binaryPath
194+
fileDescriptor:fileDescriptor
195+
cpuType:CPU_TYPE_ANY
196+
cpuSubtype:CPU_SUBTYPE_ANY
197+
error:error];
198+
}
199+
200+
- (instancetype)initWithBinaryPath:(NSString*)binaryPath
201+
fileDescriptor:(int)fileDescriptor
202+
cpuType:(cpu_type_t)cpuType
203+
cpuSubtype:(cpu_subtype_t)cpuSubtype
204+
error:(NSError**)error {
157205
OSStatus status = errSecSuccess;
158206
SecStaticCodeRef codeRef = NULL;
159207

@@ -177,6 +225,8 @@ - (instancetype)initWithBinaryPath:(NSString*)binaryPath
177225

178226
_binaryPath = binaryPath;
179227
_binaryFileDescriptor = (fileDescriptor != -1) ? fileDescriptor : -1;
228+
_cpuTypeHint = cpuType;
229+
_cpuSubtypeHint = cpuSubtype;
180230

181231
self = [self initWithSecStaticCodeRef:codeRef error:error];
182232
if (codeRef) CFRelease(codeRef); // it was retained above
@@ -415,6 +465,11 @@ - (NSError*)errorWithCode:(OSStatus)code {
415465
return [self errorWithCode:code description:nil];
416466
}
417467

468+
// Reports whether every slice of a universal binary shares the same signing
469+
// identity (cert chain, or "-" for ad-hoc). Callers that care about per-arch
470+
// identity should use the cputype-aware initializer instead; this check is
471+
// retained as an informational signal that surfaces `errSecCSSignatureInvalid`
472+
// in MOL's error domain for cross-slice inconsistencies.
418473
- (BOOL)allSigningInformationMatches:(NSArray*)signingInformation {
419474
NSMutableSet* chains = [NSMutableSet set];
420475
for (NSDictionary* arch in signingInformation) {
@@ -460,6 +515,17 @@ - (NSArray*)universalSigningInformationForBinaryPath:(NSString*)path fileDescrip
460515
return infos.count ? infos : nil;
461516
}
462517

518+
- (NSDictionary*)perSliceDictForCpuType:(cpu_type_t)cpuType cpuSubtype:(cpu_subtype_t)cpuSubtype {
519+
const char* nameRaw = macho_arch_name_for_cpu_type(cpuType, cpuSubtype);
520+
NSString* archName =
521+
nameRaw ? @(nameRaw) : [NSString stringWithFormat:@"%i:%i", cpuType, cpuSubtype];
522+
for (NSDictionary* arch in _universalSigningInformation) {
523+
NSDictionary* dict = arch[archName];
524+
if (dict) return dict;
525+
}
526+
return nil;
527+
}
528+
463529
- (NSString*)architectureString:(struct fat_arch*)fatArch bigEndian:(BOOL)bigEndian {
464530
cpu_type_t cpu = bigEndian ? OSSwapBigToHostInt(fatArch->cputype) : fatArch->cputype;
465531
cpu_subtype_t cpuSub = bigEndian ? OSSwapBigToHostInt(fatArch->cpusubtype) : fatArch->cpusubtype;

Source/common/MOLCodesignCheckerTest.mm

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include <fcntl.h>
1919
#include <mach-o/fat.h>
20+
#include <mach/machine.h>
2021
#include <unistd.h>
2122

2223
#import "Source/common/MOLCodesignChecker.h"
@@ -386,4 +387,131 @@ - (void)testEntitlements {
386387
}];
387388
}
388389

390+
// yikes-universal_signed has i386 + x86_64, both signed with the same cert chain.
391+
- (void)testCpuTypeHint_ConsistentFat_PicksActiveSlice {
392+
NSError* error;
393+
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
394+
NSString* path = [bundle pathForResource:@"yikes-universal_signed" ofType:@""];
395+
396+
MOLCodesignChecker* sutA = [[MOLCodesignChecker alloc] initWithBinaryPath:path
397+
fileDescriptor:-1
398+
cpuType:CPU_TYPE_I386
399+
cpuSubtype:CPU_SUBTYPE_I386_ALL
400+
error:&error];
401+
XCTAssertNotNil(sutA);
402+
XCTAssertNil(error);
403+
XCTAssertGreaterThan(sutA.cdhash.length, 0u);
404+
405+
error = nil;
406+
MOLCodesignChecker* sutB = [[MOLCodesignChecker alloc] initWithBinaryPath:path
407+
fileDescriptor:-1
408+
cpuType:CPU_TYPE_X86_64
409+
cpuSubtype:CPU_SUBTYPE_X86_64_ALL
410+
error:&error];
411+
XCTAssertNotNil(sutB);
412+
XCTAssertNil(error);
413+
XCTAssertGreaterThan(sutB.cdhash.length, 0u);
414+
415+
// Per-slice cdhashes always differ across slices of the same fat file.
416+
XCTAssertNotEqualObjects(sutA.cdhash, sutB.cdhash);
417+
}
418+
419+
// cal-yikes-universal_signed: two different cert chains, one per slice.
420+
- (void)testCpuTypeHint_InconsistentFat_PopulatesActiveSlice_AndSurfacesError {
421+
NSError* error;
422+
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
423+
NSString* path = [bundle pathForResource:@"cal-yikes-universal_signed" ofType:@""];
424+
425+
MOLCodesignChecker* sut = [[MOLCodesignChecker alloc] initWithBinaryPath:path
426+
fileDescriptor:-1
427+
cpuType:CPU_TYPE_I386
428+
cpuSubtype:CPU_SUBTYPE_I386_ALL
429+
error:&error];
430+
XCTAssertNotNil(sut);
431+
// Active-slice signing info is now present despite the per-arch inconsistency.
432+
XCTAssertGreaterThan(sut.cdhash.length, 0u);
433+
XCTAssertNotNil(sut.leafCertificate);
434+
// The consistency-check error still surfaces informationally.
435+
XCTAssertEqual(error.code, errSecCSSignatureInvalid);
436+
XCTAssertEqualObjects(error.domain, kMOLCodesignCheckerErrorDomain);
437+
}
438+
439+
// cal-yikes-universal: unknown-arch slice is cert-signed; i386 slice is unsigned.
440+
// Requesting i386 should yield empty identity with a consistency-check error.
441+
- (void)testCpuTypeHint_InconsistentFat_UnsignedActiveSlice_LeavesEmpty {
442+
NSError* error;
443+
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
444+
NSString* path = [bundle pathForResource:@"cal-yikes-universal" ofType:@""];
445+
446+
MOLCodesignChecker* sut = [[MOLCodesignChecker alloc] initWithBinaryPath:path
447+
fileDescriptor:-1
448+
cpuType:CPU_TYPE_I386
449+
cpuSubtype:CPU_SUBTYPE_I386_ALL
450+
error:&error];
451+
XCTAssertNotNil(sut);
452+
XCTAssertEqual(sut.cdhash.length, 0u);
453+
XCTAssertNil(sut.leafCertificate);
454+
XCTAssertEqual(error.code, errSecCSSignatureInvalid);
455+
XCTAssertEqualObjects(error.domain, kMOLCodesignCheckerErrorDomain);
456+
}
457+
458+
// PowerPC won't be in any modern universal fixture; requesting it should leave empty identity.
459+
- (void)testCpuTypeHint_NoMatchingSlice_LeavesEmpty {
460+
NSError* error;
461+
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
462+
NSString* path = [bundle pathForResource:@"yikes-universal_signed" ofType:@""];
463+
464+
MOLCodesignChecker* sut = [[MOLCodesignChecker alloc] initWithBinaryPath:path
465+
fileDescriptor:-1
466+
cpuType:CPU_TYPE_POWERPC
467+
cpuSubtype:CPU_SUBTYPE_POWERPC_ALL
468+
error:&error];
469+
XCTAssertNotNil(sut);
470+
XCTAssertEqual(sut.cdhash.length, 0u);
471+
XCTAssertNil(sut.leafCertificate);
472+
}
473+
474+
// signed-with-teamid is a thin x86_64 fixture. The cputype hint must be inert
475+
// for thin binaries: behavior must match the no-hint path exactly.
476+
- (void)testCpuTypeHint_ThinBinary_Ignored {
477+
NSError* error;
478+
NSError* errorWithoutHint;
479+
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
480+
NSString* path = [bundle pathForResource:@"signed-with-teamid" ofType:@""];
481+
482+
MOLCodesignChecker* sutNoHint = [[MOLCodesignChecker alloc] initWithBinaryPath:path
483+
error:&errorWithoutHint];
484+
485+
MOLCodesignChecker* sutWithHint =
486+
[[MOLCodesignChecker alloc] initWithBinaryPath:path
487+
fileDescriptor:-1
488+
cpuType:CPU_TYPE_X86_64
489+
cpuSubtype:CPU_SUBTYPE_X86_64_ALL
490+
error:&error];
491+
XCTAssertNotNil(sutNoHint);
492+
XCTAssertNotNil(sutWithHint);
493+
XCTAssertEqualObjects(sutNoHint.cdhash, sutWithHint.cdhash);
494+
XCTAssertEqualObjects(sutNoHint.teamID, sutWithHint.teamID);
495+
XCTAssertEqualObjects(sutNoHint.signingID, sutWithHint.signingID);
496+
}
497+
498+
// CPU_TYPE_ANY must fall back to today's (no-hint) behavior.
499+
- (void)testCpuTypeAny_FallsBackToTodayBehavior {
500+
NSError* errorBaseline;
501+
NSError* errorSentinel;
502+
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
503+
NSString* path = [bundle pathForResource:@"yikes-universal_signed" ofType:@""];
504+
505+
MOLCodesignChecker* sutBaseline = [[MOLCodesignChecker alloc] initWithBinaryPath:path
506+
error:&errorBaseline];
507+
MOLCodesignChecker* sutSentinel = [[MOLCodesignChecker alloc] initWithBinaryPath:path
508+
fileDescriptor:-1
509+
cpuType:CPU_TYPE_ANY
510+
cpuSubtype:CPU_SUBTYPE_ANY
511+
error:&errorSentinel];
512+
XCTAssertEqualObjects(sutBaseline.cdhash, sutSentinel.cdhash);
513+
XCTAssertEqualObjects(sutBaseline.teamID, sutSentinel.teamID);
514+
XCTAssertEqualObjects(sutBaseline.signingID, sutSentinel.signingID);
515+
}
516+
389517
@end

Source/common/SNTFileInfo.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#import <EndpointSecurity/EndpointSecurity.h>
1717
#import <Foundation/Foundation.h>
18+
#import <mach/machine.h>
1819

1920
#import "Source/common/SantaVnode.h"
2021

@@ -44,6 +45,17 @@
4445
///
4546
- (instancetype)initWithEndpointSecurityFile:(const es_file_t*)esFile error:(NSError**)error;
4647

48+
///
49+
/// Initialize with an Endpoint Security `es_event_exec_t`. Captures the active-slice
50+
/// CPU type/subtype from `image_cputype` / `image_cpusubtype` so subsequent
51+
/// `codesignCheckerWithError:` calls return active-slice-correct identity for
52+
/// fat binaries.
53+
///
54+
/// Equivalent to `initWithEndpointSecurityFile:` for thin binaries.
55+
///
56+
- (instancetype)initWithEndpointSecurityExecEvent:(const es_event_exec_t*)esExec
57+
error:(NSError**)error;
58+
4759
///
4860
/// Convenience initializer.
4961
///
@@ -233,6 +245,18 @@
233245
///
234246
@property(readonly) NSFileHandle* fileHandle;
235247

248+
///
249+
/// Active-slice CPU type captured at init time (from `es_event_exec_t.image_cputype`).
250+
/// Defaults to `CPU_TYPE_ANY` for inits without exec-event context.
251+
///
252+
@property(readonly) cpu_type_t cpuType;
253+
254+
///
255+
/// Active-slice CPU subtype captured at init time (from `es_event_exec_t.image_cpusubtype`).
256+
/// Defaults to `CPU_SUBTYPE_ANY` for inits without exec-event context.
257+
///
258+
@property(readonly) cpu_subtype_t cpuSubtype;
259+
236260
///
237261
/// @return Returns an instance of MOLCodeSignChecker initialized with the file's binary path.
238262
/// Both the MOLCodesignChecker and any resulting NSError are cached and returned on subsequent

0 commit comments

Comments
 (0)