This guide walks through integrating Vigil into your iOS and macOS applications.
- Prerequisites
- Installation
- macOS Integration
- iOS Integration
- Common Setup
- Build Configuration
- Testing
- Troubleshooting
- Xcode 14.0 or later
- macOS 13.0 or later (for development)
- Physical iOS device for testing (Secure Enclave not available on simulators)
- App Groups (for shared Keychain access)
- Hardened Runtime (recommended)
- App Groups
- Network Extension (Content Filter Provider)
- Active Apple Developer Program membership
- Network Extension entitlement (request from Apple for iOS)
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/user/vigil.git", from: "1.0.0")
]Or in Xcode: File → Add Packages → Enter repository URL.
pod 'Vigil', '~> 1.0'- Clone the repository
- Drag
Vigil.xcframeworkinto your project - Add to "Frameworks, Libraries, and Embedded Content"
- Set "Embed" to "Embed & Sign"
- In Xcode, File → New → Target
- Select "XPC Service"
- Name it
VigilValidator - Language: Objective-C or Swift
Important: The XPC service bundle folder name must match the bundle identifier exactly. For example, if your bundle identifier is com.yourteam.app.validator, the XPC bundle must be placed at YourApp.app/Contents/XPCServices/com.yourteam.app.validator.xpc/.
Info.plist for XPC Service:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>VigilValidator</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>XPCService</key>
<dict>
<key>ServiceType</key>
<string>Application</string>
</dict>
</dict>
</plist>VigilValidatorProtocol.h:
#import <Foundation/Foundation.h>
@protocol VigilValidatorProtocol <NSObject>
- (void)validateHash:(NSData *)hash
signature:(NSData *)signature
publicKey:(NSData *)publicKey
nonce:(NSData *)nonce
withReply:(void (^)(BOOL valid,
NSData *validatorHash,
NSData *responseSignature,
NSData *validatorPublicKey))reply;
- (void)exchangePublicKey:(NSData *)appPublicKey
withReply:(void (^)(NSData *validatorPublicKey))reply;
@endVigilValidatorService.m:
#import "VigilValidatorService.h"
#import <Vigil/HashEngine.h>
#import <Vigil/SEKeyManager.h>
#import <Vigil/AttestationStore.h>
@implementation VigilValidatorService
- (void)validateHash:(NSData *)hash
signature:(NSData *)signature
publicKey:(NSData *)publicKey
nonce:(NSData *)nonce
withReply:(void (^)(BOOL, NSData *, NSData *, NSData *))reply {
SEKeyManager *keyManager = [SEKeyManager sharedManager];
AttestationStore *store = [AttestationStore sharedStore];
// Verify the app's signature
NSMutableData *signedData = [hash mutableCopy];
[signedData appendData:nonce];
NSError *error;
BOOL signatureValid = [keyManager verifySignature:signature
forData:signedData
withPublicKey:publicKey
error:&error];
if (!signatureValid) {
reply(NO, nil, nil, nil);
return;
}
// Verify the app's public key matches stored key
NSData *storedAppKey = [store appPublicKey];
if (storedAppKey && ![storedAppKey isEqualToData:publicKey]) {
reply(NO, nil, nil, nil);
return;
}
// Verify hash matches expected value
NSData *expectedHash = [self loadExpectedAppHash];
BOOL hashValid = [hash isEqualToData:expectedHash];
// Compute our own hash
NSData *validatorHash = [HashEngine computeTextHash];
// Sign the response
NSMutableData *responseData = [NSMutableData data];
[responseData appendBytes:&hashValid length:sizeof(BOOL)];
[responseData appendData:validatorHash];
[responseData appendData:nonce];
NSData *responseSignature = [keyManager signData:responseData
withKeyTag:@"com.vigil.validator"
error:&error];
NSData *validatorPublicKey = [keyManager publicKeyDataForTag:@"com.vigil.validator"];
reply(hashValid, validatorHash, responseSignature, validatorPublicKey);
}
- (void)exchangePublicKey:(NSData *)appPublicKey
withReply:(void (^)(NSData *))reply {
AttestationStore *store = [AttestationStore sharedStore];
[store storeAppPublicKey:appPublicKey error:nil];
SEKeyManager *keyManager = [SEKeyManager sharedManager];
NSData *validatorPublicKey = [keyManager publicKeyDataForTag:@"com.vigil.validator"];
reply(validatorPublicKey);
}
- (NSData *)loadExpectedAppHash {
// Load from embedded plist (generated at build time)
NSString *path = [[NSBundle mainBundle]
pathForResource:@"ExpectedHashes" ofType:@"plist"];
NSDictionary *hashes = [NSDictionary dictionaryWithContentsOfFile:path];
NSString *hexHash = hashes[@"app_text_hash"];
return [self dataFromHexString:hexHash];
}
@endmain.m for XPC Service:
#import <Foundation/Foundation.h>
#import "VigilValidatorService.h"
#import "VigilValidatorProtocol.h"
@interface ServiceDelegate : NSObject <NSXPCListenerDelegate>
@end
@implementation ServiceDelegate
- (BOOL)listener:(NSXPCListener *)listener
shouldAcceptNewConnection:(NSXPCConnection *)connection {
connection.exportedInterface = [NSXPCInterface
interfaceWithProtocol:@protocol(VigilValidatorProtocol)];
connection.exportedObject = [[VigilValidatorService alloc] init];
[connection resume];
return YES;
}
@end
int main(int argc, const char *argv[]) {
ServiceDelegate *delegate = [[ServiceDelegate alloc] init];
NSXPCListener *listener = [NSXPCListener serviceListener];
listener.delegate = delegate;
[listener resume];
return 0;
}Both the app and XPC service must share an App Group for Keychain access:
- In Xcode, select your app target
- Signing & Capabilities → + Capability → App Groups
- Add a group:
group.com.yourteam.vigil - Repeat for the XPC Service target
#import <Vigil/Vigil.h>
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
// Initialize Vigil on launch
[Vigil initializeWithCompletion:^(BOOL success, NSError *error) {
if (!success) {
NSLog(@"Vigil initialization failed: %@", error);
// Handle initialization failure
return;
}
// Perform initial validation
[self validateIntegrity];
}];
}
- (void)validateIntegrity {
[Vigil validateWithTimeout:5.0 completion:^(VigilResult result) {
switch (result) {
case VigilResultValid:
NSLog(@"Integrity verified");
break;
case VigilResultTampered:
NSLog(@"Tampering detected!");
[self handleTampering];
break;
case VigilResultTimeout:
NSLog(@"Validator unresponsive - assuming compromise");
[self handleTampering];
break;
case VigilResultError:
NSLog(@"Validation error");
// Retry or handle gracefully
break;
}
}];
}
- (void)handleTampering {
// Options:
// 1. Terminate the app
// 2. Disable sensitive features
// 3. Log to analytics
// 4. Show warning to user
exit(1);
}Network Extension requires approval from Apple:
- Go to developer.apple.com
- Account → Certificates, Identifiers & Profiles
- Identifiers → Your App ID → Edit
- Enable "Network Extensions"
- Request the Content Filter Provider capability
This may take several days for Apple to approve.
- In Xcode, File → New → Target
- Select "Content Filter Extension" (under Network Extension)
- Name it
VigilFilter - Language: Objective-C or Swift
Info.plist for Extension:
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.filter-data</string>
<key>NSExtensionPrincipalClass</key>
<string>FilterDataProvider</string>
</dict>
<key>NEProviderClasses</key>
<dict>
<key>com.apple.networkextension.filter-data</key>
<string>$(PRODUCT_MODULE_NAME).FilterDataProvider</string>
</dict>Entitlements for Extension:
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>content-filter-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.yourteam.vigil</string>
</array>FilterDataProvider.m:
#import "FilterDataProvider.h"
#import <Vigil/HashEngine.h>
#import <Vigil/SEKeyManager.h>
#import <Vigil/AttestationStore.h>
@implementation FilterDataProvider
- (void)startFilterWithCompletionHandler:(void (^)(NSError *))completionHandler {
// Initialize SE key if needed
SEKeyManager *keyManager = [SEKeyManager sharedManager];
if (![keyManager publicKeyDataForTag:@"com.vigil.filter"]) {
[keyManager generateKeyPairWithTag:@"com.vigil.filter" error:nil];
}
// Allow all traffic - we're only using this for the IPC channel
NEFilterSettings *settings = [[NEFilterSettings alloc]
initWithRules:@[]
defaultAction:[NEFilterRule ruleWithNetworkRule:
[[NENetworkRule alloc] initWithRemoteNetwork:nil
remotePrefix:0
localNetwork:nil
localPrefix:0
protocol:NENetworkRuleProtocolAny
direction:NETrafficDirectionAny]]];
[self applySettings:settings completionHandler:^(NSError *error) {
completionHandler(error);
}];
}
- (void)handleNewFlow:(NEFilterFlow *)flow {
// Allow all flows - we're not actually filtering
[flow setValue:@(NEFilterDataVerdictAllow)
forKey:@"verdict"];
}
#pragma mark - Vigil IPC
- (void)handleAppMessage:(NSData *)messageData
completionHandler:(void (^)(NSData *))completionHandler {
// Decode the validation request
NSError *error;
NSDictionary *request = [NSJSONSerialization JSONObjectWithData:messageData
options:0
error:&error];
if (error) {
completionHandler(nil);
return;
}
NSString *action = request[@"action"];
if ([action isEqualToString:@"validate"]) {
[self handleValidation:request completion:completionHandler];
} else if ([action isEqualToString:@"exchangeKey"]) {
[self handleKeyExchange:request completion:completionHandler];
} else {
completionHandler(nil);
}
}
- (void)handleValidation:(NSDictionary *)request
completion:(void (^)(NSData *))completion {
NSData *hash = [[NSData alloc] initWithBase64EncodedString:request[@"hash"]
options:0];
NSData *signature = [[NSData alloc] initWithBase64EncodedString:request[@"signature"]
options:0];
NSData *publicKey = [[NSData alloc] initWithBase64EncodedString:request[@"publicKey"]
options:0];
NSData *nonce = [[NSData alloc] initWithBase64EncodedString:request[@"nonce"]
options:0];
SEKeyManager *keyManager = [SEKeyManager sharedManager];
AttestationStore *store = [AttestationStore sharedStore];
// Verify signature
NSMutableData *signedData = [hash mutableCopy];
[signedData appendData:nonce];
BOOL signatureValid = [keyManager verifySignature:signature
forData:signedData
withPublicKey:publicKey
error:nil];
if (!signatureValid) {
NSDictionary *response = @{@"valid": @NO};
completion([NSJSONSerialization dataWithJSONObject:response
options:0 error:nil]);
return;
}
// Verify stored key
NSData *storedKey = [store appPublicKey];
if (storedKey && ![storedKey isEqualToData:publicKey]) {
NSDictionary *response = @{@"valid": @NO};
completion([NSJSONSerialization dataWithJSONObject:response
options:0 error:nil]);
return;
}
// Verify hash
NSData *expectedHash = [self loadExpectedAppHash];
BOOL hashValid = [hash isEqualToData:expectedHash];
// Compute our hash
NSData *filterHash = [HashEngine computeTextHash];
// Sign response
NSMutableData *responseData = [NSMutableData data];
uint8_t validByte = hashValid ? 1 : 0;
[responseData appendBytes:&validByte length:1];
[responseData appendData:filterHash];
[responseData appendData:nonce];
NSData *responseSignature = [keyManager signData:responseData
withKeyTag:@"com.vigil.filter"
error:nil];
NSData *filterPublicKey = [keyManager publicKeyDataForTag:@"com.vigil.filter"];
NSDictionary *response = @{
@"valid": @(hashValid),
@"filterHash": [filterHash base64EncodedStringWithOptions:0],
@"signature": [responseSignature base64EncodedStringWithOptions:0],
@"publicKey": [filterPublicKey base64EncodedStringWithOptions:0]
};
completion([NSJSONSerialization dataWithJSONObject:response
options:0 error:nil]);
}
@endApp's Entitlements:
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>content-filter-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.yourteam.vigil</string>
</array>The user must enable the Content Filter in Settings. Provide UI to guide them:
#import <NetworkExtension/NetworkExtension.h>
- (void)setupContentFilter {
[[NEFilterManager sharedManager] loadFromPreferencesWithCompletionHandler:
^(NSError *error) {
if (error) {
NSLog(@"Failed to load filter preferences: %@", error);
return;
}
NEFilterManager *manager = [NEFilterManager sharedManager];
if (!manager.enabled) {
// Guide user to Settings
[self showFilterSetupInstructions];
return;
}
// Filter is enabled, proceed with validation
[self validateIntegrity];
}];
}
- (void)showFilterSetupInstructions {
UIAlertController *alert = [UIAlertController
alertControllerWithTitle:@"Setup Required"
message:@"To protect this app, please enable the content filter in "
@"Settings > General > VPN & Device Management > Content Filter"
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction
actionWithTitle:@"Open Settings"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
[[UIApplication sharedApplication] openURL:url
options:@{}
completionHandler:nil];
}]];
[self presentViewController:alert animated:YES completion:nil];
}- (void)validateIntegrity {
NEFilterManager *manager = [NEFilterManager sharedManager];
// Prepare validation request
NSData *hash = [HashEngine computeTextHash];
NSData *nonce = [self generateNonce];
SEKeyManager *keyManager = [SEKeyManager sharedManager];
NSMutableData *signedData = [hash mutableCopy];
[signedData appendData:nonce];
NSData *signature = [keyManager signData:signedData
withKeyTag:@"com.vigil.app"
error:nil];
NSData *publicKey = [keyManager publicKeyDataForTag:@"com.vigil.app"];
NSDictionary *request = @{
@"action": @"validate",
@"hash": [hash base64EncodedStringWithOptions:0],
@"signature": [signature base64EncodedStringWithOptions:0],
@"publicKey": [publicKey base64EncodedStringWithOptions:0],
@"nonce": [nonce base64EncodedStringWithOptions:0]
};
NSData *messageData = [NSJSONSerialization dataWithJSONObject:request
options:0
error:nil];
// Set timeout
__block BOOL responseReceived = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
if (!responseReceived) {
[self handleValidationResult:VigilResultTimeout response:nil];
}
});
// Send to filter extension
NETunnelProviderSession *session =
(NETunnelProviderSession *)manager.connection;
[session sendProviderMessage:messageData
responseHandler:^(NSData *response) {
responseReceived = YES;
[self handleValidationResponse:response nonce:nonce];
}];
}On first launch, generate SE key pairs for both app and validator:
// In app initialization
- (void)initializeVigilKeys {
SEKeyManager *keyManager = [SEKeyManager sharedManager];
// Check if keys exist
if (![keyManager publicKeyDataForTag:@"com.vigil.app"]) {
NSError *error;
BOOL success = [keyManager generateKeyPairWithTag:@"com.vigil.app"
error:&error];
if (!success) {
NSLog(@"Failed to generate app SE key: %@", error);
// Handle error - SE might not be available
}
}
}- (void)performInitialKeyExchange {
AttestationStore *store = [AttestationStore sharedStore];
if ([store isAttestationConfigured]) {
// Already configured
return;
}
SEKeyManager *keyManager = [SEKeyManager sharedManager];
NSData *appPublicKey = [keyManager publicKeyDataForTag:@"com.vigil.app"];
// Send to validator and receive validator's public key
// (Implementation depends on platform - XPC or NE)
[self exchangeKeyWithValidator:appPublicKey
completion:^(NSData *validatorPublicKey) {
[store storeValidatorPublicKey:validatorPublicKey error:nil];
}];
}Add a Run Script phase to compute expected hashes:
#!/bin/bash
# Compute hash after linking, before signing
APP_BINARY="${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}"
OUTPUT_PLIST="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpectedHashes.plist"
if [ -f "$APP_BINARY" ]; then
# Use vigil-hash-tool to compute __TEXT hash
HASH=$("${SRCROOT}/Tools/vigil-hash-tool" --binary "$APP_BINARY")
# Write to plist
/usr/libexec/PlistBuddy -c "Add :app_text_hash string $HASH" "$OUTPUT_PLIST" 2>/dev/null || \
/usr/libexec/PlistBuddy -c "Set :app_text_hash $HASH" "$OUTPUT_PLIST"
echo "Computed app hash: $HASH"
fiIn Debug builds, you may want to disable strict validation:
#ifdef DEBUG
#define VIGIL_STRICT_MODE 0
#else
#define VIGIL_STRICT_MODE 1
#endif
- (void)handleValidationResult:(VigilResult)result {
#if VIGIL_STRICT_MODE
if (result != VigilResultValid) {
[self handleTampering];
}
#else
// Log but don't enforce in debug builds
if (result != VigilResultValid) {
NSLog(@"[Vigil Debug] Validation failed: %ld", (long)result);
}
#endif
}Secure Enclave requires a physical device:
// Check SE availability
if (![SEKeyManager isSecureEnclaveAvailable]) {
// Running on simulator - use mock implementation
return;
}// Test valid state
[Vigil validateWithTimeout:5.0 completion:^(VigilResult result) {
XCTAssertEqual(result, VigilResultValid);
}];
// Test timeout (stop validator before calling)
[Vigil validateWithTimeout:1.0 completion:^(VigilResult result) {
XCTAssertEqual(result, VigilResultTimeout);
}];- Cause: Running on simulator or unsupported device
- Solution: Test on physical device with Secure Enclave (iPhone 5s or later)
- Cause: XPC service crashed or bundle ID mismatch
- Solution: Verify bundle identifiers match, check Console.app for crash logs
- Cause: User hasn't enabled the Content Filter
- Solution: Guide user to Settings, check
NEFilterManager.enabled
- Cause: SE key generation failed or app was re-installed
- Solution: Regenerate keys and perform key exchange
- Cause: Missing Keychain entitlements for key storage
- Solution: Both app and XPC service require the
keychain-access-groupsentitlement:<key>keychain-access-groups</key> <array> <string>$(AppIdentifierPrefix)com.yourteam.app</string> </array>
- Cause: Binary was modified or ExpectedHashes.plist is stale
- Solution: Rebuild with correct hash computation phase
Enable verbose logging:
[Vigil setLogLevel:VigilLogLevelDebug];Check Console.app for logs with subsystem com.vigil.