124 lines
8.5 KiB
C
124 lines
8.5 KiB
C
// Diagnostic only: private SecKeychainCreate/Delete/CFRelease, with no certificate, PFX, SDK or CLR.
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <Security/Security.h>
|
|
#include <CommonCrypto/CommonDigest.h>
|
|
#include <mach/mach.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define REPETITIONS 200
|
|
static char root[PATH_MAX];
|
|
static struct timespec started;
|
|
static unsigned created_refs, released_refs, query_releases;
|
|
static double elapsed(void) {
|
|
struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now);
|
|
return (double)(now.tv_sec-started.tv_sec)+(double)(now.tv_nsec-started.tv_nsec)/1e9;
|
|
}
|
|
static void die(const char *message) {
|
|
fprintf(stdout,"{\"kind\":\"failed\",\"message\":\"%s\",\"createdRefs\":%u,\"releasedRefs\":%u,\"elapsedSeconds\":%.6f}\n",message,created_refs,released_refs,elapsed());
|
|
fflush(stdout); exit(1);
|
|
}
|
|
static void hex(const unsigned char *bytes, size_t length, char *out) {
|
|
const char *alphabet="0123456789abcdef";
|
|
for(size_t i=0;i<length;i++){out[i*2]=alphabet[bytes[i]>>4];out[i*2+1]=alphabet[bytes[i]&15];}
|
|
out[length*2]=0;
|
|
}
|
|
static void wipe(void *memory, size_t length) {
|
|
volatile unsigned char *bytes=(volatile unsigned char *)memory;
|
|
while(length--)*bytes++=0;
|
|
}
|
|
static OSStatus keychain_path(SecKeychainRef keychain, char *path) {
|
|
UInt32 length=PATH_MAX-1;
|
|
OSStatus status=SecKeychainGetPath(keychain,&length,path);
|
|
if(status==errSecSuccess && length<PATH_MAX)path[length]=0;
|
|
return status;
|
|
}
|
|
struct fingerprint { char default_sha[65], search_sha[65]; CFIndex search_count; };
|
|
static struct fingerprint fingerprint(const char *stage) {
|
|
SecKeychainRef default_keychain=NULL;
|
|
CFArrayRef search=NULL;
|
|
struct fingerprint out; memset(&out,0,sizeof(out));
|
|
char path[PATH_MAX]; memset(path,0,sizeof(path));
|
|
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
|
OSStatus default_status=SecKeychainCopyDefault(&default_keychain);
|
|
OSStatus default_path_status=default_keychain?keychain_path(default_keychain,path):errSecInvalidKeychain;
|
|
if(default_status==errSecSuccess && default_path_status==errSecSuccess){CC_SHA256(path,(CC_LONG)strlen(path),digest);hex(digest,sizeof(digest),out.default_sha);}
|
|
if(default_keychain){CFRelease(default_keychain);query_releases++;default_keychain=NULL;}
|
|
OSStatus search_status=SecKeychainCopySearchList(&search);
|
|
OSStatus search_path_status=errSecSuccess;
|
|
CC_SHA256_CTX hash; CC_SHA256_Init(&hash);
|
|
if(search_status==errSecSuccess && search){
|
|
out.search_count=CFArrayGetCount(search);
|
|
uint64_t count=(uint64_t)out.search_count; CC_SHA256_Update(&hash,&count,sizeof(count));
|
|
for(CFIndex i=0;i<out.search_count;i++){
|
|
// CFArray values are borrowed. Releasing the owned array balances its element references.
|
|
SecKeychainRef borrowed=(SecKeychainRef)CFArrayGetValueAtIndex(search,i);
|
|
memset(path,0,sizeof(path)); OSStatus status=keychain_path(borrowed,path);
|
|
if(status!=errSecSuccess){search_path_status=status;break;}
|
|
uint32_t length=(uint32_t)strlen(path);CC_SHA256_Update(&hash,&length,sizeof(length));CC_SHA256_Update(&hash,path,length);
|
|
}
|
|
}
|
|
CC_SHA256_Final(digest,&hash);hex(digest,sizeof(digest),out.search_sha);
|
|
if(search){CFRelease(search);query_releases++;search=NULL;}
|
|
fprintf(stdout,"{\"kind\":\"fingerprint\",\"stage\":\"%s\",\"defaultStatus\":%d,\"defaultPathStatus\":%d,\"searchStatus\":%d,\"searchPathStatus\":%d,\"defaultSha256\":\"%s\",\"searchSha256\":\"%s\",\"searchCount\":%ld,\"ownedQueryReferencesReleased\":%u}\n",stage,(int)default_status,(int)default_path_status,(int)search_status,(int)search_path_status,out.default_sha,out.search_sha,(long)out.search_count,query_releases);fflush(stdout);
|
|
if(default_status || default_path_status || search_status || search_path_status)die("Read-only keychain fingerprint failed");
|
|
return out;
|
|
}
|
|
static void checkpoint(const char *stage, unsigned operations) {
|
|
struct mach_task_basic_info info; mach_msg_type_number_t count=MACH_TASK_BASIC_INFO_COUNT;
|
|
kern_return_t status=task_info(mach_task_self(),MACH_TASK_BASIC_INFO,(task_info_t)&info,&count);
|
|
if(status!=KERN_SUCCESS)die("task_info failed");
|
|
struct timespec wall;clock_gettime(CLOCK_REALTIME,&wall);struct tm utc;gmtime_r(&wall.tv_sec,&utc);
|
|
char stamp[40];strftime(stamp,sizeof(stamp),"%Y-%m-%dT%H:%M:%S",&utc);
|
|
fprintf(stdout,"{\"kind\":\"checkpoint\",\"mode\":\"pure-apple\",\"stage\":\"%s\",\"operations\":%u,\"processId\":%d,\"utc\":\"%s.%03ldZ\",\"elapsedSeconds\":%.6f,\"rssBytes\":%llu,\"createdRefs\":%u,\"releasedRefs\":%u,\"managedRuntimePresent\":false}\n",stage,operations,getpid(),stamp,wall.tv_nsec/1000000,elapsed(),(unsigned long long)info.resident_size,created_refs,released_refs);fflush(stdout);
|
|
char next[PATH_MAX];int n=snprintf(next,sizeof(next),"%s/continue-%s",root,stage);if(n<0 || (size_t)n>=sizeof(next))die("Checkpoint path overflow");
|
|
while(access(next,F_OK)!=0){if(elapsed()>180)die("Checkpoint deadline exceeded");struct timespec pause={0,25000000};nanosleep(&pause,NULL);}
|
|
}
|
|
static int absent(const char *path){struct stat state;if(lstat(path,&state)==0)return 0;return errno==ENOENT;}
|
|
static void operation(unsigned iteration) {
|
|
unsigned char random[48];if(SecRandomCopyBytes(kSecRandomDefault,sizeof(random),random)!=errSecSuccess)die("Secure random failed");
|
|
char suffix[33],password[65];hex(random,16,suffix);hex(random+16,32,password);wipe(random,sizeof(random));
|
|
char path[PATH_MAX],backing[PATH_MAX];int n=snprintf(path,sizeof(path),"%s/tmp/native-retention-%s.keychain",root,suffix);
|
|
if(n<0 || (size_t)n>=sizeof(path))die("Fixture path overflow");
|
|
n=snprintf(backing,sizeof(backing),"%s-db",path);if(n<0 || (size_t)n>=sizeof(backing))die("Fixture backing path overflow");
|
|
if(!absent(path)||!absent(backing))die("Generated private fixture path already exists");
|
|
SecKeychainRef keychain=NULL;
|
|
OSStatus create_status=SecKeychainCreate(path,(UInt32)strlen(password),password,false,NULL,&keychain);
|
|
wipe(password,sizeof(password));
|
|
OSStatus path_status=errSecInvalidKeychain,delete_status=INT32_MIN;
|
|
int verified=0,released=0;
|
|
if(keychain){
|
|
created_refs++;
|
|
char actual[PATH_MAX];memset(actual,0,sizeof(actual));path_status=keychain_path(keychain,actual);
|
|
// Only a returned reference whose path is exactly this generated private fixture can be deleted.
|
|
verified=path_status==errSecSuccess && (!strcmp(actual,path)||!strcmp(actual,backing));
|
|
if(verified)delete_status=SecKeychainDelete(keychain);
|
|
// Exactly one CFRelease for this returned owned reference, irrespective of delete status.
|
|
CFRelease(keychain);keychain=NULL;released_refs++;released=1;
|
|
}
|
|
fprintf(stdout,"{\"kind\":\"operation\",\"iteration\":%u,\"createStatus\":%d,\"pathStatus\":%d,\"deleteStatus\":%d,\"returnedRef\":%s,\"privatePathVerified\":%s,\"releasedExactlyOnce\":%s}\n",iteration,(int)create_status,(int)path_status,(int)delete_status,released?"true":"false",verified?"true":"false",released?"true":"false");fflush(stdout);
|
|
if(create_status || path_status || delete_status || !verified || !released)die("Private keychain lifecycle operation failed");
|
|
}
|
|
int main(int argc,char **argv) {
|
|
clock_gettime(CLOCK_MONOTONIC,&started);
|
|
if(argc!=2 || !realpath(argv[1],root))die("Expected existing private fixture root");
|
|
struct stat state;if(stat(root,&state) || !S_ISDIR(state.st_mode) || (state.st_mode&077))die("Fixture root must be mode0700");
|
|
struct fingerprint before=fingerprint("before");checkpoint("before",0);
|
|
for(unsigned i=1;i<=REPETITIONS;i++){operation(i);if(i==1)checkpoint("first",i);}
|
|
checkpoint("repeated",REPETITIONS);
|
|
struct timespec idle={5,0};while(nanosleep(&idle,&idle)!=0){if(errno!=EINTR)die("Idle delay failed");}
|
|
struct fingerprint after=fingerprint("after");
|
|
checkpoint("idle",REPETITIONS);
|
|
int unchanged=!strcmp(before.default_sha,after.default_sha)&&!strcmp(before.search_sha,after.search_sha)&&before.search_count==after.search_count;
|
|
if(!unchanged || created_refs!=REPETITIONS || released_refs!=REPETITIONS)die("Fingerprint or native ownership guard failed");
|
|
fprintf(stdout,"{\"kind\":\"complete\",\"operations\":200,\"createdRefs\":%u,\"releasedRefs\":%u,\"readOnlyDefaultAndSearchListUnchanged\":true,\"elapsedSeconds\":%.6f,\"fullSoakGate\":false}\n",created_refs,released_refs,elapsed());fflush(stdout);return 0;
|
|
}
|