Use IcliKit for guest app installation

This commit is contained in:
Lakr
2026-09-25 00:34:39 +09:00
parent 9e7d820412
commit a624c632ec
15 changed files with 282 additions and 1101 deletions
+6 -2
View File
@@ -24,6 +24,10 @@ The guest links IcliKit directly. App registration refresh is available through
JPEG with `mime_type`, `width`, and `height`; the current VM produces 1290×2796.
The host's Save/Copy Screenshot menu decodes this guest image. It omits the
notch and cutout drawn by the host VM window.
`apps.install` accepts IPA and TIPA archives. IcliKit 0.6.6 validates and
extracts the archive, then calls vphone's signer on the temporary app bundle
before IcliKit copies it into a container, registers it, and owns rollback.
`apps.uninstall` delegates removal to IcliKit and requires `force=true`.
## HTTP and WebSocket contract
@@ -35,7 +39,7 @@ directory then renames it after all chunks have been written. JSON bodies
have a 1 MiB limit. Binary transfers stream without loading the entire file
into memory.
`apps.launch` returns a PID and `frontmost_verified`. IcliKit 0.6.5 checks
`apps.launch` returns a PID and `frontmost_verified`. IcliKit 0.6.6 checks
RunningBoard's live focal assertion and accepts it only when one real app owns
it. iOS 26.6.2 uses `SuspendableRole-UIFocal`; older systems may use
`Workspace-ForegroundFocal`. The Home screen's widget renderer can also hold
@@ -62,7 +66,7 @@ correlate them by `id`. The socket also sends
receive pong frames. JSON WebSocket frames are limited to 1 MiB after
fragment reassembly.
SwiftNIO handles parsing, upgrade, masking, and backpressure. IcliKit 0.6.5
SwiftNIO handles parsing, upgrade, masking, and backpressure. IcliKit 0.6.6
owns general device operations. Each HTTP or WebSocket request runs independently
on a concurrent worker queue, so a stalled system service does not block HID,
file browsing, or unrelated requests. The host serializes the input events it
@@ -1,5 +1,5 @@
{
"originHash" : "f64f0141522e4fc1ff1b3bb6ad935f5a786d14cb195f34f75733bb0b4c35ac72",
"originHash" : "01b56dbe07461c056f9f787f47fe980bea82d0877db7a293439df58ce597d835",
"pins" : [
{
"identity" : "applemobiledevicelibrary",
@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/owngoal-dev/icli.git",
"state" : {
"revision" : "bbb4d238bb06285dacb5385e372b5e990398d688",
"version" : "0.6.5"
"revision" : "0bc454f61f54d99439e0f8a7f9e1c43f0fb49dc2",
"version" : "0.6.6"
}
},
{
+26 -17
View File
@@ -20,8 +20,8 @@ enum GuestAPIError: Error, CustomStringConvertible {
}
/// The API boundary is deliberately small: named operations and JSON values.
/// IcliKit owns general device work, including Keychain metadata. Only
/// vphone-specific installation crosses into the native daemon code.
/// IcliKit owns general device work, including app installation and Keychain
/// metadata. vphone signs app code before IcliKit installs it.
enum GuestAPI {
// Each request executes independently. A synchronous system service such
// as powerd may wait during boot; it must not hold up HID or file requests.
@@ -149,6 +149,8 @@ enum GuestAPI {
]
case "apps.terminate":
return try killApp(string(params, "bundle_id"), force: true)
case "apps.uninstall":
return try uninstallApp(string(params, "bundle_id"), force: params["force"] as? Bool == true)
case "apps.foreground":
let front = frontmostApp()
let id = front["bundle_id"] as? String ?? ""
@@ -167,12 +169,19 @@ enum GuestAPI {
case "apps.open_url":
return try openAppURL(string(params, "url"), bundleID: params["bundle_id"] as? String)
case "apps.install":
return try native([
"t": "ipa_install",
"path": string(params, "path"),
"registration": params["registration"] as? String ?? "User",
"cert_path": params["cert_path"] as? String ?? "",
])
let path = try string(params, "path")
let certificate = params["cert_path"] as? String ?? ""
let registration: AppRegistrationType = params["registration"] as? String == "System" ? .system : .user
defer {
try? FileManager.default.removeItem(atPath: path)
if !certificate.isEmpty { try? FileManager.default.removeItem(atPath: certificate) }
}
var result = try installIPAInContainer(path, registration: registration) { app in
try signAppForInstall(app, certificate: certificate)
}
let id = result["bundle_id"] as? String ?? "app"
result["msg"] = "Installed \(id) as a \(registration.rawValue) app."
return result
case "input.touch":
guard let phase = (params["phase"] as? String).flatMap(TouchPhase.init(rawValue:)) else {
throw GuestAPIError.invalidRequest("phase must be down, move or up")
@@ -285,16 +294,16 @@ enum GuestAPI {
}
}
private static func native(_ message: [String: Any]) throws -> [String: Any] {
let result = vp_native_api_command(message) as? [String: Any] ?? [:]
if result["t"] as? String == "err" {
throw GuestAPIError.operationFailed(result["msg"] as? String ?? "Guest operation failed")
private static func signAppForInstall(_ app: String, certificate: String) throws {
let usableCertificate = FileManager.default.fileExists(atPath: certificate) ? certificate : ""
let error = app.withCString { appPath in
if usableCertificate.isEmpty { return vp_sign_app_for_install(appPath, nil) }
return usableCertificate.withCString { vp_sign_app_for_install(appPath, $0) }
}
if let error {
defer { free(error) }
throw GuestAPIError.operationFailed(String(cString: error))
}
var payload = result
payload.removeValue(forKey: "v")
payload.removeValue(forKey: "t")
payload.removeValue(forKey: "id")
return payload
}
private static func fileList(_ path: String) throws -> [String: Any] {
+1 -1
View File
@@ -2,7 +2,7 @@ import Darwin
import Foundation
import VPhoneSign
/// The native IPA installer prepares each executable's guest entitlements.
/// vphone prepares each executable's guest entitlements before IcliKit installs it.
/// Sign them with the same bundled signer used by the host CLI, without a
/// package-manager supplied ldid executable inside the VM.
@_cdecl("vp_guest_sign_binary")
+2 -2
View File
@@ -1,8 +1,8 @@
#pragma once
#import <Foundation/Foundation.h>
/// vphone-specific operations that IcliKit does not own.
NSDictionary *vp_native_api_command(NSDictionary *message);
/// Sign all executable code in an extracted app. Returns a malloc-owned error or NULL.
char *vp_sign_app_for_install(const char *appPath, const char *certificatePath);
void vp_native_bootstrap_cached_binary(void);
void vp_native_confirm_cached_binary(void);
void vp_vcam_start(void);
-82
View File
@@ -1,82 +0,0 @@
/*-
* Copyright (c) 2003-2010 Tim Kientzle
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Minimal vendored header — only the API surface used by unarchive.m.
* Linked against iOS system libarchive (-larchive).
*/
#ifndef ARCHIVE_H_INCLUDED
#define ARCHIVE_H_INCLUDED
#include <sys/types.h>
#include <stddef.h>
#include <stdint.h>
#include <unistd.h>
typedef int64_t la_int64_t;
typedef ssize_t la_ssize_t;
struct archive;
struct archive_entry;
/* Status codes */
#define ARCHIVE_EOF 1
#define ARCHIVE_OK 0
#define ARCHIVE_WARN (-20)
/* Extract flags */
#define ARCHIVE_EXTRACT_TIME 0x0004
#define ARCHIVE_EXTRACT_PERM 0x0002
#define ARCHIVE_EXTRACT_ACL 0x0020
#define ARCHIVE_EXTRACT_FFLAGS 0x0040
#define ARCHIVE_EXTRACT_SECURE_SYMLINKS 0x0100
#define ARCHIVE_EXTRACT_SECURE_NODOTDOT 0x0200
#define ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS 0x10000
/* Error string */
const char *archive_error_string(struct archive *);
/* Read API */
struct archive *archive_read_new(void);
int archive_read_support_format_all(struct archive *);
int archive_read_support_filter_all(struct archive *);
int archive_read_open_filename(struct archive *, const char *filename, size_t block_size);
int archive_read_next_header(struct archive *, struct archive_entry **);
int archive_read_data_block(struct archive *, const void **buf, size_t *size, la_int64_t *offset);
int archive_read_close(struct archive *);
int archive_read_free(struct archive *);
/* Write-to-disk API */
struct archive *archive_write_disk_new(void);
int archive_write_disk_set_options(struct archive *, int flags);
int archive_write_disk_set_standard_lookup(struct archive *);
int archive_write_header(struct archive *, struct archive_entry *);
int archive_write_data_block(struct archive *, const void *buf, size_t size, la_int64_t offset);
int archive_write_finish_entry(struct archive *);
int archive_write_close(struct archive *);
int archive_write_free(struct archive *);
#endif /* !ARCHIVE_H_INCLUDED */
-47
View File
@@ -1,47 +0,0 @@
/*-
* Copyright (c) 2003-2008 Tim Kientzle
* Copyright (c) 2016 Martin Matuska
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Minimal vendored header — only the API surface used by unarchive.m.
* Linked against iOS system libarchive (-larchive).
*/
#ifndef ARCHIVE_ENTRY_H_INCLUDED
#define ARCHIVE_ENTRY_H_INCLUDED
#include <stdint.h>
#ifndef la_int64_t
typedef int64_t la_int64_t;
#endif
struct archive_entry;
const char *archive_entry_pathname(struct archive_entry *);
void archive_entry_set_pathname(struct archive_entry *, const char *);
la_int64_t archive_entry_size(struct archive_entry *);
#endif /* !ARCHIVE_ENTRY_H_INCLUDED */
-3
View File
@@ -1,3 +0,0 @@
#import <Foundation/Foundation.h>
extern int vp_extract_archive(NSString *archivePath, NSString *extractionPath, NSString **errorOutput);
-107
View File
@@ -1,107 +0,0 @@
#import "unarchive.h"
#include <archive.h>
#include <archive_entry.h>
static int copy_data(struct archive *ar, struct archive *aw) {
const void *buff;
size_t size;
la_int64_t offset;
for (;;) {
int r = archive_read_data_block(ar, &buff, &size, &offset);
if (r == ARCHIVE_EOF) return ARCHIVE_OK;
if (r < ARCHIVE_OK) return r;
r = archive_write_data_block(aw, buff, size, offset);
if (r < ARCHIVE_OK) {
fprintf(stderr, "%s\n", archive_error_string(aw));
return r;
}
}
}
int vp_extract_archive(NSString *archivePath, NSString *extractionPath, NSString **errorOutput) {
int flags = ARCHIVE_EXTRACT_TIME
| ARCHIVE_EXTRACT_PERM
| ARCHIVE_EXTRACT_SECURE_NODOTDOT;
// Resolve symlinks in extractionPath (e.g. /tmp -> /private/tmp on iOS)
// so ARCHIVE_EXTRACT_SECURE_SYMLINKS doesn't reject trusted system symlinks.
NSString *resolvedPath = [extractionPath stringByResolvingSymlinksInPath];
NSLog(@"vphoned: extract %@ -> %@ (resolved: %@)", archivePath, extractionPath, resolvedPath);
struct archive *a = archive_read_new();
archive_read_support_format_all(a);
archive_read_support_filter_all(a);
struct archive *ext = archive_write_disk_new();
archive_write_disk_set_options(ext, flags);
archive_write_disk_set_standard_lookup(ext);
int ret = 0;
if (archive_read_open_filename(a, archivePath.fileSystemRepresentation, 10240) != ARCHIVE_OK) {
NSString *err = [NSString stringWithFormat:@"Unable to open the app package (%s).", archive_error_string(a)];
NSLog(@"vphoned: %@", err);
if (errorOutput) *errorOutput = err;
ret = 1;
goto cleanup;
}
for (;;) {
struct archive_entry *entry;
int r = archive_read_next_header(a, &entry);
if (r == ARCHIVE_EOF) break;
if (r < ARCHIVE_OK)
NSLog(@"vphoned: archive_read_next_header: %s", archive_error_string(a));
if (r < ARCHIVE_WARN) {
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"Unable to read the app package (%s).",
archive_error_string(a)];
ret = 1; goto cleanup;
}
const char *entryPath = archive_entry_pathname(entry);
if (!entryPath) { ret = 1; goto cleanup; }
NSString *currentFile = [NSString stringWithUTF8String:entryPath];
if (!currentFile) { ret = 1; goto cleanup; }
NSString *fullOutputPath = [resolvedPath stringByAppendingPathComponent:currentFile];
archive_entry_set_pathname(entry, fullOutputPath.fileSystemRepresentation);
r = archive_write_header(ext, entry);
if (r < ARCHIVE_OK)
NSLog(@"vphoned: archive_write_header(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
if (r < ARCHIVE_WARN) {
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"Unable to extract %@ (%s).",
currentFile,
archive_error_string(ext)];
ret = 1; goto cleanup;
}
if (archive_entry_size(entry) > 0) {
r = copy_data(a, ext);
if (r < ARCHIVE_OK)
NSLog(@"vphoned: copy_data(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
if (r < ARCHIVE_WARN) {
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"Unable to extract %@ (%s).",
currentFile,
archive_error_string(ext)];
ret = 1; goto cleanup;
}
}
r = archive_write_finish_entry(ext);
if (r < ARCHIVE_OK)
NSLog(@"vphoned: archive_write_finish_entry(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
if (r < ARCHIVE_WARN) {
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"Unable to extract %@ (%s).",
currentFile,
archive_error_string(ext)];
ret = 1; goto cleanup;
}
}
cleanup:
archive_read_close(a);
archive_read_free(a);
archive_write_close(ext);
archive_write_free(ext);
return ret;
}
-4
View File
@@ -1,4 +0,0 @@
#import <Foundation/Foundation.h>
BOOL vp_custom_installer_available(void);
NSDictionary *vp_handle_custom_install(NSDictionary *msg);
-804
View File
@@ -1,804 +0,0 @@
#import "vphoned_install.h"
#import "unarchive.h"
#import <Security/Security.h>
#include <dlfcn.h>
#include <errno.h>
#include <mach-o/fat.h>
#include <mach-o/loader.h>
#include <sys/stat.h>
#include <unistd.h>
#import "vphoned_response.h"
typedef struct __SecCode const *SecStaticCodeRef;
typedef CF_OPTIONS(uint32_t, SecCSFlags) {
kSecCSDefaultFlags = 0
};
#define kSecCSRequirementInformation (1 << 2)
OSStatus SecStaticCodeCreateWithPathAndAttributes(
CFURLRef path,
SecCSFlags flags,
CFDictionaryRef attributes,
SecStaticCodeRef *staticCode
);
OSStatus SecCodeCopySigningInformation(SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *information);
extern CFStringRef kSecCodeInfoEntitlementsDict;
@interface LSApplicationProxy : NSObject
+ (instancetype)applicationProxyForIdentifier:(NSString *)identifier;
@property (nonatomic, readonly) NSString *bundleIdentifier;
@property (nonatomic, readonly) NSURL *bundleURL;
@property (getter=isInstalled, nonatomic, readonly) BOOL installed;
@end
@interface LSApplicationWorkspace : NSObject
+ (instancetype)defaultWorkspace;
- (BOOL)registerApplicationDictionary:(NSDictionary *)dict;
- (BOOL)registerContainerizedApplicationWithInfoDictionaries:(NSArray *)infos
operationUUID:(NSUUID *)uuid
requestContext:(id)context
saveObserver:(id)observer
registrationError:(NSError **)error;
- (BOOL)unregisterApplication:(id)arg1;
@end
@interface LSEnumerator : NSEnumerator
@property (nonatomic, copy) NSPredicate *predicate;
+ (instancetype)enumeratorForApplicationProxiesWithOptions:(NSUInteger)options;
@end
@interface MCMContainer : NSObject
+ (id)containerWithIdentifier:(id)arg1 createIfNecessary:(BOOL)arg2 existed:(BOOL *)arg3 error:(id *)arg4;
@property (nonatomic, readonly) NSURL *url;
@end
static NSString *const VPManagedMarker = @"_VPhone";
// Implemented in GuestSigner.swift using the shared VPhoneSign target.
// A non-null result is a malloc-owned error message.
extern char *vp_guest_sign_binary(const char *path, const char *entitlementsPath, const char *certificatePath);
static void vp_load_private_frameworks(void) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dlopen("/System/Library/PrivateFrameworks/MobileContainerManager.framework/MobileContainerManager", RTLD_NOW);
dlopen("/System/Library/Frameworks/CoreServices.framework/CoreServices", RTLD_NOW);
});
}
static NSString *vp_trimmed_output(NSString *string) {
NSString *trimmed = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (trimmed.length > 4000) {
return [trimmed substringToIndex:4000];
}
return trimmed;
}
static NSDictionary *vp_info_dictionary_for_app_path(NSString *appPath) {
if (appPath.length == 0) return nil;
return [NSDictionary dictionaryWithContentsOfFile:[appPath stringByAppendingPathComponent:@"Info.plist"]];
}
static NSString *vp_app_id_for_app_path(NSString *appPath) {
return vp_info_dictionary_for_app_path(appPath)[@"CFBundleIdentifier"];
}
static NSString *vp_app_main_executable_path_for_app_path(NSString *appPath) {
NSDictionary *info = vp_info_dictionary_for_app_path(appPath);
NSString *executable = info[@"CFBundleExecutable"];
if (executable.length == 0) return nil;
return [appPath stringByAppendingPathComponent:executable];
}
static NSString *vp_find_app_name_in_bundle_path(NSString *bundlePath) {
NSArray<NSString *> *bundleItems = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:bundlePath error:nil];
for (NSString *bundleItem in bundleItems) {
if ([bundleItem.pathExtension isEqualToString:@"app"]) {
return bundleItem;
}
}
return nil;
}
static NSString *vp_find_app_path_in_bundle_path(NSString *bundlePath) {
NSString *appName = vp_find_app_name_in_bundle_path(bundlePath);
if (appName.length == 0) return nil;
return [bundlePath stringByAppendingPathComponent:appName];
}
static NSURL *vp_find_app_url_in_bundle_url(NSURL *bundleURL) {
NSString *appName = vp_find_app_name_in_bundle_path(bundleURL.path);
if (appName.length == 0) return nil;
return [bundleURL URLByAppendingPathComponent:appName];
}
static BOOL vp_is_macho_file(NSString *filePath) {
FILE *file = fopen(filePath.fileSystemRepresentation, "r");
if (!file) return NO;
uint32_t magic = 0;
fread(&magic, sizeof(uint32_t), 1, file);
fclose(file);
return magic == FAT_MAGIC || magic == FAT_CIGAM || magic == MH_MAGIC_64 || magic == MH_CIGAM_64;
}
static void vp_fix_permissions_of_app_bundle(NSString *appBundlePath) {
NSURL *fileURL = nil;
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appBundlePath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
chown(filePath.fileSystemRepresentation, 33, 33);
chmod(filePath.fileSystemRepresentation, 0644);
}
enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appBundlePath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
BOOL isDir = NO;
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDir];
if (isDir || vp_is_macho_file(filePath)) {
chmod(filePath.fileSystemRepresentation, 0755);
}
}
}
static SecStaticCodeRef vp_get_static_code_ref(NSString *binaryPath) {
if (binaryPath.length == 0) return NULL;
CFURLRef binaryURL = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
(__bridge CFStringRef)binaryPath,
kCFURLPOSIXPathStyle,
false
);
if (binaryURL == NULL) return NULL;
SecStaticCodeRef codeRef = NULL;
OSStatus result = SecStaticCodeCreateWithPathAndAttributes(binaryURL, kSecCSDefaultFlags, NULL, &codeRef);
CFRelease(binaryURL);
if (result != errSecSuccess) {
return NULL;
}
return codeRef;
}
static NSDictionary *vp_dump_entitlements_from_binary_at_path(NSString *binaryPath) {
SecStaticCodeRef codeRef = vp_get_static_code_ref(binaryPath);
if (codeRef == NULL) return nil;
CFDictionaryRef signingInfo = NULL;
OSStatus result = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &signingInfo);
CFRelease(codeRef);
if (result != errSecSuccess || signingInfo == NULL) {
if (signingInfo) CFRelease(signingInfo);
return nil;
}
NSDictionary *entitlementsNSDict = nil;
CFDictionaryRef entitlements = CFDictionaryGetValue(signingInfo, kSecCodeInfoEntitlementsDict);
if (entitlements && CFGetTypeID(entitlements) == CFDictionaryGetTypeID()) {
entitlementsNSDict = [(__bridge NSDictionary *)entitlements copy];
}
CFRelease(signingInfo);
return entitlementsNSDict;
}
static int vp_sign_binary(
NSString *filePath,
NSDictionary *entitlements,
NSString *certPath,
NSString **errorOutput
) {
NSString *entitlementsPath = nil;
NSData *entitlementsXML = entitlements ? [NSPropertyListSerialization
dataWithPropertyList:entitlements
format:NSPropertyListXMLFormat_v1_0
options:0
error:nil] : nil;
if (entitlementsXML) {
entitlementsPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]
stringByAppendingPathExtension:@"plist"];
if (![entitlementsXML writeToFile:entitlementsPath atomically:YES]) {
if (errorOutput) *errorOutput = @"Could not prepare app entitlements.";
return EIO;
}
}
char *error = vp_guest_sign_binary(
filePath.fileSystemRepresentation,
entitlementsPath.fileSystemRepresentation,
certPath.length > 0 ? certPath.fileSystemRepresentation : NULL
);
if (entitlementsPath) {
[[NSFileManager defaultManager] removeItemAtPath:entitlementsPath error:nil];
}
if (!error) return 0;
if (errorOutput) *errorOutput = [NSString stringWithUTF8String:error] ?: @"Could not sign app executable.";
free(error);
return EINVAL;
}
static int vp_sign_app(NSString *appPath, NSString *certPath, NSString **errorOutput) {
if (!vp_info_dictionary_for_app_path(appPath)) {
if (errorOutput) *errorOutput = @"The app package is incomplete and cannot be signed.";
return 172;
}
NSString *mainExecutablePath = vp_app_main_executable_path_for_app_path(appPath);
if (mainExecutablePath.length == 0 || ![[NSFileManager defaultManager] fileExistsAtPath:mainExecutablePath]) {
if (errorOutput) *errorOutput = @"The app package is missing its program and cannot be signed.";
return 174;
}
NSMutableSet<NSString *> *signedExecutables = [NSMutableSet set];
NSURL *fileURL = nil;
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appPath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
if (![filePath.lastPathComponent isEqualToString:@"Info.plist"]) {
continue;
}
NSDictionary *infoDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSString *bundleId = infoDict[@"CFBundleIdentifier"];
NSString *bundleExecutable = infoDict[@"CFBundleExecutable"];
if (bundleId.length == 0 || bundleExecutable.length == 0) {
continue;
}
NSString *bundleMainExecutablePath = [[filePath stringByDeletingLastPathComponent]
stringByAppendingPathComponent:bundleExecutable];
if (![[NSFileManager defaultManager] fileExistsAtPath:bundleMainExecutablePath]) {
continue;
}
NSString *packageType = infoDict[@"CFBundlePackageType"];
if ([packageType isEqualToString:@"FMWK"]) {
continue;
}
NSMutableDictionary *entitlementsToUse =
[vp_dump_entitlements_from_binary_at_path(bundleMainExecutablePath) mutableCopy];
if (!entitlementsToUse && [bundleMainExecutablePath isEqualToString:mainExecutablePath]) {
entitlementsToUse = [@{
@"application-identifier": @"TROLLTROLL.*",
@"com.apple.developer.team-identifier": @"TROLLTROLL",
@"get-task-allow": @YES,
@"keychain-access-groups": @[@"TROLLTROLL.*", @"com.apple.token"],
} mutableCopy];
}
if (!entitlementsToUse) {
entitlementsToUse = [NSMutableDictionary dictionary];
}
NSObject *containerRequired = entitlementsToUse[@"com.apple.private.security.container-required"];
BOOL shouldWriteContainerRequired = YES;
if ([containerRequired isKindOfClass:[NSString class]]) {
shouldWriteContainerRequired = NO;
} else if ([containerRequired isKindOfClass:[NSNumber class]]) {
shouldWriteContainerRequired = [(NSNumber *)containerRequired boolValue];
}
BOOL noContainer =
[entitlementsToUse[@"com.apple.private.security.no-container"] respondsToSelector:@selector(boolValue)]
? [entitlementsToUse[@"com.apple.private.security.no-container"] boolValue]
: NO;
BOOL noSandbox =
[entitlementsToUse[@"com.apple.private.security.no-sandbox"] respondsToSelector:@selector(boolValue)]
? [entitlementsToUse[@"com.apple.private.security.no-sandbox"] boolValue]
: NO;
if (shouldWriteContainerRequired && !noContainer && !noSandbox) {
entitlementsToUse[@"com.apple.private.security.container-required"] = bundleId;
}
entitlementsToUse[@"jb.pmap_cs_custom_trust"] = @"PMAP_CS_APP_STORE";
NSString *signOutput = @"";
int ret = vp_sign_binary(bundleMainExecutablePath, entitlementsToUse, certPath, &signOutput);
if (ret != 0) {
if (errorOutput) *errorOutput = signOutput;
return 173;
}
[signedExecutables addObject:bundleMainExecutablePath];
}
// Sign code without an Info.plist executable declaration, such as dylibs.
// The declared executables above already carry their guest entitlements.
enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appPath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
if ([signedExecutables containsObject:filePath] || !vp_is_macho_file(filePath)) continue;
NSString *signOutput = @"";
if (vp_sign_binary(filePath, nil, certPath, &signOutput) != 0) {
if (errorOutput) *errorOutput = signOutput;
return 173;
}
}
return 0;
}
static NSDictionary *vp_construct_groups_containers_for_entitlements(NSDictionary *entitlements, BOOL systemGroups) {
if (!entitlements) return nil;
NSString *entitlementForGroups = systemGroups
? @"com.apple.security.system-groups"
: @"com.apple.security.application-groups";
Class mcmClass = NSClassFromString(systemGroups ? @"MCMSystemDataContainer" : @"MCMSharedDataContainer");
if (!mcmClass) return nil;
NSArray *groupIDs = entitlements[entitlementForGroups];
if (![groupIDs isKindOfClass:[NSArray class]]) return nil;
NSMutableDictionary *groupContainers = [NSMutableDictionary dictionary];
for (NSString *groupID in groupIDs) {
MCMContainer *container = [mcmClass
containerWithIdentifier:groupID
createIfNecessary:YES
existed:nil
error:nil];
if (container.url.path.length > 0) {
groupContainers[groupID] = container.url.path;
}
}
return groupContainers.count > 0 ? groupContainers.copy : nil;
}
static BOOL vp_construct_containerization_for_entitlements(NSDictionary *entitlements, NSString **customContainerOut) {
NSNumber *noContainer = entitlements[@"com.apple.private.security.no-container"];
if ([noContainer isKindOfClass:[NSNumber class]] && noContainer.boolValue) {
return NO;
}
NSObject *containerRequired = entitlements[@"com.apple.private.security.container-required"];
if ([containerRequired isKindOfClass:[NSNumber class]] && ![(NSNumber *)containerRequired boolValue]) {
return NO;
}
if ([containerRequired isKindOfClass:[NSString class]]) {
*customContainerOut = (NSString *)containerRequired;
}
return YES;
}
static NSString *vp_construct_team_identifier_for_entitlements(NSDictionary *entitlements) {
NSString *teamIdentifier = entitlements[@"com.apple.developer.team-identifier"];
return [teamIdentifier isKindOfClass:[NSString class]] ? teamIdentifier : nil;
}
static NSDictionary *vp_construct_environment_variables_for_container_path(
NSString *containerPath,
BOOL isContainerized
) {
NSString *homeDir = isContainerized ? containerPath : @"/var/mobile";
NSString *tmpDir = isContainerized ? [containerPath stringByAppendingPathComponent:@"tmp"] : @"/var/tmp";
return @{
@"CFFIXED_USER_HOME": homeDir,
@"HOME": homeDir,
@"TMPDIR": tmpDir,
};
}
static NSSet<NSString *> *vp_immutable_app_bundle_identifiers(void) {
NSMutableSet<NSString *> *systemAppIdentifiers = [NSMutableSet set];
LSEnumerator *enumerator = [(id)NSClassFromString(@"LSEnumerator") enumeratorForApplicationProxiesWithOptions:0];
LSApplicationProxy *appProxy = nil;
while ((appProxy = [enumerator nextObject])) {
if (appProxy.installed && ![appProxy.bundleURL.path hasPrefix:@"/private/var/containers"]) {
[systemAppIdentifiers addObject:appProxy.bundleIdentifier.lowercaseString];
}
}
return systemAppIdentifiers.copy;
}
/// Build the LaunchServices registration dictionary shared by an app bundle and its PlugIns.
/// The caller adds the keys that differ: ApplicationType, Path, and the app- or plugin-only keys.
static NSMutableDictionary *vp_registration_dictionary(
NSString *bundleID,
NSString *executablePath,
Class containerClass
) {
NSDictionary *entitlements = vp_dump_entitlements_from_binary_at_path(executablePath);
NSString *dataContainerID = bundleID;
BOOL containerized = vp_construct_containerization_for_entitlements(entitlements ?: @{}, &dataContainerID);
MCMContainer *dataContainer = [containerClass
containerWithIdentifier:dataContainerID
createIfNecessary:YES
existed:nil
error:nil];
NSString *containerPath = dataContainer.url.path;
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
if (entitlements) {
dict[@"Entitlements"] = entitlements;
}
dict[@"CFBundleIdentifier"] = bundleID;
dict[@"CodeInfoIdentifier"] = bundleID;
dict[@"CompatibilityState"] = @0;
dict[@"IsContainerized"] = @(containerized);
if (containerPath.length > 0) {
dict[@"Container"] = containerPath;
dict[@"EnvironmentVariables"] =
vp_construct_environment_variables_for_container_path(containerPath, containerized);
}
dict[@"SignerOrganization"] = @"Apple Inc.";
dict[@"SignatureVersion"] = @132352;
dict[@"SignerIdentity"] = @"Apple iPhone OS Application Signing";
NSString *teamIdentifier = vp_construct_team_identifier_for_entitlements(entitlements ?: @{});
if (teamIdentifier.length > 0) {
dict[@"TeamIdentifier"] = teamIdentifier;
}
NSDictionary *appGroupContainers = vp_construct_groups_containers_for_entitlements(entitlements, NO);
NSDictionary *systemGroupContainers = vp_construct_groups_containers_for_entitlements(entitlements, YES);
NSMutableDictionary *groupContainers = [NSMutableDictionary dictionary];
[groupContainers addEntriesFromDictionary:appGroupContainers];
[groupContainers addEntriesFromDictionary:systemGroupContainers];
if (groupContainers.count > 0) {
if (appGroupContainers.count > 0) {
dict[@"HasAppGroupContainers"] = @YES;
}
if (systemGroupContainers.count > 0) {
dict[@"HasSystemGroupContainers"] = @YES;
}
dict[@"GroupContainers"] = groupContainers.copy;
}
return dict;
}
static BOOL vp_register_path(NSString *path, BOOL unregister, BOOL forceSystem) {
if (path.length == 0) return NO;
LSApplicationWorkspace *workspace = [(id)NSClassFromString(@"LSApplicationWorkspace") defaultWorkspace];
if (unregister && ![[NSFileManager defaultManager] fileExistsAtPath:path]) {
LSApplicationProxy *app = [LSApplicationProxy applicationProxyForIdentifier:path];
if (app.bundleURL.path.length > 0) {
path = app.bundleURL.path;
}
}
path = path.stringByResolvingSymlinksInPath.stringByStandardizingPath;
NSDictionary *appInfoPlist =
[NSDictionary dictionaryWithContentsOfFile:[path stringByAppendingPathComponent:@"Info.plist"]];
NSString *appBundleID = appInfoPlist[@"CFBundleIdentifier"];
if (appBundleID.length == 0) return NO;
if ([vp_immutable_app_bundle_identifiers() containsObject:appBundleID.lowercaseString]) return NO;
if (!unregister) {
NSString *appExecutablePath = [path stringByAppendingPathComponent:appInfoPlist[@"CFBundleExecutable"]];
NSMutableDictionary *dictToRegister = vp_registration_dictionary(
appBundleID,
appExecutablePath,
NSClassFromString(@"MCMAppDataContainer"));
BOOL isRemovableSystemApp = [[NSFileManager defaultManager]
fileExistsAtPath:[@"/System/Library/AppSignatures" stringByAppendingPathComponent:appBundleID]];
BOOL registerAsUser = [path hasPrefix:@"/var/containers"] && !isRemovableSystemApp && !forceSystem;
dictToRegister[@"ApplicationType"] = registerAsUser ? @"User" : @"System";
dictToRegister[@"IsDeletable"] = @YES;
dictToRegister[@"Path"] = path;
dictToRegister[@"IsAdHocSigned"] = @YES;
dictToRegister[@"LSInstallType"] = @1;
dictToRegister[@"HasMIDBasedSINF"] = @0;
dictToRegister[@"MissingSINF"] = @0;
dictToRegister[@"FamilyID"] = @0;
dictToRegister[@"IsOnDemandInstallCapable"] = @0;
NSString *pluginsPath = [path stringByAppendingPathComponent:@"PlugIns"];
NSArray<NSString *> *plugins = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:pluginsPath error:nil];
NSMutableDictionary *bundlePlugins = [NSMutableDictionary dictionary];
for (NSString *pluginName in plugins) {
NSString *pluginPath = [pluginsPath stringByAppendingPathComponent:pluginName];
NSDictionary *pluginInfoPlist =
[NSDictionary dictionaryWithContentsOfFile:[pluginPath stringByAppendingPathComponent:@"Info.plist"]];
NSString *pluginBundleID = pluginInfoPlist[@"CFBundleIdentifier"];
NSString *pluginExecutable = pluginInfoPlist[@"CFBundleExecutable"];
if (pluginBundleID.length == 0 || pluginExecutable.length == 0) {
continue;
}
NSString *pluginExecutablePath = [pluginPath stringByAppendingPathComponent:pluginExecutable];
NSMutableDictionary *pluginDict = vp_registration_dictionary(
pluginBundleID,
pluginExecutablePath,
NSClassFromString(@"MCMPluginKitPluginDataContainer"));
pluginDict[@"ApplicationType"] = @"PluginKitPlugin";
pluginDict[@"Path"] = pluginPath;
pluginDict[@"PluginOwnerBundleID"] = appBundleID;
bundlePlugins[pluginBundleID] = pluginDict;
}
dictToRegister[@"_LSBundlePlugins"] = bundlePlugins;
if ([workspace registerApplicationDictionary:dictToRegister]) {
return YES;
}
// iOS 27+: the plain registerApplicationDictionary path is gated off in lsd
// (returns NO). Fall back to the containerized registration path, which
// works once lsd's clientIsEntitledForEmbeddedRegistrationOperations gate
// is patched (cfw_patch_lsd_embedded_reg). It returns NO even on success,
// so treat a nil registrationError as success.
SEL containerizedSel = @selector(registerContainerizedApplicationWithInfoDictionaries:operationUUID:requestContext:saveObserver:registrationError:);
if ([workspace respondsToSelector:containerizedSel]) {
NSError *regError = nil;
[workspace registerContainerizedApplicationWithInfoDictionaries:@[dictToRegister]
operationUUID:[NSUUID UUID]
requestContext:nil
saveObserver:nil
registrationError:&regError];
if (regError == nil) {
return YES;
}
}
return NO;
}
NSURL *url = [NSURL fileURLWithPath:path];
return [workspace unregisterApplication:url];
}
static BOOL vp_container_has_known_marker(NSString *containerPath) {
NSFileManager *fm = [NSFileManager defaultManager];
for (NSString *marker in @[VPManagedMarker, @"_TrollStoreLite", @"_TrollStore"]) {
if ([fm fileExistsAtPath:[containerPath stringByAppendingPathComponent:marker]]) {
return YES;
}
}
return NO;
}
static BOOL vp_mark_container_as_managed(NSString *containerPath) {
NSString *markerPath = [containerPath stringByAppendingPathComponent:VPManagedMarker];
if ([[NSFileManager defaultManager] fileExistsAtPath:markerPath]) {
return YES;
}
return [@"" writeToFile:markerPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
static void vp_rollback_app_install(
NSString *newPath,
NSString *oldPath,
NSString *backupPath,
NSString *markerPath,
BOOL markerExisted,
BOOL newMoved,
BOOL oldMoved,
BOOL restoreRegistration,
BOOL forceSystem
) {
NSFileManager *fm = [NSFileManager defaultManager];
if (newMoved) [fm removeItemAtPath:newPath error:nil];
if (oldMoved && [fm moveItemAtPath:backupPath toPath:oldPath error:nil] && restoreRegistration) {
vp_register_path(oldPath, NO, forceSystem);
}
if (!markerExisted) [fm removeItemAtPath:markerPath error:nil];
}
static int vp_install_app_from_package(
NSString *appPackagePath,
BOOL forceSystem,
NSString *certPath,
NSString **detailOutput
) {
NSString *appPayloadPath = [appPackagePath stringByAppendingPathComponent:@"Payload"];
NSString *appBundleToInstallPath = vp_find_app_path_in_bundle_path(appPayloadPath);
if (appBundleToInstallPath.length == 0) {
if (detailOutput) *detailOutput = @"The app package does not contain an app.";
return 167;
}
NSString *appId = vp_app_id_for_app_path(appBundleToInstallPath);
if (appId.length == 0) {
if (detailOutput) *detailOutput = @"The app package has no bundle identifier.";
return 176;
}
if ([vp_immutable_app_bundle_identifiers() containsObject:appId.lowercaseString]) {
if (detailOutput) *detailOutput = @"This app is part of iOS and cannot be replaced.";
return 179;
}
NSString *signOutput = @"";
int signRet = vp_sign_app(appBundleToInstallPath, certPath, &signOutput);
if (signRet != 0) {
if (detailOutput) *detailOutput = signOutput;
return signRet;
}
Class appContainerClass = NSClassFromString(@"MCMAppContainer");
if (!appContainerClass) {
if (detailOutput) *detailOutput = @"The app container service is unavailable.";
return 170;
}
MCMContainer *appContainer = [appContainerClass
containerWithIdentifier:appId
createIfNecessary:NO
existed:nil
error:nil];
NSString *oldAppPath = nil;
if (appContainer) {
NSURL *bundleContainerURL = appContainer.url;
NSURL *appBundleURL = vp_find_app_url_in_bundle_url(bundleContainerURL);
if (appBundleURL.path.length > 0 && !vp_container_has_known_marker(bundleContainerURL.path)) {
if (detailOutput) *detailOutput = @"An app with the same bundle identifier is already installed. Remove it and try again.";
return 171;
}
oldAppPath = appBundleURL.path;
} else {
NSError *mcmError = nil;
appContainer = [appContainerClass
containerWithIdentifier:appId
createIfNecessary:YES
existed:nil
error:&mcmError];
if (!appContainer || mcmError) {
if (detailOutput) *detailOutput = mcmError.localizedDescription ?: @"Unable to prepare storage for the app.";
return 170;
}
}
NSFileManager *fm = [NSFileManager defaultManager];
NSString *containerPath = appContainer.url.path;
NSString *newAppBundlePath =
[containerPath stringByAppendingPathComponent:appBundleToInstallPath.lastPathComponent];
NSString *stagedPath = [containerPath stringByAppendingPathComponent:
[@".vphone-install-" stringByAppendingString:[NSUUID UUID].UUIDString]];
NSString *backupPath = oldAppPath.length > 0 ? [containerPath stringByAppendingPathComponent:
[@".vphone-backup-" stringByAppendingString:[NSUUID UUID].UUIDString]] : nil;
NSString *markerPath = [containerPath stringByAppendingPathComponent:VPManagedMarker];
BOOL markerExisted = [fm fileExistsAtPath:markerPath];
NSError *copyError = nil;
if (![fm copyItemAtPath:appBundleToInstallPath toPath:stagedPath error:&copyError]) {
[fm removeItemAtPath:stagedPath error:nil];
if (detailOutput) *detailOutput = copyError.localizedDescription ?: @"Unable to copy the app onto the guest.";
return 178;
}
if (oldAppPath.length > 0 && ![fm moveItemAtPath:oldAppPath toPath:backupPath error:&copyError]) {
[fm removeItemAtPath:stagedPath error:nil];
if (detailOutput) *detailOutput = copyError.localizedDescription ?: @"Unable to back up the existing app.";
return 178;
}
BOOL oldMoved = oldAppPath.length > 0;
if (![fm moveItemAtPath:stagedPath toPath:newAppBundlePath error:&copyError]) {
[fm removeItemAtPath:stagedPath error:nil];
vp_rollback_app_install(newAppBundlePath, oldAppPath, backupPath, markerPath,
markerExisted, NO, oldMoved, NO, forceSystem);
if (detailOutput) *detailOutput = copyError.localizedDescription ?: @"Unable to place the app onto the guest.";
return 178;
}
vp_fix_permissions_of_app_bundle(newAppBundlePath);
if (!vp_mark_container_as_managed(containerPath)) {
vp_rollback_app_install(newAppBundlePath, oldAppPath, backupPath, markerPath,
markerExisted, YES, oldMoved, NO, forceSystem);
if (detailOutput) *detailOutput = @"The app was copied but could not be marked as managed.";
return 177;
}
if (!vp_register_path(newAppBundlePath, NO, forceSystem)) {
vp_rollback_app_install(newAppBundlePath, oldAppPath, backupPath, markerPath,
markerExisted, YES, oldMoved, YES, forceSystem);
if (detailOutput) *detailOutput = @"The app was copied but could not be registered with the system.";
return 181;
}
if (oldMoved) [fm removeItemAtPath:backupPath error:nil];
if (detailOutput) {
*detailOutput = [NSString stringWithFormat:@"%@ (%@)", newAppBundlePath.lastPathComponent, appId];
}
return 0;
}
static int vp_extract_package_to_directory(
NSString *fileToExtract,
NSString *extractionPath,
NSString **detailOutput
) {
NSString *archiveError = nil;
int ret = vp_extract_archive(fileToExtract, extractionPath, &archiveError);
if (ret != 0) {
if (detailOutput) *detailOutput = archiveError ?: @"Unable to extract the app package.";
return 168;
}
return 0;
}
BOOL vp_custom_installer_available(void) {
vp_load_private_frameworks();
return NSClassFromString(@"MCMAppContainer") != Nil
&& NSClassFromString(@"LSApplicationWorkspace") != Nil;
}
NSDictionary *vp_handle_custom_install(NSDictionary *msg) {
vp_load_private_frameworks();
id reqId = msg[@"id"];
NSString *ipaPath = msg[@"path"];
NSString *registration = msg[@"registration"];
NSString *certPath = msg[@"cert_path"];
BOOL forceSystem = [registration isEqualToString:@"System"];
if (ipaPath.length == 0) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
response[@"msg"] = @"No app package was specified.";
return response;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:ipaPath]) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
response[@"msg"] = [NSString stringWithFormat:@"App package not found at %@.", ipaPath];
return response;
}
if (!vp_custom_installer_available()) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
NSMutableArray<NSString *> *missing = [NSMutableArray array];
if (NSClassFromString(@"MCMAppContainer") == Nil) [missing addObject:@"MCMAppContainer"];
if (NSClassFromString(@"LSApplicationWorkspace") == Nil) [missing addObject:@"LSApplicationWorkspace"];
NSString *detail = missing.count > 0 ? [missing componentsJoinedByString:@", "] : @"unknown";
NSLog(@"vphoned: custom installer unavailable: %@", detail);
response[@"msg"] = @"This guest cannot install apps. The built-in installer is not supported here.";
return response;
}
if (certPath.length > 0 && ![[NSFileManager defaultManager] fileExistsAtPath:certPath]) {
certPath = nil;
}
NSString *tmpPackagePath = [[NSTemporaryDirectory() stringByResolvingSymlinksInPath]
stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
if (![[NSFileManager defaultManager] createDirectoryAtPath:tmpPackagePath
withIntermediateDirectories:NO
attributes:nil
error:nil]) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
response[@"msg"] = @"Unable to prepare the guest for installation. Try again.";
return response;
}
NSString *detail = @"";
int extractRet = vp_extract_package_to_directory(ipaPath, tmpPackagePath, &detail);
int installRet = 0;
if (extractRet == 0) {
installRet = vp_install_app_from_package(tmpPackagePath, forceSystem, certPath, &detail);
}
[[NSFileManager defaultManager] removeItemAtPath:tmpPackagePath error:nil];
[[NSFileManager defaultManager] removeItemAtPath:ipaPath error:nil];
if (certPath.length > 0) {
[[NSFileManager defaultManager] removeItemAtPath:certPath error:nil];
}
if (extractRet != 0 || installRet != 0) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
int retCode = extractRet != 0 ? extractRet : installRet;
NSString *trimmed = vp_trimmed_output(detail ?: @"");
response[@"msg"] = trimmed.length > 0
? [NSString stringWithFormat:@"Unable to install the app (code %d).\n%@", retCode, trimmed]
: [NSString stringWithFormat:@"Unable to install the app (code %d).", retCode];
return response;
}
NSMutableDictionary *response = vp_make_response(@"ok", reqId);
response[@"msg"] = forceSystem
? [NSString stringWithFormat:@"Installed %@ as a system app.", detail]
: [NSString stringWithFormat:@"Installed %@ as a user app.", detail];
return response;
}
-7
View File
@@ -1,5 +1,4 @@
#import "Include/VphonedNative.h"
#import "vphoned_install.h"
#import <CommonCrypto/CommonDigest.h>
#include <mach-o/dyld.h>
#include <unistd.h>
@@ -8,12 +7,6 @@
static const char *marker = "/var/root/Library/Caches/vphoned.api-v2";
static const char *pending = "/var/root/Library/Caches/vphoned.api-v2.pending";
NSDictionary *vp_native_api_command(NSDictionary *message) {
NSString *type = message[@"t"];
if ([type isEqualToString:@"ipa_install"]) return vp_handle_custom_install(message);
return @{@"t": @"err", @"msg": @"Unknown native operation"};
}
void vp_native_bootstrap_cached_binary(void) {
// A cached update gets one attempt to bind. If it fails, launchd restarts
// the bundled daemon, which remains the fallback.
+241
View File
@@ -0,0 +1,241 @@
#import "Include/VphonedNative.h"
#import <Security/Security.h>
#include <errno.h>
#include <mach-o/fat.h>
#include <mach-o/loader.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct __SecCode const *SecStaticCodeRef;
typedef CF_OPTIONS(uint32_t, SecCSFlags) {
kSecCSDefaultFlags = 0
};
#define kSecCSRequirementInformation (1 << 2)
OSStatus SecStaticCodeCreateWithPathAndAttributes(
CFURLRef path,
SecCSFlags flags,
CFDictionaryRef attributes,
SecStaticCodeRef *staticCode
);
OSStatus SecCodeCopySigningInformation(SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *information);
extern CFStringRef kSecCodeInfoEntitlementsDict;
// Implemented in GuestSigner.swift using the shared VPhoneSign target.
extern char *vp_guest_sign_binary(const char *path, const char *entitlementsPath, const char *certificatePath);
static NSDictionary *vp_info_dictionary_for_app_path(NSString *appPath) {
if (appPath.length == 0) return nil;
return [NSDictionary dictionaryWithContentsOfFile:[appPath stringByAppendingPathComponent:@"Info.plist"]];
}
static NSString *vp_app_main_executable_path_for_app_path(NSString *appPath) {
NSDictionary *info = vp_info_dictionary_for_app_path(appPath);
NSString *executable = info[@"CFBundleExecutable"];
if (executable.length == 0) return nil;
return [appPath stringByAppendingPathComponent:executable];
}
static BOOL vp_is_macho_file(NSString *filePath) {
FILE *file = fopen(filePath.fileSystemRepresentation, "r");
if (!file) return NO;
uint32_t magic = 0;
fread(&magic, sizeof(uint32_t), 1, file);
fclose(file);
return magic == FAT_MAGIC || magic == FAT_CIGAM || magic == MH_MAGIC_64 || magic == MH_CIGAM_64;
}
static SecStaticCodeRef vp_get_static_code_ref(NSString *binaryPath) {
if (binaryPath.length == 0) return NULL;
CFURLRef binaryURL = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
(__bridge CFStringRef)binaryPath,
kCFURLPOSIXPathStyle,
false
);
if (binaryURL == NULL) return NULL;
SecStaticCodeRef codeRef = NULL;
OSStatus result = SecStaticCodeCreateWithPathAndAttributes(binaryURL, kSecCSDefaultFlags, NULL, &codeRef);
CFRelease(binaryURL);
if (result != errSecSuccess) {
return NULL;
}
return codeRef;
}
static NSDictionary *vp_dump_entitlements_from_binary_at_path(NSString *binaryPath) {
SecStaticCodeRef codeRef = vp_get_static_code_ref(binaryPath);
if (codeRef == NULL) return nil;
CFDictionaryRef signingInfo = NULL;
OSStatus result = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &signingInfo);
CFRelease(codeRef);
if (result != errSecSuccess || signingInfo == NULL) {
if (signingInfo) CFRelease(signingInfo);
return nil;
}
NSDictionary *entitlementsNSDict = nil;
CFDictionaryRef entitlements = CFDictionaryGetValue(signingInfo, kSecCodeInfoEntitlementsDict);
if (entitlements && CFGetTypeID(entitlements) == CFDictionaryGetTypeID()) {
entitlementsNSDict = [(__bridge NSDictionary *)entitlements copy];
}
CFRelease(signingInfo);
return entitlementsNSDict;
}
static int vp_sign_binary(
NSString *filePath,
NSDictionary *entitlements,
NSString *certPath,
NSString **errorOutput
) {
NSString *entitlementsPath = nil;
NSData *entitlementsXML = entitlements ? [NSPropertyListSerialization
dataWithPropertyList:entitlements
format:NSPropertyListXMLFormat_v1_0
options:0
error:nil] : nil;
if (entitlementsXML) {
entitlementsPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]
stringByAppendingPathExtension:@"plist"];
if (![entitlementsXML writeToFile:entitlementsPath atomically:YES]) {
if (errorOutput) *errorOutput = @"Could not prepare app entitlements.";
return EIO;
}
}
char *error = vp_guest_sign_binary(
filePath.fileSystemRepresentation,
entitlementsPath.fileSystemRepresentation,
certPath.length > 0 ? certPath.fileSystemRepresentation : NULL
);
if (entitlementsPath) {
[[NSFileManager defaultManager] removeItemAtPath:entitlementsPath error:nil];
}
if (!error) return 0;
if (errorOutput) *errorOutput = [NSString stringWithUTF8String:error] ?: @"Could not sign app executable.";
free(error);
return EINVAL;
}
static int vp_sign_app(NSString *appPath, NSString *certPath, NSString **errorOutput) {
if (!vp_info_dictionary_for_app_path(appPath)) {
if (errorOutput) *errorOutput = @"The app package is incomplete and cannot be signed.";
return 172;
}
NSString *mainExecutablePath = vp_app_main_executable_path_for_app_path(appPath);
if (mainExecutablePath.length == 0 || ![[NSFileManager defaultManager] fileExistsAtPath:mainExecutablePath]) {
if (errorOutput) *errorOutput = @"The app package is missing its program and cannot be signed.";
return 174;
}
NSMutableSet<NSString *> *signedExecutables = [NSMutableSet set];
NSURL *fileURL = nil;
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appPath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
if (![filePath.lastPathComponent isEqualToString:@"Info.plist"]) {
continue;
}
NSDictionary *infoDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSString *bundleId = infoDict[@"CFBundleIdentifier"];
NSString *bundleExecutable = infoDict[@"CFBundleExecutable"];
if (bundleId.length == 0 || bundleExecutable.length == 0) {
continue;
}
NSString *bundleMainExecutablePath = [[filePath stringByDeletingLastPathComponent]
stringByAppendingPathComponent:bundleExecutable];
if (![[NSFileManager defaultManager] fileExistsAtPath:bundleMainExecutablePath]) {
continue;
}
NSString *packageType = infoDict[@"CFBundlePackageType"];
if ([packageType isEqualToString:@"FMWK"]) {
continue;
}
NSMutableDictionary *entitlementsToUse =
[vp_dump_entitlements_from_binary_at_path(bundleMainExecutablePath) mutableCopy];
if (!entitlementsToUse && [bundleMainExecutablePath isEqualToString:mainExecutablePath]) {
entitlementsToUse = [@{
@"application-identifier": @"TROLLTROLL.*",
@"com.apple.developer.team-identifier": @"TROLLTROLL",
@"get-task-allow": @YES,
@"keychain-access-groups": @[@"TROLLTROLL.*", @"com.apple.token"],
} mutableCopy];
}
if (!entitlementsToUse) {
entitlementsToUse = [NSMutableDictionary dictionary];
}
NSObject *containerRequired = entitlementsToUse[@"com.apple.private.security.container-required"];
BOOL shouldWriteContainerRequired = YES;
if ([containerRequired isKindOfClass:[NSString class]]) {
shouldWriteContainerRequired = NO;
} else if ([containerRequired isKindOfClass:[NSNumber class]]) {
shouldWriteContainerRequired = [(NSNumber *)containerRequired boolValue];
}
BOOL noContainer =
[entitlementsToUse[@"com.apple.private.security.no-container"] respondsToSelector:@selector(boolValue)]
? [entitlementsToUse[@"com.apple.private.security.no-container"] boolValue]
: NO;
BOOL noSandbox =
[entitlementsToUse[@"com.apple.private.security.no-sandbox"] respondsToSelector:@selector(boolValue)]
? [entitlementsToUse[@"com.apple.private.security.no-sandbox"] boolValue]
: NO;
if (shouldWriteContainerRequired && !noContainer && !noSandbox) {
entitlementsToUse[@"com.apple.private.security.container-required"] = bundleId;
}
entitlementsToUse[@"jb.pmap_cs_custom_trust"] = @"PMAP_CS_APP_STORE";
NSString *signOutput = @"";
int ret = vp_sign_binary(bundleMainExecutablePath, entitlementsToUse, certPath, &signOutput);
if (ret != 0) {
if (errorOutput) *errorOutput = signOutput;
return 173;
}
[signedExecutables addObject:bundleMainExecutablePath];
}
// Sign code without an Info.plist executable declaration, such as dylibs.
// The declared executables above already carry their guest entitlements.
enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appPath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
if ([signedExecutables containsObject:filePath] || !vp_is_macho_file(filePath)) continue;
NSString *signOutput = @"";
if (vp_sign_binary(filePath, nil, certPath, &signOutput) != 0) {
if (errorOutput) *errorOutput = signOutput;
return 173;
}
}
return 0;
}
char *vp_sign_app_for_install(const char *appPath, const char *certificatePath) {
if (!appPath) return strdup("Missing app path");
NSString *failure = nil;
NSString *app = [NSString stringWithUTF8String:appPath];
NSString *certificate = certificatePath ? [NSString stringWithUTF8String:certificatePath] : nil;
if (vp_sign_app(app, certificate, &failure) == 0) return NULL;
return strdup((failure ?: @"Could not sign app.").UTF8String);
}
@@ -13,7 +13,6 @@
C10000000000000000000014 /* NIOHTTP1 in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000063 /* NIOHTTP1 */; };
C10000000000000000000015 /* NIOPosix in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000064 /* NIOPosix */; };
C10000000000000000000016 /* NIOWebSocket in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000065 /* NIOWebSocket */; };
C10000000000000000000017 /* LibArchive in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000066 /* LibArchive */; };
C10000000000000000000018 /* IcliSystem in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000068 /* IcliSystem */; };
/* End PBXBuildFile section */
@@ -56,7 +55,6 @@
C10000000000000000000014 /* NIOHTTP1 in Frameworks */,
C10000000000000000000015 /* NIOPosix in Frameworks */,
C10000000000000000000016 /* NIOWebSocket in Frameworks */,
C10000000000000000000017 /* LibArchive in Frameworks */,
C10000000000000000000018 /* IcliSystem in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -128,7 +126,6 @@
C10000000000000000000063 /* NIOHTTP1 */,
C10000000000000000000064 /* NIOPosix */,
C10000000000000000000065 /* NIOWebSocket */,
C10000000000000000000066 /* LibArchive */,
C10000000000000000000068 /* IcliSystem */,
);
productName = vphoned;
@@ -155,7 +152,6 @@
packageReferences = (
C10000000000000000000071 /* XCRemoteSwiftPackageReference "icli" */,
C10000000000000000000072 /* XCRemoteSwiftPackageReference "swift-nio" */,
C10000000000000000000073 /* XCRemoteSwiftPackageReference "libarchive.xcframework" */,
C10000000000000000000074 /* XCRemoteSwiftPackageReference "swift-collections" */,
);
projectDirPath = "";
@@ -224,7 +220,6 @@
HEADER_SEARCH_PATHS = (
"$(PROJECT_DIR)/Native",
"$(PROJECT_DIR)/Native/Include",
"$(PROJECT_DIR)/Native/Vendor/LibArchive",
);
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
OTHER_LDFLAGS = "-ObjC";
@@ -247,7 +242,6 @@
HEADER_SEARCH_PATHS = (
"$(PROJECT_DIR)/Native",
"$(PROJECT_DIR)/Native/Include",
"$(PROJECT_DIR)/Native/Vendor/LibArchive",
);
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
OTHER_LDFLAGS = "-ObjC";
@@ -351,7 +345,7 @@
repositoryURL = "https://github.com/owngoal-dev/icli.git";
requirement = {
kind = exactVersion;
version = 0.6.5;
version = 0.6.6;
};
};
C10000000000000000000072 /* XCRemoteSwiftPackageReference "swift-nio" */ = {
@@ -362,14 +356,6 @@
version = 2.83.0;
};
};
C10000000000000000000073 /* XCRemoteSwiftPackageReference "libarchive.xcframework" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Lakr233/libarchive.xcframework.git";
requirement = {
kind = exactVersion;
version = 0.1.1;
};
};
C10000000000000000000074 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-collections.git";
@@ -406,11 +392,6 @@
package = C10000000000000000000072 /* XCRemoteSwiftPackageReference "swift-nio" */;
productName = NIOWebSocket;
};
C10000000000000000000066 /* LibArchive */ = {
isa = XCSwiftPackageProductDependency;
package = C10000000000000000000073 /* XCRemoteSwiftPackageReference "libarchive.xcframework" */;
productName = LibArchive;
};
C10000000000000000000068 /* IcliSystem */ = {
isa = XCSwiftPackageProductDependency;
package = C10000000000000000000071 /* XCRemoteSwiftPackageReference "icli" */;
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/owngoal-dev/icli.git",
"state" : {
"revision" : "bbb4d238bb06285dacb5385e372b5e990398d688",
"version" : "0.6.5"
"revision" : "0bc454f61f54d99439e0f8a7f9e1c43f0fb49dc2",
"version" : "0.6.6"
}
},
{