spotlight meta data, entitlements, improved code signing
-extracts spotlight metadata -extracts entitlements -improves code-singing checks (mahalo @midnite_runr)
This commit is contained in:
Binary file not shown.
+69
-19
@@ -29,6 +29,24 @@
|
||||
#define EVENT_EXEC 27
|
||||
#define EVENT_SPAWN 43190
|
||||
|
||||
//signature status
|
||||
#define KEY_SIGNATURE_STATUS @"signatureStatus"
|
||||
|
||||
//signing auths
|
||||
#define KEY_SIGNING_AUTHORITIES @"signingAuthorities"
|
||||
|
||||
//code signing id
|
||||
#define KEY_SIGNATURE_IDENTIFIER @"signingIdentifier"
|
||||
|
||||
//file belongs to apple?
|
||||
#define KEY_SIGNING_IS_APPLE @"signedByApple"
|
||||
|
||||
//file signed with apple dev id
|
||||
#define KEY_SIGNING_IS_APPLE_DEV_ID @"signedWithDevID"
|
||||
|
||||
//from app store
|
||||
#define KEY_SIGNING_IS_APP_STORE @"fromAppStore"
|
||||
|
||||
/* TYPEDEFS */
|
||||
|
||||
//block for library
|
||||
@@ -39,12 +57,12 @@ typedef void (^ProcessCallbackBlock)(Process* _Nonnull);
|
||||
|
||||
@interface ProcInfo : NSObject
|
||||
|
||||
//init
|
||||
// flag dictates if CPU intensive signing checks should be performed
|
||||
-(id _Nullable )init:(BOOL)skipSigningInfo;
|
||||
//init w/ flag
|
||||
// flag dictates if CPU-intensive logic (code signing, etc) should be preformed
|
||||
-(id _Nullable)init:(BOOL)goEasy;
|
||||
|
||||
//start monitoring
|
||||
-(BOOL)start:(ProcessCallbackBlock _Nonnull )callback;
|
||||
-(void)start:(ProcessCallbackBlock _Nonnull )callback;
|
||||
|
||||
//stop monitoring
|
||||
-(void)stop;
|
||||
@@ -77,26 +95,26 @@ typedef void (^ProcessCallbackBlock)(Process* _Nonnull);
|
||||
@property u_int32_t exit;
|
||||
|
||||
//path
|
||||
@property (nonatomic, retain) NSString* _Nullable path;
|
||||
@property(nonatomic, retain)NSString* _Nullable path;
|
||||
|
||||
//args
|
||||
@property (nonatomic, retain) NSMutableArray* _Nonnull arguments;
|
||||
@property(nonatomic, retain)NSMutableArray* _Nonnull arguments;
|
||||
|
||||
//ancestors
|
||||
@property (nonatomic, retain) NSMutableArray* _Nonnull ancestors;
|
||||
@property(nonatomic, retain)NSMutableArray* _Nonnull ancestors;
|
||||
|
||||
//Binary object
|
||||
// has path, hash, etc
|
||||
@property (nonatomic, retain) Binary* _Nonnull binary;
|
||||
@property(nonatomic, retain)Binary* _Nonnull binary;
|
||||
|
||||
//timestamp
|
||||
@property (nonatomic, retain) NSDate* _Nonnull timestamp;
|
||||
@property(nonatomic, retain)NSDate* _Nonnull timestamp;
|
||||
|
||||
/* METHODS */
|
||||
|
||||
//init with a pid
|
||||
// method will then (try) fill out rest of object
|
||||
-(id _Nullable )init:(pid_t)processID;
|
||||
-(id _Nullable)init:(pid_t)processID;
|
||||
|
||||
//set process's path
|
||||
-(void)pathFromPid;
|
||||
@@ -119,23 +137,36 @@ typedef void (^ProcessCallbackBlock)(Process* _Nonnull);
|
||||
/* PROPERTIES */
|
||||
|
||||
//path
|
||||
@property (nonatomic, retain)NSString* _Nonnull path;
|
||||
@property(nonatomic, retain)NSString* _Nonnull path;
|
||||
|
||||
//name
|
||||
@property (nonatomic, retain)NSString* _Nonnull name;
|
||||
@property(nonatomic, retain)NSString* _Nonnull name;
|
||||
|
||||
//icon
|
||||
@property (nonatomic, retain)NSImage* _Nonnull icon;
|
||||
@property(nonatomic, retain)NSImage* _Nonnull icon;
|
||||
|
||||
//file attributes
|
||||
@property (nonatomic, retain)NSDictionary* _Nullable attributes;
|
||||
@property(nonatomic, retain)NSDictionary* _Nullable attributes;
|
||||
|
||||
//spotlight meta data
|
||||
@property(nonatomic, retain)NSDictionary* _Nullable metadata;
|
||||
|
||||
//bundle
|
||||
// nil for non-apps
|
||||
@property (nonatomic, retain)NSBundle* _Nullable bundle;
|
||||
@property(nonatomic, retain)NSBundle* _Nullable bundle;
|
||||
|
||||
//signing info
|
||||
@property (nonatomic, retain)NSDictionary* _Nonnull signingInfo;
|
||||
@property(nonatomic, retain)NSDictionary* _Nonnull signingInfo;
|
||||
|
||||
//entitlements
|
||||
@property(nonatomic, retain)NSDictionary* _Nonnull entitlements;
|
||||
|
||||
//hash
|
||||
@property(nonatomic, retain)NSString* _Nonnull sha256;
|
||||
|
||||
//identifier
|
||||
// either signing id or sha256 hash
|
||||
@property(nonatomic, retain)NSString* _Nonnull identifier;
|
||||
|
||||
//flag indicating binary belongs to Apple OS
|
||||
@property BOOL isApple;
|
||||
@@ -145,13 +176,32 @@ typedef void (^ProcessCallbackBlock)(Process* _Nonnull);
|
||||
|
||||
/* METHODS */
|
||||
|
||||
//init w/ an info dictionary
|
||||
-(id _Nonnull )init:(NSString* _Nonnull)path;
|
||||
//init w/ a path
|
||||
-(id _Nonnull)init:(NSString* _Nonnull)path;
|
||||
|
||||
/* the following methods are rather CPU-intensive
|
||||
as such, if the proc monitoring is run with the 'goEasy' option, they aren't automatically invoked
|
||||
*/
|
||||
|
||||
//get an icon for a process
|
||||
// for apps, this will be app's icon, otherwise just a standard system one
|
||||
-(void)getIcon;
|
||||
|
||||
//generate signing info
|
||||
// also classifies if Apple/from App Store/etc.
|
||||
-(void)generateSigningInfo;
|
||||
-(void)generateSigningInfo:(SecCSFlags)flags entitlements:(BOOL)entitlements;
|
||||
|
||||
//generate entitlements
|
||||
// note: can also call 'generateSigningInfo' w/ 'entitlements:YES'
|
||||
-(void)generateEntitlements;
|
||||
|
||||
//generate hash
|
||||
-(void)generateHash;
|
||||
|
||||
//generate id
|
||||
// eithersigning id, or sha256 hash
|
||||
// note: will generate signing info if needed
|
||||
-(void)generateIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ static const SecAsn1Template kSetOfReceiptAttributeTemplate[] =
|
||||
/* METHODS */
|
||||
|
||||
//init with app path
|
||||
// ->locate/load receipt, etc
|
||||
// locate/load receipt, etc
|
||||
-(instancetype)init:(NSBundle *)bundle;
|
||||
|
||||
/* PROPERTIES */
|
||||
|
||||
+11
-11
@@ -13,7 +13,7 @@
|
||||
#import "AppReceipt.h"
|
||||
|
||||
//helper function from [1]
|
||||
// ->extract an int from ASN.1 data
|
||||
// extract an int from ASN.1 data
|
||||
inline static int getIntValueFromASN1Data(const ASN1_Data *asn1Data)
|
||||
{
|
||||
int ret = 0;
|
||||
@@ -25,7 +25,7 @@ inline static int getIntValueFromASN1Data(const ASN1_Data *asn1Data)
|
||||
}
|
||||
|
||||
//helper function from [1]
|
||||
// ->decode string from ASN.1 data
|
||||
// decode string from ASN.1 data
|
||||
inline static NSString *decodeUTF8StringFromASN1Data(SecAsn1CoderRef decoder, ASN1_Data srcData)
|
||||
{
|
||||
//data struct
|
||||
@@ -61,7 +61,7 @@ bail:
|
||||
@synthesize decodedData;
|
||||
|
||||
//init with app path
|
||||
// ->locate/load/decode receipt, etc
|
||||
// locate/load/decode receipt, etc
|
||||
-(instancetype)init:(NSBundle *)bundle
|
||||
{
|
||||
//init
|
||||
@@ -101,7 +101,7 @@ bail:
|
||||
}
|
||||
|
||||
//parse out values
|
||||
// ->bundle id, app version, etc
|
||||
// bundle id, app version, etc
|
||||
self.components = [self parse];
|
||||
if( (nil == self.components) ||
|
||||
(0 == self.components.count) )
|
||||
@@ -120,7 +120,7 @@ bail:
|
||||
}
|
||||
|
||||
//decode receipt data
|
||||
// ->some validations performed here too
|
||||
// some validations performed here too
|
||||
-(NSData*)decode
|
||||
{
|
||||
//decoded data
|
||||
@@ -183,7 +183,7 @@ bail:
|
||||
}
|
||||
|
||||
//CHECK 1:
|
||||
// ->make sure there is a signer
|
||||
// make sure there is a signer
|
||||
status = CMSDecoderGetNumSigners(decoder, &signers);
|
||||
if( (noErr != status) ||
|
||||
(0 == signers) )
|
||||
@@ -193,7 +193,7 @@ bail:
|
||||
}
|
||||
|
||||
//CHECK 2:
|
||||
// ->make sure signer status is ok
|
||||
// make sure signer status is ok
|
||||
status = CMSDecoderCopySignerStatus(decoder, 0, policy, TRUE, &signerStatus, &trust, &certVerifyResult);
|
||||
if( (noErr != status) ||
|
||||
(kCMSSignerValid != signerStatus) )
|
||||
@@ -259,7 +259,7 @@ bail:
|
||||
}
|
||||
|
||||
//parse decoded receipt
|
||||
// ->extract out items such as bundle id, app version, etc.
|
||||
// extract out items such as bundle id, app version, etc.
|
||||
-(NSMutableDictionary*)parse
|
||||
{
|
||||
//decoder
|
||||
@@ -297,14 +297,14 @@ bail:
|
||||
items = [NSMutableDictionary dictionary];
|
||||
|
||||
//extact attributes
|
||||
// ->save those of interest
|
||||
// save those of interest
|
||||
for(int i = 0; (attribute = payload.attrs[i]); i++)
|
||||
{
|
||||
//process each type
|
||||
switch(getIntValueFromASN1Data(&attribute->type))
|
||||
{
|
||||
//bundle id
|
||||
// ->save bundle id and data
|
||||
// save bundle id and data
|
||||
case RECEIPT_ATTR_BUNDLE_ID:
|
||||
{
|
||||
//save bundle id
|
||||
@@ -345,7 +345,7 @@ bail:
|
||||
}
|
||||
|
||||
//default
|
||||
// ->ignore
|
||||
// ignore
|
||||
default:
|
||||
{
|
||||
break;
|
||||
|
||||
+115
-14
@@ -19,12 +19,15 @@
|
||||
@synthesize path;
|
||||
@synthesize bundle;
|
||||
@synthesize isApple;
|
||||
@synthesize metadata;
|
||||
@synthesize attributes;
|
||||
@synthesize identifier;
|
||||
@synthesize isAppStore;
|
||||
@synthesize signingInfo;
|
||||
@synthesize entitlements;
|
||||
|
||||
//init binary object
|
||||
// generates signing info, classifies binary, etc
|
||||
// note: CPU-intensive logic (code signing, etc) called manually
|
||||
-(id)init:(NSString*)binaryPath
|
||||
{
|
||||
//init super
|
||||
@@ -57,11 +60,11 @@
|
||||
//get name
|
||||
[self getName];
|
||||
|
||||
//get icon
|
||||
[self getIcon];
|
||||
|
||||
//get attributes
|
||||
//get file attributes
|
||||
[self getAttributes];
|
||||
|
||||
//get meta data (spotlight)
|
||||
[self getMetadata];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -89,17 +92,17 @@
|
||||
// either via app bundle, or from path
|
||||
-(void)getName
|
||||
{
|
||||
//found app bundle?
|
||||
// grab name from 'CFBundleName'
|
||||
//first try get name from app bundle
|
||||
// specifically, via grab name from 'CFBundleName'
|
||||
if(nil != self.bundle)
|
||||
{
|
||||
//extract name
|
||||
self.name = [self.bundle infoDictionary][@"CFBundleName"];
|
||||
}
|
||||
|
||||
//no app bundle
|
||||
//no app bundle || no 'CFBundleName'
|
||||
// just use last component from path
|
||||
else
|
||||
if(nil == self.name)
|
||||
{
|
||||
//set name
|
||||
self.name = [self.path lastPathComponent];
|
||||
@@ -108,7 +111,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
//get attributes
|
||||
//get file attributes
|
||||
-(void)getAttributes
|
||||
{
|
||||
//grab (file) attributes
|
||||
@@ -117,6 +120,53 @@
|
||||
return;
|
||||
}
|
||||
|
||||
//get (spotlight) meta data
|
||||
-(void)getMetadata
|
||||
{
|
||||
//md item ref
|
||||
MDItemRef mdItem = nil;
|
||||
|
||||
//attributes names
|
||||
CFArrayRef attributeNames = nil;
|
||||
|
||||
//create
|
||||
mdItem = MDItemCreate(kCFAllocatorDefault, (CFStringRef)self.path);
|
||||
if(nil == mdItem)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//copy names
|
||||
attributeNames = MDItemCopyAttributeNames(mdItem);
|
||||
if(nil == attributeNames)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//get metadata
|
||||
self.metadata = CFBridgingRelease(MDItemCopyAttributes(mdItem, attributeNames));
|
||||
|
||||
bail:
|
||||
|
||||
//release names
|
||||
if(nil != attributeNames)
|
||||
{
|
||||
//release
|
||||
CFRelease(attributeNames);
|
||||
}
|
||||
|
||||
//release item
|
||||
if(nil != mdItem)
|
||||
{
|
||||
//release
|
||||
CFRelease(mdItem);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//get an icon for a process
|
||||
// for apps, this will be app's icon, otherwise just a standard system one
|
||||
-(void)getIcon
|
||||
@@ -153,7 +203,7 @@
|
||||
iconExtension = [iconFile pathExtension];
|
||||
|
||||
//if its blank (i.e. not specified)
|
||||
// ->go with 'icns'
|
||||
// go with 'icns'
|
||||
if(YES == [iconExtension isEqualTo:@""])
|
||||
{
|
||||
//set type
|
||||
@@ -176,7 +226,7 @@
|
||||
self.icon = [[NSWorkspace sharedWorkspace] iconForFile:self.path];
|
||||
|
||||
//load system document icon
|
||||
// ->static var, so only load once
|
||||
// static var, so only load once
|
||||
if(nil == documentIcon)
|
||||
{
|
||||
//load
|
||||
@@ -204,11 +254,11 @@ bail:
|
||||
|
||||
//generate signing info
|
||||
// also classifies if Apple/from App Store/etc.
|
||||
-(void)generateSigningInfo
|
||||
-(void)generateSigningInfo:(SecCSFlags)flags entitlements:(BOOL)entitlements
|
||||
{
|
||||
//extract signing info (do this first!)
|
||||
// from Apple, App Store, signing authorities, etc
|
||||
self.signingInfo = extractSigningInfo(self.path);
|
||||
self.signingInfo = extractSigningInfo(self.path, flags, entitlements);
|
||||
|
||||
//perform more signing checks and lists
|
||||
// gotta be happily signed for checks though
|
||||
@@ -229,6 +279,57 @@ bail:
|
||||
return;
|
||||
}
|
||||
|
||||
//generate hash
|
||||
-(void)generateHash
|
||||
{
|
||||
//hash
|
||||
self.sha256 = PI_hashFile(self.path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//generate id
|
||||
// either signing id, or sha256 hash
|
||||
// note: will generate signing info if needed
|
||||
-(void)generateIdentifier
|
||||
{
|
||||
//generate signing info?
|
||||
if(nil == self.signingInfo)
|
||||
{
|
||||
//generate
|
||||
[self generateSigningInfo:kSecCSDefaultFlags entitlements:NO];
|
||||
}
|
||||
|
||||
//validly signed binary?
|
||||
// use its signing identifier
|
||||
if( (noErr == [self.signingInfo[KEY_SIGNATURE_STATUS] intValue]) &&
|
||||
(0 != [self.signingInfo[KEY_SIGNING_AUTHORITIES] count]) &&
|
||||
(nil != self.signingInfo[KEY_SIGNATURE_IDENTIFIER]) )
|
||||
{
|
||||
//use signing id
|
||||
self.identifier = self.signingInfo[KEY_SIGNATURE_IDENTIFIER];
|
||||
}
|
||||
//not validly signed or unsigned
|
||||
// generate sha256 hash for identifier
|
||||
else
|
||||
{
|
||||
//hash
|
||||
self.identifier = PI_hashFile(self.path);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//generate entitlements
|
||||
// note: can also call 'generateSigningInfo' w/ 'entitlements:YES'
|
||||
-(void)generateEntitlements
|
||||
{
|
||||
//call into helper function
|
||||
self.entitlements = extractEntitlements(self.path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//for pretty printing
|
||||
-(NSString *)description
|
||||
{
|
||||
|
||||
+10
-13
@@ -28,22 +28,19 @@
|
||||
//audit class for exec events
|
||||
#define AUDIT_CLASS_EXEC 0x40000000
|
||||
|
||||
//signature status
|
||||
#define KEY_SIGNATURE_STATUS @"signatureStatus"
|
||||
//key for stdout output
|
||||
#define STDOUT @"stdOutput"
|
||||
|
||||
//signing auths
|
||||
#define KEY_SIGNING_AUTHORITIES @"signingAuthorities"
|
||||
|
||||
//file belongs to apple?
|
||||
#define KEY_SIGNING_IS_APPLE @"signedByApple"
|
||||
|
||||
//file signed with apple dev id
|
||||
#define KEY_SIGNING_IS_APPLE_DEV_ID @"signedWithDevID"
|
||||
|
||||
//from app store
|
||||
#define KEY_SIGNING_IS_APP_STORE @"fromAppStore"
|
||||
//key for stderr output
|
||||
#define STDERR @"stdError"
|
||||
|
||||
//key for exit code
|
||||
#define EXIT_CODE @"exitCode"
|
||||
|
||||
//path to codesign
|
||||
#define CODE_SIGN @"/usr/bin/codesign"
|
||||
|
||||
//entitlements
|
||||
#define KEY_SIGNING_ENTITLEMENTS @"entitlements"
|
||||
|
||||
#endif /* Consts_h */
|
||||
|
||||
+16
-17
@@ -102,7 +102,7 @@ bail:
|
||||
}
|
||||
|
||||
//get uid
|
||||
// ->sets 'user' instance var
|
||||
// sets 'user' instance var
|
||||
-(void)getUser
|
||||
{
|
||||
//kinfo_proc struct
|
||||
@@ -156,7 +156,7 @@ bail:
|
||||
currentPID = self.ppid;
|
||||
}
|
||||
//don't know parent
|
||||
// ->just start with self
|
||||
// just start with self
|
||||
else
|
||||
{
|
||||
//start w/ self
|
||||
@@ -164,7 +164,7 @@ bail:
|
||||
}
|
||||
|
||||
//add until we get to to end (pid 0)
|
||||
// ->or error out during the traversal
|
||||
// or error out during the traversal
|
||||
while(YES)
|
||||
{
|
||||
//get parent pid
|
||||
@@ -300,7 +300,7 @@ bail:
|
||||
int signalStatus = -1;
|
||||
|
||||
//send kill with 0 to determine if alive
|
||||
// -> see: http://stackoverflow.com/questions/9152979/check-if-process-exists-given-its-pid
|
||||
// see: http://stackoverflow.com/questions/9152979/check-if-process-exists-given-its-pid
|
||||
signalStatus = kill(self.pid, 0);
|
||||
|
||||
//is alive?
|
||||
@@ -315,7 +315,7 @@ bail:
|
||||
}
|
||||
|
||||
//extract commandline args
|
||||
// ->saves into 'arguments' ivar
|
||||
// saves into 'arguments' ivar
|
||||
-(void)getArgs
|
||||
{
|
||||
//'management info base' array
|
||||
@@ -340,7 +340,7 @@ bail:
|
||||
char* parser = NULL;
|
||||
|
||||
//init mib
|
||||
// ->want system's size for max args
|
||||
// want system's size for max args
|
||||
mib[0] = CTL_KERN;
|
||||
mib[1] = KERN_ARGMAX;
|
||||
|
||||
@@ -363,7 +363,7 @@ bail:
|
||||
}
|
||||
|
||||
//init mib
|
||||
// ->want process args
|
||||
// want process args
|
||||
mib[0] = CTL_KERN;
|
||||
mib[1] = KERN_PROCARGS2;
|
||||
mib[2] = pid;
|
||||
@@ -379,7 +379,7 @@ bail:
|
||||
}
|
||||
|
||||
//sanity check
|
||||
// ->ensure buffer is somewhat sane
|
||||
// ensure buffer is somewhat sane
|
||||
if(size <= sizeof(int))
|
||||
{
|
||||
//bail
|
||||
@@ -387,12 +387,11 @@ bail:
|
||||
}
|
||||
|
||||
//extract number of args
|
||||
// ->at start of buffer
|
||||
// at start of buffer
|
||||
memcpy(&numberOfArgs, processArgs, sizeof(numberOfArgs));
|
||||
|
||||
|
||||
//init pointer to start of args
|
||||
// ->they start right after # of args
|
||||
// they start right after # of args
|
||||
parser = processArgs + sizeof(numberOfArgs);
|
||||
|
||||
//scan until end of process's NULL-terminated path
|
||||
@@ -410,7 +409,7 @@ bail:
|
||||
}
|
||||
|
||||
//sanity check
|
||||
// ->make sure end-of-buffer wasn't reached
|
||||
// make sure end-of-buffer wasn't reached
|
||||
if(parser == &processArgs[size])
|
||||
{
|
||||
//bail
|
||||
@@ -418,7 +417,7 @@ bail:
|
||||
}
|
||||
|
||||
//skip all trailing NULLs
|
||||
// ->scan will end when non-NULL is found
|
||||
// scan will end when non-NULL is found
|
||||
while(parser < &processArgs[size])
|
||||
{
|
||||
//scan till NULL-terminator
|
||||
@@ -433,7 +432,7 @@ bail:
|
||||
}
|
||||
|
||||
//sanity check
|
||||
// ->(again), make sure end-of-buffer wasn't reached
|
||||
// (again), make sure end-of-buffer wasn't reached
|
||||
if(parser == &processArgs[size])
|
||||
{
|
||||
//bail
|
||||
@@ -441,15 +440,15 @@ bail:
|
||||
}
|
||||
|
||||
//parser should now point to argv[0], process name
|
||||
// ->init arg start
|
||||
// init arg start
|
||||
argStart = parser;
|
||||
|
||||
//keep scanning until all args are found
|
||||
// ->each is NULL-terminated
|
||||
// each is NULL-terminated
|
||||
while(parser < &processArgs[size])
|
||||
{
|
||||
//each arg is NULL-terminated
|
||||
// ->so scan till NULL, then save into array
|
||||
// so scan till NULL, then save into array
|
||||
if(*parser == '\0')
|
||||
{
|
||||
//save arg
|
||||
|
||||
+26
-19
@@ -8,7 +8,7 @@
|
||||
//
|
||||
|
||||
//disable incomplete/umbrella warnings
|
||||
// ->otherwise complains about 'audit_kevents.h'
|
||||
// otherwise complains about 'audit_kevents.h'
|
||||
#pragma clang diagnostic ignored "-Wincomplete-umbrella"
|
||||
|
||||
#import "Consts.h"
|
||||
@@ -30,8 +30,8 @@
|
||||
|
||||
/* INSTANCE VARIABLES */
|
||||
|
||||
//do signing checks?
|
||||
@property BOOL skipSigningInfo;
|
||||
//skip CPU-intensive logic
|
||||
@property BOOL goEasy;
|
||||
|
||||
//callback block
|
||||
@property(nonatomic, copy)ProcessCallbackBlock processCallback;
|
||||
@@ -72,8 +72,8 @@ bail:
|
||||
}
|
||||
|
||||
//init w/ flag
|
||||
// flag dictates if CPU intensive signing checks should be performed
|
||||
-(id _Nullable )init:(BOOL)skipSigningInfo;
|
||||
// flag dictates if CPU-intensive logic (code signing, etc) should be preformed
|
||||
-(id _Nullable )init:(BOOL)goEasy;
|
||||
{
|
||||
//init
|
||||
// calls 'super' too
|
||||
@@ -81,7 +81,7 @@ bail:
|
||||
if(self)
|
||||
{
|
||||
//save mode
|
||||
self.skipSigningInfo = skipSigningInfo;
|
||||
self.goEasy = goEasy;
|
||||
}
|
||||
|
||||
bail:
|
||||
@@ -91,7 +91,7 @@ bail:
|
||||
|
||||
//start monitoring
|
||||
// note: requires root/macOS 10.12.4+ for full monitoring
|
||||
-(BOOL)start:(ProcessCallbackBlock)callback
|
||||
-(void)start:(ProcessCallbackBlock)callback
|
||||
{
|
||||
//OS version info
|
||||
NSDictionary* osVersionInfo = nil;
|
||||
@@ -103,7 +103,7 @@ bail:
|
||||
osVersionInfo = PI_getOSVersion();
|
||||
|
||||
//do basic (app) monitoring
|
||||
// ->if not root, or OS version is < 10.12.4 (due to kernel bug)
|
||||
// if not root, or OS version is < 10.12.4 (due to kernel bug)
|
||||
if( (0 != getuid()) ||
|
||||
([osVersionInfo[@"minorVersion"] intValue] < OS_MINOR_VERSION_SIERRA) ||
|
||||
(([osVersionInfo[@"minorVersion"] intValue] == OS_MINOR_VERSION_SIERRA) && ([osVersionInfo[@"bugfixVersion"] intValue] < 4)) )
|
||||
@@ -125,7 +125,7 @@ bail:
|
||||
});
|
||||
}
|
||||
|
||||
return NO;
|
||||
return;
|
||||
}
|
||||
|
||||
//stop monitoring
|
||||
@@ -136,7 +136,7 @@ bail:
|
||||
[[NSNotificationCenter defaultCenter] removeObserver: self];
|
||||
|
||||
//set 'stop' monitor bool
|
||||
// ->is checked in 'monitor' method as termination condition
|
||||
// is checked in 'monitor' method as termination condition
|
||||
self.shouldStop = YES;
|
||||
|
||||
return;
|
||||
@@ -231,7 +231,7 @@ bail:
|
||||
}
|
||||
|
||||
//set preselect flags
|
||||
// ->event classes we're interested in
|
||||
// event classes we're interested in
|
||||
status = ioctl(auditFileDescriptor, AUDITPIPE_SET_PRESELECT_FLAGS, &eventClasses);
|
||||
if(-1 == status)
|
||||
{
|
||||
@@ -240,7 +240,7 @@ bail:
|
||||
}
|
||||
|
||||
//set non-attributable flags
|
||||
// ->event classes we're interested in
|
||||
// event classes we're interested in
|
||||
status = ioctl(auditFileDescriptor, AUDITPIPE_SET_PRESELECT_NAFLAGS, &eventClasses);
|
||||
if(-1 == status)
|
||||
{
|
||||
@@ -249,7 +249,7 @@ bail:
|
||||
}
|
||||
|
||||
//forever
|
||||
// ->read/parse/process audit records
|
||||
// read/parse/process audit records
|
||||
while(YES)
|
||||
{
|
||||
@autoreleasepool
|
||||
@@ -293,7 +293,7 @@ bail:
|
||||
processedLength = 0;
|
||||
|
||||
//parse record
|
||||
// ->read all tokens/process
|
||||
// read all tokens/process
|
||||
while(0 != recordBalance)
|
||||
{
|
||||
//extract token
|
||||
@@ -301,7 +301,7 @@ bail:
|
||||
if(-1 == au_fetch_tok(&tokenStruct, recordBuffer + processedLength, recordBalance))
|
||||
{
|
||||
//error
|
||||
// ->skip record
|
||||
// skip record
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ bail:
|
||||
(YES != [self shouldProcessRecord:process.type]) )
|
||||
{
|
||||
//bail
|
||||
// ->skips rest of record
|
||||
// skips rest of record
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -700,11 +700,15 @@ bail:
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//automatically generate signing info?
|
||||
if(YES != self.skipSigningInfo)
|
||||
//automatically generate signing info/icon?
|
||||
// these can be skipped for performance reasons
|
||||
if(YES != self.goEasy)
|
||||
{
|
||||
//generate signing info
|
||||
[process.binary generateSigningInfo];
|
||||
[process.binary generateSigningInfo:kSecCSDefaultFlags entitlements:NO];
|
||||
|
||||
//set icon
|
||||
[process.binary getIcon];
|
||||
}
|
||||
|
||||
//invoke user callback
|
||||
@@ -756,6 +760,9 @@ bail:
|
||||
continue;
|
||||
}
|
||||
|
||||
//generate signing info
|
||||
[currentProcess.binary generateSigningInfo:kSecCSDefaultFlags entitlements:NO];
|
||||
|
||||
//add
|
||||
[processes addObject:currentProcess];
|
||||
}
|
||||
|
||||
+10
-6
@@ -16,20 +16,24 @@
|
||||
/* FUNCTIONS */
|
||||
|
||||
//get the signing info of a file
|
||||
NSDictionary* extractSigningInfo(NSString* path);
|
||||
NSMutableDictionary* extractSigningInfo(NSString* path, SecCSFlags flags, BOOL entitlements);
|
||||
|
||||
//determine if a file is signed by Apple proper
|
||||
BOOL isApple(NSString* path, SecCSFlags flags);
|
||||
|
||||
//determine if file is signed with Apple Dev ID/cert
|
||||
BOOL isSignedDevID(NSString* binary);
|
||||
BOOL isSignedDevID(NSString* path, SecCSFlags flags);
|
||||
|
||||
//determine if a file is from the app store
|
||||
// ->gotta be signed w/ Apple Dev ID & have valid app receipt
|
||||
// gotta be signed w/ Apple Dev ID & have valid app receipt
|
||||
BOOL fromAppStore(NSString* path);
|
||||
|
||||
//get GUID (really just computer's MAC address)
|
||||
// ->from Apple's 'Get the GUID in OS X' (see: 'Validating Receipts Locally')
|
||||
// from Apple's 'Get the GUID in OS X' (see: 'Validating Receipts Locally')
|
||||
NSData* getGUID(void);
|
||||
|
||||
//determine if a file is signed by Apple proper
|
||||
BOOL isApple(NSString* path);
|
||||
//extact entitlements
|
||||
// note: execs apple's 'codesign' binary
|
||||
NSDictionary* extractEntitlements(NSString* path);
|
||||
|
||||
#endif
|
||||
|
||||
+290
-78
@@ -9,19 +9,130 @@
|
||||
|
||||
#import "Consts.h"
|
||||
#import "Signing.h"
|
||||
#import "procInfo.h"
|
||||
#import "Utilities.h"
|
||||
#import "AppReceipt.h"
|
||||
|
||||
#import <mach-o/fat.h>
|
||||
#import <mach-o/arch.h>
|
||||
#import <mach-o/swap.h>
|
||||
|
||||
#import <sys/sysctl.h>
|
||||
#import <Security/Security.h>
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
|
||||
//determine the offset (if any)
|
||||
// of the 'best' architecture in a (fat) binary
|
||||
uint32_t bestArchOffset(NSString* path)
|
||||
{
|
||||
//offset of best architecture
|
||||
uint32_t offset = 0;
|
||||
|
||||
//pool
|
||||
@autoreleasepool
|
||||
{
|
||||
|
||||
//binary
|
||||
NSMutableData* binary = nil;
|
||||
|
||||
//le bytez
|
||||
const void* binaryBytes = NULL;
|
||||
|
||||
//fat header
|
||||
struct fat_header* fatHeader = NULL;
|
||||
|
||||
//number of fat architectures
|
||||
uint32_t fatArchitectureCount = 0;
|
||||
|
||||
//fat architectures
|
||||
void *fatArchitectures = NULL;
|
||||
|
||||
//local architecture
|
||||
const NXArchInfo *localArchitecture = NULL;
|
||||
|
||||
//best matching architecture
|
||||
struct fat_arch *bestArchitecture = NULL;
|
||||
|
||||
//load binary into memory
|
||||
binary = [NSMutableData dataWithContentsOfFile:path];
|
||||
if(binary.length < sizeof(struct fat_header))
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//grab bytes
|
||||
binaryBytes = binary.bytes;
|
||||
|
||||
//not universal (fat)
|
||||
if( (FAT_MAGIC != *(const uint32_t *)binaryBytes) &&
|
||||
(FAT_CIGAM != *(const uint32_t *)binaryBytes) )
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//binary is fat
|
||||
// init pointer to fat header
|
||||
fatHeader = (struct fat_header*)binaryBytes;
|
||||
|
||||
//swap size?
|
||||
if(fatHeader->magic == OSSwapHostToBigInt32(FAT_MAGIC))
|
||||
{
|
||||
//swap
|
||||
fatArchitectureCount = OSSwapBigToHostInt32(fatHeader->nfat_arch);
|
||||
}
|
||||
|
||||
//sanity check
|
||||
if(binary.length <= sizeof(struct fat_header) + fatArchitectureCount * sizeof(struct fat_arch))
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//init pointer to fat architectures
|
||||
fatArchitectures = (char*)binaryBytes + sizeof(struct fat_header);
|
||||
|
||||
//get local architecture
|
||||
localArchitecture = NXGetLocalArchInfo();
|
||||
|
||||
//swap fat architectures?
|
||||
if(fatHeader->magic == OSSwapHostToBigInt32(FAT_MAGIC))
|
||||
{
|
||||
//swap
|
||||
swap_fat_arch(fatArchitectures, fatArchitectureCount, localArchitecture->byteorder);
|
||||
}
|
||||
|
||||
//find best architecture
|
||||
bestArchitecture = NXFindBestFatArch(localArchitecture->cputype, localArchitecture->cpusubtype, fatArchitectures, fatArchitectureCount);
|
||||
if(NULL == bestArchitecture)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//init offset
|
||||
offset = bestArchitecture->offset;
|
||||
|
||||
bail:
|
||||
|
||||
;
|
||||
|
||||
}//autorelease
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
//get the signing info of a item
|
||||
NSDictionary* extractSigningInfo(NSString* path)
|
||||
NSMutableDictionary* extractSigningInfo(NSString* path, SecCSFlags flags, BOOL entitlements)
|
||||
{
|
||||
//info dictionary
|
||||
NSMutableDictionary* signingStatus = nil;
|
||||
NSMutableDictionary* signingInfo = nil;
|
||||
|
||||
//offset of best architecture
|
||||
// for universal/fat binary, need to check correct arch
|
||||
uint32_t offset = 0;
|
||||
|
||||
//code
|
||||
SecStaticCodeRef staticCode = NULL;
|
||||
@@ -30,7 +141,7 @@ NSDictionary* extractSigningInfo(NSString* path)
|
||||
OSStatus status = -1;
|
||||
|
||||
//signing information
|
||||
CFDictionaryRef signingInformation = NULL;
|
||||
CFDictionaryRef signingDetails = NULL;
|
||||
|
||||
//cert chain
|
||||
NSArray* certificateChain = nil;
|
||||
@@ -45,23 +156,27 @@ NSDictionary* extractSigningInfo(NSString* path)
|
||||
CFStringRef commonName = NULL;
|
||||
|
||||
//init signing status
|
||||
signingStatus = [NSMutableDictionary dictionary];
|
||||
signingInfo = [NSMutableDictionary dictionary];
|
||||
|
||||
//sanity check
|
||||
if(nil == path)
|
||||
{
|
||||
//set err
|
||||
signingStatus[KEY_SIGNATURE_STATUS] = [NSNumber numberWithInt:errSecCSObjectRequired];
|
||||
signingInfo[KEY_SIGNATURE_STATUS] = [NSNumber numberWithInt:errSecCSObjectRequired];
|
||||
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//get offset of 'best' architecute
|
||||
// this is what loader will run, and thus, what we should validate!
|
||||
offset = bestArchOffset(path);
|
||||
|
||||
//create static code
|
||||
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode);
|
||||
status = SecStaticCodeCreateWithPathAndAttributes((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, (__bridge CFDictionaryRef)@{(__bridge NSString *)kSecCodeAttributeUniversalFileOffset : [NSNumber numberWithUnsignedInt:offset]}, &staticCode);
|
||||
|
||||
//save signature status
|
||||
signingStatus[KEY_SIGNATURE_STATUS] = [NSNumber numberWithInt:status];
|
||||
signingInfo[KEY_SIGNATURE_STATUS] = [NSNumber numberWithInt:status];
|
||||
if(noErr != status)
|
||||
{
|
||||
//bail
|
||||
@@ -69,60 +184,59 @@ NSDictionary* extractSigningInfo(NSString* path)
|
||||
}
|
||||
|
||||
//check signature
|
||||
status = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSDoNotValidateResources, NULL, NULL);
|
||||
status = SecStaticCodeCheckValidity(staticCode, flags, NULL);
|
||||
|
||||
//(re)save signature status
|
||||
signingStatus[KEY_SIGNATURE_STATUS] = [NSNumber numberWithInt:status];
|
||||
|
||||
//if file is signed
|
||||
// ->grab signing authorities
|
||||
if(noErr == status)
|
||||
{
|
||||
//grab signing authorities
|
||||
status = SecCodeCopySigningInformation(staticCode, kSecCSSigningInformation, &signingInformation);
|
||||
if(noErr != status)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//determine if binary is signed by Apple
|
||||
signingStatus[KEY_SIGNING_IS_APPLE] = [NSNumber numberWithBool:isApple(path)];
|
||||
|
||||
//not apple proper
|
||||
// ->is signed with Apple Dev ID?
|
||||
if(YES != [signingStatus[KEY_SIGNING_IS_APPLE] boolValue])
|
||||
{
|
||||
//determine if binary is Apple Dev ID
|
||||
signingStatus[KEY_SIGNING_IS_APPLE_DEV_ID] = [NSNumber numberWithBool:isSignedDevID(path)];
|
||||
|
||||
//if dev id
|
||||
// ->from app store?
|
||||
if(YES == [signingStatus[KEY_SIGNING_IS_APPLE_DEV_ID] boolValue])
|
||||
{
|
||||
//from app store?
|
||||
signingStatus[KEY_SIGNING_IS_APP_STORE] = [NSNumber numberWithBool:fromAppStore(path)];
|
||||
}
|
||||
}
|
||||
}
|
||||
//error
|
||||
// ->not signed, or something else, so no need to check cert's names
|
||||
else
|
||||
signingInfo[KEY_SIGNATURE_STATUS] = [NSNumber numberWithInt:status];
|
||||
if(noErr != status)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//grab signing info
|
||||
status = SecCodeCopySigningInformation(staticCode, kSecCSSigningInformation, &signingDetails);
|
||||
if(noErr != status)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//grab signing ID
|
||||
signingInfo[KEY_SIGNATURE_IDENTIFIER] = [(__bridge NSDictionary*)signingDetails objectForKey:(__bridge NSString*)kSecCodeInfoIdentifier];
|
||||
|
||||
//determine if binary is signed by Apple
|
||||
signingInfo[KEY_SIGNING_IS_APPLE] = [NSNumber numberWithBool:isApple(path, flags)];
|
||||
|
||||
//not apple proper
|
||||
// is signed with Apple Dev ID?
|
||||
if(YES != [signingInfo[KEY_SIGNING_IS_APPLE] boolValue])
|
||||
{
|
||||
//determine if binary is Apple Dev ID
|
||||
signingInfo[KEY_SIGNING_IS_APPLE_DEV_ID] = [NSNumber numberWithBool:isSignedDevID(path, flags)];
|
||||
|
||||
//if dev id
|
||||
// from app store?
|
||||
if(YES == [signingInfo[KEY_SIGNING_IS_APPLE_DEV_ID] boolValue])
|
||||
{
|
||||
//from app store?
|
||||
signingInfo[KEY_SIGNING_IS_APP_STORE] = [NSNumber numberWithBool:fromAppStore(path)];
|
||||
}
|
||||
}
|
||||
|
||||
//init array for certificate names
|
||||
signingStatus[KEY_SIGNING_AUTHORITIES] = [NSMutableArray array];
|
||||
signingInfo[KEY_SIGNING_AUTHORITIES] = [NSMutableArray array];
|
||||
|
||||
//get cert chain
|
||||
certificateChain = [(__bridge NSDictionary*)signingInformation objectForKey:(__bridge NSString*)kSecCodeInfoCertificates];
|
||||
certificateChain = [(__bridge NSDictionary*)signingDetails objectForKey:(__bridge NSString*)kSecCodeInfoCertificates];
|
||||
|
||||
//get name of all certs
|
||||
// ->add each to list
|
||||
// add each to list
|
||||
for(index = 0; index < certificateChain.count; index++)
|
||||
{
|
||||
//reset
|
||||
commonName = NULL;
|
||||
|
||||
//extract cert
|
||||
certificate = (__bridge SecCertificateRef)([certificateChain objectAtIndex:index]);
|
||||
|
||||
@@ -133,28 +247,41 @@ NSDictionary* extractSigningInfo(NSString* path)
|
||||
if( (noErr != status) ||
|
||||
(NULL == commonName))
|
||||
{
|
||||
//release
|
||||
if(NULL != commonName)
|
||||
{
|
||||
//release
|
||||
CFRelease(commonName);
|
||||
}
|
||||
|
||||
//skip
|
||||
continue;
|
||||
}
|
||||
|
||||
//save
|
||||
[signingStatus[KEY_SIGNING_AUTHORITIES] addObject:(__bridge NSString*)commonName];
|
||||
[signingInfo[KEY_SIGNING_AUTHORITIES] addObject:(__bridge NSString*)commonName];
|
||||
|
||||
//release name
|
||||
CFRelease(commonName);
|
||||
}
|
||||
|
||||
|
||||
//add entitlements?
|
||||
if(YES == entitlements)
|
||||
{
|
||||
//extract entitlements via Apple's 'codesign'
|
||||
signingInfo[KEY_SIGNING_ENTITLEMENTS] = extractEntitlements(path);
|
||||
}
|
||||
|
||||
bail:
|
||||
|
||||
//free signing info
|
||||
if(NULL != signingInformation)
|
||||
if(NULL != signingDetails)
|
||||
{
|
||||
//free
|
||||
CFRelease(signingInformation);
|
||||
CFRelease(signingDetails);
|
||||
|
||||
//unset
|
||||
signingInformation = NULL;
|
||||
signingDetails = NULL;
|
||||
}
|
||||
|
||||
//free static code
|
||||
@@ -167,11 +294,11 @@ bail:
|
||||
staticCode = NULL;
|
||||
}
|
||||
|
||||
return signingStatus;
|
||||
return signingInfo;
|
||||
}
|
||||
|
||||
//determine if a file is signed by Apple proper
|
||||
BOOL isApple(NSString* path)
|
||||
BOOL isApple(NSString* path, SecCSFlags flags)
|
||||
{
|
||||
//flag
|
||||
BOOL isApple = NO;
|
||||
@@ -205,17 +332,17 @@ BOOL isApple(NSString* path)
|
||||
|
||||
//check if file is signed by apple by checking if it conforms to req string
|
||||
// note: ignore 'errSecCSBadResource' as lots of signed apple files return this issue :/
|
||||
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirementRef);
|
||||
status = SecStaticCodeCheckValidity(staticCode, flags, requirementRef);
|
||||
if( (noErr != status) &&
|
||||
(errSecCSBadResource != status) )
|
||||
{
|
||||
//bail
|
||||
// ->just means app isn't signed by apple
|
||||
// just means isn't signed by apple
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//ok, happy (SecStaticCodeCheckValidity() didn't fail)
|
||||
// ->file is signed by Apple
|
||||
// file is signed by Apple
|
||||
isApple = YES;
|
||||
|
||||
bail:
|
||||
@@ -244,7 +371,7 @@ bail:
|
||||
}
|
||||
|
||||
//verify the receipt
|
||||
// ->check bundle ID, app version, and receipt's hash
|
||||
// check bundle ID, app version, and receipt's hash
|
||||
BOOL verifyReceipt(NSBundle* appBundle, AppReceipt* receipt)
|
||||
{
|
||||
//flag
|
||||
@@ -280,7 +407,7 @@ BOOL verifyReceipt(NSBundle* appBundle, AppReceipt* receipt)
|
||||
[digestData appendData:receipt.bundleIdentifierData];
|
||||
|
||||
//CHECK 1:
|
||||
// ->app's bundle ID should match receipt's bundle ID
|
||||
// app's bundle ID should match receipt's bundle ID
|
||||
if(YES != [receipt.bundleIdentifier isEqualToString:appBundle.bundleIdentifier])
|
||||
{
|
||||
//bail
|
||||
@@ -288,7 +415,7 @@ BOOL verifyReceipt(NSBundle* appBundle, AppReceipt* receipt)
|
||||
}
|
||||
|
||||
//CHECK 2:
|
||||
// ->app's version should match receipt's version
|
||||
// app's version should match receipt's version
|
||||
if(YES != [receipt.appVersion isEqualToString:appBundle.infoDictionary[@"CFBundleShortVersionString"]])
|
||||
{
|
||||
//bail
|
||||
@@ -296,7 +423,7 @@ BOOL verifyReceipt(NSBundle* appBundle, AppReceipt* receipt)
|
||||
}
|
||||
|
||||
//CHECK 3:
|
||||
// ->verify receipt's hash (UUID)
|
||||
// verify receipt's hash (UUID)
|
||||
|
||||
//init SHA 1 hash
|
||||
CC_SHA1(digestData.bytes, (CC_LONG)digestData.length, digestBuffer);
|
||||
@@ -317,7 +444,7 @@ bail:
|
||||
}
|
||||
|
||||
//get GUID (really just computer's MAC address)
|
||||
// ->from Apple's 'Get the GUID in OS X' (see: 'Validating Receipts Locally')
|
||||
// from Apple's 'Get the GUID in OS X' (see: 'Validating Receipts Locally')
|
||||
NSData* getGUID()
|
||||
{
|
||||
//status var
|
||||
@@ -346,7 +473,7 @@ NSData* getGUID()
|
||||
|
||||
//only init guid once
|
||||
dispatch_once(&onceToken,
|
||||
^{
|
||||
^{
|
||||
|
||||
//get master port
|
||||
kernResult = IOMasterPort(MACH_PORT_NULL, &masterPort);
|
||||
@@ -404,7 +531,7 @@ NSData* getGUID()
|
||||
IOObjectRelease(iterator);
|
||||
|
||||
//convert guid to NSData*
|
||||
// ->also release registry property
|
||||
// also release registry property
|
||||
if(NULL != registryProperty)
|
||||
{
|
||||
//convert
|
||||
@@ -415,7 +542,7 @@ NSData* getGUID()
|
||||
}
|
||||
|
||||
bail:
|
||||
;
|
||||
;
|
||||
|
||||
});//only once
|
||||
|
||||
@@ -423,7 +550,7 @@ bail:
|
||||
}
|
||||
|
||||
//determine if file is signed with Apple Dev ID/cert
|
||||
BOOL isSignedDevID(NSString* binary)
|
||||
BOOL isSignedDevID(NSString* path, SecCSFlags flags)
|
||||
{
|
||||
//flag
|
||||
BOOL signedOK = NO;
|
||||
@@ -438,7 +565,7 @@ BOOL isSignedDevID(NSString* binary)
|
||||
OSStatus status = -1;
|
||||
|
||||
//create static code
|
||||
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:binary]), kSecCSDefaultFlags, &staticCode);
|
||||
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode);
|
||||
if(noErr != status)
|
||||
{
|
||||
//bail
|
||||
@@ -455,16 +582,16 @@ BOOL isSignedDevID(NSString* binary)
|
||||
}
|
||||
|
||||
//check if file is signed w/ apple dev id by checking if it conforms to req string
|
||||
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirementRef);
|
||||
status = SecStaticCodeCheckValidity(staticCode, flags, requirementRef);
|
||||
if(noErr != status)
|
||||
{
|
||||
//bail
|
||||
// ->just means app isn't signed by apple dev id
|
||||
// just means app isn't signed by apple dev id
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//ok, happy
|
||||
// ->file is signed by Apple Dev ID
|
||||
// file is signed by Apple Dev ID
|
||||
signedOK = YES;
|
||||
|
||||
bail:
|
||||
@@ -493,8 +620,8 @@ bail:
|
||||
}
|
||||
|
||||
//determine if a file is from the app store
|
||||
// ->gotta be signed w/ Apple Dev ID & have valid app receipt
|
||||
// note: here, assume this function is only called on Apps signed with Apple Dev ID!
|
||||
// gotta be signed w/ Apple Dev ID & have valid app receipt
|
||||
// note: here, assume this function is only called on Apps signed with Apple Dev ID!
|
||||
BOOL fromAppStore(NSString* path)
|
||||
{
|
||||
//flag
|
||||
@@ -504,16 +631,16 @@ BOOL fromAppStore(NSString* path)
|
||||
AppReceipt* appReceipt = nil;
|
||||
|
||||
//path to app bundle
|
||||
// ->just have binary
|
||||
// just have binary
|
||||
NSBundle* appBundle = nil;
|
||||
|
||||
//if it's an app
|
||||
// ->can directly load app bundle
|
||||
// can directly load app bundle
|
||||
appBundle = [NSBundle bundleWithPath:path];
|
||||
if(nil == appBundle)
|
||||
{
|
||||
//find app bundle from binary
|
||||
// ->likely not an application if this fails
|
||||
// likely not an application if this fails
|
||||
appBundle = PI_findAppBundle(path);
|
||||
if(nil == appBundle)
|
||||
{
|
||||
@@ -523,7 +650,7 @@ BOOL fromAppStore(NSString* path)
|
||||
}
|
||||
|
||||
//bail if it doesn't have an receipt
|
||||
// ->done here, since checking signature is expensive!
|
||||
// done here, since checking signature is expensive!
|
||||
if( (nil == appBundle.appStoreReceiptURL) ||
|
||||
(YES != [[NSFileManager defaultManager] fileExistsAtPath:appBundle.appStoreReceiptURL.path]) )
|
||||
{
|
||||
@@ -532,7 +659,7 @@ BOOL fromAppStore(NSString* path)
|
||||
}
|
||||
|
||||
//init
|
||||
// ->will parse/decode, etc
|
||||
// will parse/decode, etc
|
||||
appReceipt = [[AppReceipt alloc] init:appBundle];
|
||||
if(nil == appReceipt)
|
||||
{
|
||||
@@ -548,10 +675,95 @@ BOOL fromAppStore(NSString* path)
|
||||
}
|
||||
|
||||
//happy
|
||||
// ->app is signed w/ dev ID & its receipt is solid
|
||||
// app is signed w/ dev ID & its receipt is solid
|
||||
appStoreApp = YES;
|
||||
|
||||
bail:
|
||||
|
||||
return appStoreApp;
|
||||
}
|
||||
|
||||
//extact entitlements
|
||||
// note: execs apple's 'codesign' binary
|
||||
NSDictionary* extractEntitlements(NSString* path)
|
||||
{
|
||||
//entitlements
|
||||
NSDictionary* entitlements = nil;
|
||||
|
||||
//results
|
||||
NSMutableDictionary* results = nil;
|
||||
|
||||
//entitlements at bytes
|
||||
unsigned char* entitlementBytes = nil;
|
||||
|
||||
//entitlements as data
|
||||
NSData* entitlementsData = nil;
|
||||
|
||||
//entitlements as xml
|
||||
NSString* entitlementsXML = nil;
|
||||
|
||||
//if path is '/usr/bin/codesign'
|
||||
// this isn't entitled, and to avoid recursive calling, just bail
|
||||
if(YES == [path isEqualToString:CODE_SIGN])
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//exec 'codesign'
|
||||
results = PI_execTask(CODE_SIGN, @[@"-d", @"--entitlements", @"-", path], YES, YES);
|
||||
if(noErr != [results[EXIT_CODE] intValue])
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//not entitled?
|
||||
// could just check for nil, but use offset below
|
||||
if([results[STDOUT] length] < 0x10)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//grab bytes
|
||||
entitlementBytes = (unsigned char*)[results[STDOUT] bytes];
|
||||
|
||||
//codesign has a bug where it returns some (encoding?) bytes first
|
||||
// check for that here, and if found, start string conversion at offset 0x8
|
||||
if(0xFA == entitlementBytes[0])
|
||||
{
|
||||
//convert to string
|
||||
entitlementsXML = [[NSString alloc] initWithData:[results[STDOUT] subdataWithRange:NSMakeRange(0x8, [results[STDOUT] length] - 0x8)] encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
//other just convert as is
|
||||
else
|
||||
{
|
||||
//convert to string
|
||||
entitlementsXML = [[NSString alloc] initWithData:results[STDOUT] encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
//sanity check
|
||||
// make sure conversion to string ok
|
||||
if(0 == [entitlementsXML length])
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//convert to data
|
||||
entitlementsData = [entitlementsXML dataUsingEncoding:NSUTF8StringEncoding];
|
||||
if(nil == entitlementsData)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//convert to dictionary
|
||||
entitlements = [NSPropertyListSerialization propertyListWithData:entitlementsData options:NSPropertyListImmutable format:nil error:nil];
|
||||
|
||||
bail:
|
||||
|
||||
return entitlements;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
NSBundle* PI_findAppBundle(NSString* binaryPath);
|
||||
|
||||
//check if current OS version is supported
|
||||
// ->for now, just...?
|
||||
// for now, just...?
|
||||
BOOL PI_isSupportedOS(void);
|
||||
|
||||
//get OS version
|
||||
@@ -27,7 +27,7 @@ NSDictionary* PI_getOSVersion(void);
|
||||
NSMutableArray* PI_enumerateProcesses(void);
|
||||
|
||||
//given a bundle
|
||||
// ->find its executable
|
||||
// find its executable
|
||||
NSString* PI_findAppBinary(NSString* appPath);
|
||||
|
||||
//sha256 a file
|
||||
@@ -37,4 +37,7 @@ NSString* PI_hashFile(NSString* filePath);
|
||||
// find the full path by scanning $PATH
|
||||
NSString* PI_which(NSString* processName);
|
||||
|
||||
//exec a process with args
|
||||
NSMutableDictionary* PI_execTask(NSString* binaryPath, NSArray* arguments, BOOL shouldWait, BOOL grabOutput);
|
||||
|
||||
#endif
|
||||
|
||||
+165
-13
@@ -15,7 +15,7 @@
|
||||
#import <sys/sysctl.h>
|
||||
|
||||
//disable deprecated warnings
|
||||
// ->use 'Gestalt' as this code may run on old OS X vers.
|
||||
// use 'Gestalt' as this code may run on old OS X vers.
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
//get OS version
|
||||
@@ -81,7 +81,7 @@ bail:
|
||||
}
|
||||
|
||||
//is current OS version supported?
|
||||
// ->for now, just OS X 10.8+
|
||||
// for now, just OS X 10.8+
|
||||
BOOL PI_isSupportedOS()
|
||||
{
|
||||
//support flag
|
||||
@@ -158,7 +158,7 @@ NSMutableArray* PI_enumerateProcesses()
|
||||
}
|
||||
|
||||
//iterate over all pids
|
||||
// ->save pid into return array
|
||||
// save pid into return array
|
||||
for(int i = 0; i < numberOfProcesses; ++i)
|
||||
{
|
||||
//save each pid
|
||||
@@ -185,7 +185,7 @@ bail:
|
||||
}
|
||||
|
||||
//given a path to binary
|
||||
// ->parse it back up to find app's bundle
|
||||
// parse it back up to find app's bundle
|
||||
NSBundle* PI_findAppBundle(NSString* binaryPath)
|
||||
{
|
||||
//app's bundle
|
||||
@@ -204,24 +204,24 @@ NSBundle* PI_findAppBundle(NSString* binaryPath)
|
||||
appBundle = [NSBundle bundleWithPath:appPath];
|
||||
|
||||
//check for match
|
||||
// ->binary path's match
|
||||
// binary path's match
|
||||
if( (nil != appBundle) &&
|
||||
(YES == [appBundle.executablePath isEqualToString:binaryPath]))
|
||||
(YES == [appBundle.executablePath isEqualToString:binaryPath]))
|
||||
{
|
||||
//all done
|
||||
break;
|
||||
}
|
||||
|
||||
//always unset bundle var since it's being returned
|
||||
// ->and at this point, its not a match
|
||||
// and at this point, its not a match
|
||||
appBundle = nil;
|
||||
|
||||
//remove last part
|
||||
// ->will try this next
|
||||
// will try this next
|
||||
appPath = [appPath stringByDeletingLastPathComponent];
|
||||
|
||||
//scan until we get to root
|
||||
// ->of course, loop will be exited if app info dictionary is found/loaded
|
||||
// of course, loop will be exited if app info dictionary is found/loaded
|
||||
} while( (nil != appPath) &&
|
||||
(YES != [appPath isEqualToString:@"/"]) &&
|
||||
(YES != [appPath isEqualToString:@""]) );
|
||||
@@ -229,7 +229,8 @@ NSBundle* PI_findAppBundle(NSString* binaryPath)
|
||||
return appBundle;
|
||||
}
|
||||
|
||||
//sha256 a file
|
||||
//hash a file
|
||||
// algorithm: sha256
|
||||
NSString* PI_hashFile(NSString* filePath)
|
||||
{
|
||||
//file's contents
|
||||
@@ -258,7 +259,7 @@ NSString* PI_hashFile(NSString* filePath)
|
||||
CC_SHA256(fileContents.bytes, (unsigned int)fileContents.length, digestSHA256);
|
||||
|
||||
//convert to NSString
|
||||
// ->iterate over each bytes in computed digest and format
|
||||
// iterate over each bytes in computed digest and format
|
||||
for(index=0; index < CC_SHA256_DIGEST_LENGTH; index++)
|
||||
{
|
||||
//format/append
|
||||
@@ -293,11 +294,11 @@ NSString* PI_which(NSString* processName)
|
||||
pathComponents = [path componentsSeparatedByString:@":"];
|
||||
|
||||
//iterate over all path components
|
||||
// ->build candidate path and check if it exists
|
||||
// build candidate path and check if it exists
|
||||
for(NSString* pathComponent in pathComponents)
|
||||
{
|
||||
//build candidate path
|
||||
// ->current path component + process name
|
||||
// current path component + process name
|
||||
candidateBinary = [pathComponent stringByAppendingPathComponent:processName];
|
||||
|
||||
//check if it exists
|
||||
@@ -318,3 +319,154 @@ NSString* PI_which(NSString* processName)
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
//exec a process with args
|
||||
NSMutableDictionary* PI_execTask(NSString* binaryPath, NSArray* arguments, BOOL shouldWait, BOOL grabOutput)
|
||||
{
|
||||
//task
|
||||
NSTask* task = nil;
|
||||
|
||||
//output pipe for stdout
|
||||
NSPipe* stdOutPipe = nil;
|
||||
|
||||
//output pipe for stderr
|
||||
NSPipe* stdErrPipe = nil;
|
||||
|
||||
//read handle for stdout
|
||||
NSFileHandle* stdOutReadHandle = nil;
|
||||
|
||||
//read handle for stderr
|
||||
NSFileHandle* stdErrReadHandle = nil;
|
||||
|
||||
//results dictionary
|
||||
NSMutableDictionary* results = nil;
|
||||
|
||||
//output for stdout
|
||||
NSMutableData *stdOutData = nil;
|
||||
|
||||
//output for stderr
|
||||
NSMutableData *stdErrData = nil;
|
||||
|
||||
//init dictionary for results
|
||||
results = [NSMutableDictionary dictionary];
|
||||
|
||||
//init task
|
||||
task = [[NSTask alloc] init];
|
||||
|
||||
//only setup pipes if wait flag is set
|
||||
if(YES == grabOutput)
|
||||
{
|
||||
//init stdout pipe
|
||||
stdOutPipe = [NSPipe pipe];
|
||||
|
||||
//init stderr pipe
|
||||
stdErrPipe = [NSPipe pipe];
|
||||
|
||||
//init stdout read handle
|
||||
stdOutReadHandle = [stdOutPipe fileHandleForReading];
|
||||
|
||||
//init stderr read handle
|
||||
stdErrReadHandle = [stdErrPipe fileHandleForReading];
|
||||
|
||||
//init stdout output buffer
|
||||
stdOutData = [NSMutableData data];
|
||||
|
||||
//init stderr output buffer
|
||||
stdErrData = [NSMutableData data];
|
||||
|
||||
//set task's stdout
|
||||
task.standardOutput = stdOutPipe;
|
||||
|
||||
//set task's stderr
|
||||
task.standardError = stdErrPipe;
|
||||
}
|
||||
|
||||
//set task's path
|
||||
task.launchPath = binaryPath;
|
||||
|
||||
//set task's args
|
||||
if(nil != arguments)
|
||||
{
|
||||
//set
|
||||
task.arguments = arguments;
|
||||
}
|
||||
|
||||
//wrap task launch
|
||||
@try
|
||||
{
|
||||
//launch
|
||||
[task launch];
|
||||
}
|
||||
@catch(NSException *exception)
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//no need to wait
|
||||
// can just bail w/ no output
|
||||
if( (YES != shouldWait) &&
|
||||
(YES != grabOutput) )
|
||||
{
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
|
||||
//wait
|
||||
// ...but no output
|
||||
else if( (YES == shouldWait) &&
|
||||
(YES != grabOutput) )
|
||||
{
|
||||
//wait
|
||||
[task waitUntilExit];
|
||||
|
||||
//add exit code
|
||||
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
|
||||
|
||||
//bail
|
||||
goto bail;
|
||||
}
|
||||
|
||||
//grab output?
|
||||
// even if wait not set, still will wait!
|
||||
else
|
||||
{
|
||||
//read in stdout/stderr
|
||||
while(YES == [task isRunning])
|
||||
{
|
||||
//accumulate stdout
|
||||
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
|
||||
|
||||
//accumulate stderr
|
||||
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
|
||||
}
|
||||
|
||||
//grab any leftover stdout
|
||||
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
|
||||
|
||||
//grab any leftover stderr
|
||||
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
|
||||
|
||||
//add stdout
|
||||
if(0 != stdOutData.length)
|
||||
{
|
||||
//add
|
||||
results[STDOUT] = stdOutData;
|
||||
}
|
||||
|
||||
//add stderr
|
||||
if(0 != stdErrData.length)
|
||||
{
|
||||
//add
|
||||
results[STDERR] = stdErrData;
|
||||
}
|
||||
|
||||
//add exit code
|
||||
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
|
||||
}
|
||||
|
||||
bail:
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
+69
-19
@@ -29,6 +29,24 @@
|
||||
#define EVENT_EXEC 27
|
||||
#define EVENT_SPAWN 43190
|
||||
|
||||
//signature status
|
||||
#define KEY_SIGNATURE_STATUS @"signatureStatus"
|
||||
|
||||
//signing auths
|
||||
#define KEY_SIGNING_AUTHORITIES @"signingAuthorities"
|
||||
|
||||
//code signing id
|
||||
#define KEY_SIGNATURE_IDENTIFIER @"signingIdentifier"
|
||||
|
||||
//file belongs to apple?
|
||||
#define KEY_SIGNING_IS_APPLE @"signedByApple"
|
||||
|
||||
//file signed with apple dev id
|
||||
#define KEY_SIGNING_IS_APPLE_DEV_ID @"signedWithDevID"
|
||||
|
||||
//from app store
|
||||
#define KEY_SIGNING_IS_APP_STORE @"fromAppStore"
|
||||
|
||||
/* TYPEDEFS */
|
||||
|
||||
//block for library
|
||||
@@ -39,12 +57,12 @@ typedef void (^ProcessCallbackBlock)(Process* _Nonnull);
|
||||
|
||||
@interface ProcInfo : NSObject
|
||||
|
||||
//init
|
||||
// flag dictates if CPU intensive signing checks should be performed
|
||||
-(id _Nullable )init:(BOOL)skipSigningInfo;
|
||||
//init w/ flag
|
||||
// flag dictates if CPU-intensive logic (code signing, etc) should be preformed
|
||||
-(id _Nullable)init:(BOOL)goEasy;
|
||||
|
||||
//start monitoring
|
||||
-(BOOL)start:(ProcessCallbackBlock _Nonnull )callback;
|
||||
-(void)start:(ProcessCallbackBlock _Nonnull )callback;
|
||||
|
||||
//stop monitoring
|
||||
-(void)stop;
|
||||
@@ -77,26 +95,26 @@ typedef void (^ProcessCallbackBlock)(Process* _Nonnull);
|
||||
@property u_int32_t exit;
|
||||
|
||||
//path
|
||||
@property (nonatomic, retain) NSString* _Nullable path;
|
||||
@property(nonatomic, retain)NSString* _Nullable path;
|
||||
|
||||
//args
|
||||
@property (nonatomic, retain) NSMutableArray* _Nonnull arguments;
|
||||
@property(nonatomic, retain)NSMutableArray* _Nonnull arguments;
|
||||
|
||||
//ancestors
|
||||
@property (nonatomic, retain) NSMutableArray* _Nonnull ancestors;
|
||||
@property(nonatomic, retain)NSMutableArray* _Nonnull ancestors;
|
||||
|
||||
//Binary object
|
||||
// has path, hash, etc
|
||||
@property (nonatomic, retain) Binary* _Nonnull binary;
|
||||
@property(nonatomic, retain)Binary* _Nonnull binary;
|
||||
|
||||
//timestamp
|
||||
@property (nonatomic, retain) NSDate* _Nonnull timestamp;
|
||||
@property(nonatomic, retain)NSDate* _Nonnull timestamp;
|
||||
|
||||
/* METHODS */
|
||||
|
||||
//init with a pid
|
||||
// method will then (try) fill out rest of object
|
||||
-(id _Nullable )init:(pid_t)processID;
|
||||
-(id _Nullable)init:(pid_t)processID;
|
||||
|
||||
//set process's path
|
||||
-(void)pathFromPid;
|
||||
@@ -119,23 +137,36 @@ typedef void (^ProcessCallbackBlock)(Process* _Nonnull);
|
||||
/* PROPERTIES */
|
||||
|
||||
//path
|
||||
@property (nonatomic, retain)NSString* _Nonnull path;
|
||||
@property(nonatomic, retain)NSString* _Nonnull path;
|
||||
|
||||
//name
|
||||
@property (nonatomic, retain)NSString* _Nonnull name;
|
||||
@property(nonatomic, retain)NSString* _Nonnull name;
|
||||
|
||||
//icon
|
||||
@property (nonatomic, retain)NSImage* _Nonnull icon;
|
||||
@property(nonatomic, retain)NSImage* _Nonnull icon;
|
||||
|
||||
//file attributes
|
||||
@property (nonatomic, retain)NSDictionary* _Nullable attributes;
|
||||
@property(nonatomic, retain)NSDictionary* _Nullable attributes;
|
||||
|
||||
//spotlight meta data
|
||||
@property(nonatomic, retain)NSDictionary* _Nullable metadata;
|
||||
|
||||
//bundle
|
||||
// nil for non-apps
|
||||
@property (nonatomic, retain)NSBundle* _Nullable bundle;
|
||||
@property(nonatomic, retain)NSBundle* _Nullable bundle;
|
||||
|
||||
//signing info
|
||||
@property (nonatomic, retain)NSDictionary* _Nonnull signingInfo;
|
||||
@property(nonatomic, retain)NSDictionary* _Nonnull signingInfo;
|
||||
|
||||
//entitlements
|
||||
@property(nonatomic, retain)NSDictionary* _Nonnull entitlements;
|
||||
|
||||
//hash
|
||||
@property(nonatomic, retain)NSString* _Nonnull sha256;
|
||||
|
||||
//identifier
|
||||
// either signing id or sha256 hash
|
||||
@property(nonatomic, retain)NSString* _Nonnull identifier;
|
||||
|
||||
//flag indicating binary belongs to Apple OS
|
||||
@property BOOL isApple;
|
||||
@@ -145,13 +176,32 @@ typedef void (^ProcessCallbackBlock)(Process* _Nonnull);
|
||||
|
||||
/* METHODS */
|
||||
|
||||
//init w/ an info dictionary
|
||||
-(id _Nonnull )init:(NSString* _Nonnull)path;
|
||||
//init w/ a path
|
||||
-(id _Nonnull)init:(NSString* _Nonnull)path;
|
||||
|
||||
/* the following methods are rather CPU-intensive
|
||||
as such, if the proc monitoring is run with the 'goEasy' option, they aren't automatically invoked
|
||||
*/
|
||||
|
||||
//get an icon for a process
|
||||
// for apps, this will be app's icon, otherwise just a standard system one
|
||||
-(void)getIcon;
|
||||
|
||||
//generate signing info
|
||||
// also classifies if Apple/from App Store/etc.
|
||||
-(void)generateSigningInfo;
|
||||
-(void)generateSigningInfo:(SecCSFlags)flags entitlements:(BOOL)entitlements;
|
||||
|
||||
//generate entitlements
|
||||
// note: can also call 'generateSigningInfo' w/ 'entitlements:YES'
|
||||
-(void)generateEntitlements;
|
||||
|
||||
//generate hash
|
||||
-(void)generateHash;
|
||||
|
||||
//generate id
|
||||
// eithersigning id, or sha256 hash
|
||||
// note: will generate signing info if needed
|
||||
-(void)generateIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user