Pure Swift, ZSign-compatible iOS code signing for Mach-O files, app bundles, and IPA archives.
rork-sign is a SwiftPM-native code-signing toolkit with a ZSign-compatible command-line interface. It signs Mach-O files, app bundles, and IPA archives; rewrites bundle metadata; injects or removes dylib load commands; and exposes the same signing engine as a library API.
The implementation is written in Swift and does not shell out to Apple's
codesign or OpenSSL for production signing. Tests use OpenSSL as an external
oracle for certificate and CMS interoperability.
- Features
- Supported Targets
- Build
- Usage
- Examples
- Certificate Checks
- Swift API
- Objective-C API
- ZSign Compatibility
- Fast Re-signing
- Logging
- Limitations
- Development
- License
- ZSign-compatible CLI - supports the public ZSign-style signing flags for IPA, bundle, and Mach-O workflows.
- Swift-first library - the
RorkSignmodule exposes signing, inspection, resource sealing, and credential APIs directly to Swift applications. - Objective-C facade - the
RorkSignObjCmodule exposes the public signer surface to Objective-C and Objective-C++ with Foundation types. - Mach-O signing - ad-hoc and identity-backed signatures for thin and universal 64-bit Mach-O files.
- CMS signatures - pure Swift detached CMS SignedData generation and verification for RSA and NIST EC signing identities.
- Provisioning profiles - decode
.mobileprovisionfiles, derive entitlements, validate profile/private-key matches, and embed profiles when requested. - PKCS#12 and private keys - import
.p12/.pfx, PEM, DER, encrypted PKCS#8, and traditional encrypted RSA PEM credentials. - Bundle signing - sign nested bundles inside-out, generate
_CodeSignature/CodeResources, and hash resource seals into the executable CodeDirectory. - IPA signing - extract, sign, and repack
Payload/*.apparchives with stored or deflated output. - Dylib editing - inspect, inject, and remove
LC_LOAD_DYLIBandLC_LOAD_WEAK_DYLIBcommands. - Metadata rewriting - update bundle identifier, display name, version,
minimum OS version, Files app keys, extensions, Watch apps, and
UISupportedDevices. - Inspection workflows - check certificates, provisioning profiles, PKCS#12 identities, bundle resource seals, embedded Mach-O CodeDirectories, CMS signatures, OCSP responder URLs, and CRL distribution points.
- OCSP utilities - build OCSP requests, fetch responder DER, parse BasicOCSPResponse payloads, verify signatures, and apply explicit freshness policy.
- Signature cache - reuse unchanged Mach-O signatures across repeated
bundle or IPA signing runs through
.zsign_cache.
The package currently declares:
- macOS 13+
- iOS 15+
The CLI is intended for macOS development and CI environments with a Swift toolchain. The library code is pure Swift and SwiftPM-based; additional platform support should be added with CI coverage before it is advertised as supported.
git clone https://github.com/rorkai/rork-sign.git
cd rork-sign
swift build -c release
.build/release/rorksign --helpDuring development, run the executable through SwiftPM:
swift run rorksign --help
swift run rorksign zsign --helprorksign uses Swift ArgumentParser. The root command defaults to the
ZSign-compatible signing flow, so these two forms are equivalent:
rorksign -a -o output.ipa input.ipa
rorksign zsign -a -o output.ipa input.ipaUSAGE: rorksign zsign [<options>] [<input-path>]
ARGUMENTS:
<input-path> Input Mach-O file, app bundle, extracted archive
folder, or IPA.
OPTIONS:
-a, --adhoc Perform ad-hoc signing only.
-c, --cert <cert> Path to a PEM or DER certificate.
-k, --pkey <pkey> Path to a private key or PKCS#12 credential.
-m, --prov <prov> Path to a provisioning profile. Repeat for nested
bundles.
-p, --password <password>
Password for the private key or PKCS#12 credential.
-b, --bundle_id <bundle_id>
Replacement root bundle identifier.
-n, --bundle_name <bundle_name>
Replacement root display name.
-r, --bundle_version <bundle_version>
Replacement root bundle version.
-e, --entitlements <entitlements>
Path to an entitlements plist.
--entitlements-resource <entitlements-resource>
Bundle-local entitlements plist filename for unsigned
app artifacts.
-o, --output <output> Path to the output IPA or Mach-O.
-z, --zip_level <zip_level>
ZIP compression level. Level 0 stores files; levels
1-9 use Deflate.
-l, --dylib <dylib> Path to a dylib to copy and load. Repeat to inject
multiple dylibs.
-D, --rm_dylib <rm_dylib>
Dylib install name or basename to remove. Repeat to
remove multiple dylibs.
-w, --weak Inject dylibs with LC_LOAD_WEAK_DYLIB.
-2, --sha256_only Emit SHA-256-only CodeDirectory signatures.
-x, --metadata <metadata>
Write ZSign-compatible metadata.json and icon output.
-R, --rm_provision Use provisioning profiles for signing without
embedding them.
-S, --enable_docs Enable document-browser Info.plist keys.
-M, --min_version <min_version>
Replacement MinimumOSVersion.
-E, --rm_extensions Remove app extensions before signing.
-W, --rm_watch Remove embedded Watch apps before signing.
-U, --rm_uisd Remove UISupportedDevices from the root Info.plist.
-t, --temp_folder <temp_folder>
Directory used for IPA extraction and metadata
workspaces.
-f, --force Rebuild signatures and resource seals while
refreshing cache.
-d, --debug Write generated signature slots to .zsign_debug.
-V, --verbose Print detailed signing paths and outputs.
-q, --quiet Reduce compatibility command output.
-i, --install Install the signed IPA with ideviceinstaller.
-C, --check Inspect certificate, profile, credential, and Mach-O
signature metadata.
--online-ocsp Fetch and validate OCSP status during -C checks when
issuer material is available.
-v, --version Print the version.
-h, --help Show help information.
Inspect a Mach-O file
rorksign Demo.app/DemoAd-hoc sign an IPA
rorksign -a -o output.ipa Demo.ipaSign an IPA with a PKCS#12 identity
rorksign -k dev.p12 -p password -m app.mobileprovision -o output.ipa Demo.ipaRead bundle-local entitlements for unsigned app artifacts
rorksign -k dev.p12 -p password -m app.mobileprovision \
--entitlements-resource Entitlements.plist -o output.ipa Demo.ipa--entitlements-resource names a plist file inside each bundle. It is used as
the executable's entitlement request when the executable has no embedded
entitlements, and the selected provisioning profile still constrains the final
signed values.
Sign an app bundle with a private key and certificate
rorksign -c cert.pem -k private-key.pem -m app.mobileprovision -o output.ipa Demo.appChange bundle identifier and display name
rorksign -k dev.p12 -p password -m app.mobileprovision \
-b com.example.newapp -n "New Name" -o output.ipa Demo.ipaInject a dylib and re-sign
rorksign -k dev.p12 -p password -m app.mobileprovision \
-l Hook.dylib -o output.ipa Demo.ipaInject weak dylibs into a Mach-O
rorksign -a -w \
-l "@executable_path/HookA.dylib" \
-l "@executable_path/HookB.dylib" \
Demo.app/DemoRemove a dylib load command
rorksign -a -D Hook.dylib Demo.app/DemoExtract metadata and icon
rorksign -x ./metadata Demo.ipa
# writes ./metadata/metadata.json and the selected icon fileEnable Files app integration
rorksign -k dev.p12 -p password -m app.mobileprovision -S -o output.ipa Demo.ipaSet minimum OS version
rorksign -k dev.p12 -p password -m app.mobileprovision -M 14.0 -o output.ipa Demo.ipaRemove extensions, Watch apps, or UISupportedDevices
rorksign -k dev.p12 -p password -m app.mobileprovision -E -o output.ipa Demo.ipa
rorksign -k dev.p12 -p password -m app.mobileprovision -W -o output.ipa Demo.ipa
rorksign -k dev.p12 -p password -m app.mobileprovision -U -o output.ipa Demo.ipaUse SHA-256-only CodeDirectories
rorksign -a -2 -o output.ipa Demo.ipaInstall after signing
rorksign -k dev.p12 -p password -m app.mobileprovision -i Demo.ipa-i/--install invokes ideviceinstaller install <signed-ipa>. When no
-o/--output path is supplied, the CLI creates a temporary IPA and removes it
after the install attempt.
-C/--check inspects supported signing assets without mutating them.
Supported inputs:
.ipaarchives.appand.appexbundles- Mach-O binaries
.mobileprovisionprovisioning profiles.p12/.pfxidentities- PEM or DER certificates
# Check an IPA or app bundle
rorksign -C Demo.ipa
rorksign -C Demo.app
# Check a provisioning profile
rorksign -C app.mobileprovision
# Check a PKCS#12 identity
rorksign -C -p password dev.p12
# Check a certificate chain and fetch OCSP status when issuer material exists
rorksign -C --online-ocsp cert-chain.pem
# Sign and then report the embedded executable signature
rorksign -C -k dev.p12 -p password -m app.mobileprovision -o output.ipa Demo.ipaExample output uses stable key=value fields for scripts:
certificateCN=Apple Distribution: Example Corp certificateType=Apple Distribution certificateOrg=Example Corp certificateIssuer=Apple Worldwide Developer Relations Certification Authority certificateOCSP=https://ocsp.apple.com/ocsp03-wwdr... certificateCRL= certificateSerial=12:34:56 certificateAlgorithm=RSA 2048-bit certificateCA=false certificateCanSign=false certificateKeyUsage=digitalSignature certificateExpiration=2027-01-01T00:00:00Z certificateExpired=false
Local checks include:
- certificate validity windows
- issuer/subject chain links
- certificate signatures
- BasicConstraints, KeyUsage, and path-length constraints
- profile/private-key authorization
- CodeResources verification
- embedded CodeDirectory hash verification
- embedded CMS signature verification against the primary CodeDirectory
Trust roots, Apple certificate policy, CRL download, and platform trust evaluation are intentionally left to caller policy.
Add the package to your SwiftPM project:
dependencies: [
.package(url: "https://github.com/rorkai/rork-sign.git", from: "0.2.18"),
]Then depend on the library product:
.product(name: "RorkSign", package: "rork-sign")Ad-hoc sign an IPA:
import Foundation
import RorkSign
let report = try RorkSigner.signIPAAdHoc(
at: URL(fileURLWithPath: "Demo.ipa"),
outputURL: URL(fileURLWithPath: "output.ipa")
)
print(report.signedCodePaths)Sign with a PKCS#12 identity:
import Foundation
import RorkSign
let identity = try SigningIdentity(
pkcs12Data: Data(contentsOf: URL(fileURLWithPath: "dev.p12")),
password: "password"
)
try RorkSigner.signIPAWithIdentity(
at: URL(fileURLWithPath: "Demo.ipa"),
outputURL: URL(fileURLWithPath: "output.ipa"),
identity: identity
)Sign a framework directly:
let frameworkURL = URL(fileURLWithPath: "Demo.framework")
let profile = try Data(contentsOf: URL(fileURLWithPath: "app.mobileprovision"))
let credential = try Data(contentsOf: URL(fileURLWithPath: "dev.p12"))
try RorkSigner.signFrameworkWithCredential(
at: frameworkURL,
provisioningProfileData: profile,
credentialData: credential,
password: "password"
)Framework signing seals resources and signs the framework executable in place. It does not embed provisioning profiles or copy app entitlements from the profile by default.
Sign a hosted bundle for a runtime that loads guest code from an installed host:
try RorkSigner.signHostedBundleWithCredential(
at: URL(fileURLWithPath: "Guest.app"),
provisioningProfileData: profile,
credentialData: credential,
password: "password",
options: HostedBundleSigningOptions(
hostExecutableURL: URL(fileURLWithPath: "HostExecutable"),
hostBundleIdentifier: "com.example.host"
)
)Hosted signing temporarily signs a copied host executable as the root
executable, signs the original executable as loose code, then restores the
guest Info.plist and removes the temporary stub. Use app signing
instead when the output must be installed and launched directly.
Read a team id from a provisioning profile:
let profileData = try Data(contentsOf: URL(fileURLWithPath: "app.mobileprovision"))
let profile = try RorkSigner.decodeProvisioningProfile(profileData)
print(profile.teamIdentifier)
print(profile.authorizedBundleIdentifier ?? "unknown")
print(profile.usesWildcardBundleIdentifier)Preview the provisioning profiles needed for app signing:
let inspection = try RorkSigner.inspectApp(
at: URL(fileURLWithPath: "Demo.app"),
replacementBundleIdentifier: "com.example.demo"
)
for requirement in inspection.provisioningRequirements {
print("\(requirement.relativePath): \(requirement.rewrittenBundleIdentifier)")
}Sign an IPA and read bundle-local entitlements when unsigned executables have no embedded entitlement slot:
let profileData = try Data(contentsOf: URL(fileURLWithPath: "app.mobileprovision"))
let identity = try SigningIdentity(
pkcs12Data: Data(contentsOf: URL(fileURLWithPath: "dev.p12")),
password: "password"
)
try RorkSigner.signIPA(
at: URL(fileURLWithPath: "Demo.ipa"),
outputURL: URL(fileURLWithPath: "output.ipa"),
identity: identity,
options: AppSigningOptions(
bundleIdentifier: "com.example.demo",
rootProvisioningProfile: profileData,
entitlementsResourceName: "Entitlements.plist"
)
)Validate a profile/private-key pair before signing:
let teamID = try RorkSigner.validatedTeamIdentifier(
provisioningProfileData: Data(contentsOf: URL(fileURLWithPath: "app.mobileprovision")),
credentialData: Data(contentsOf: URL(fileURLWithPath: "dev.p12")),
password: "password"
)Add the RorkSignObjC product when integrating from Objective-C or
Objective-C++:
.product(name: "RorkSignObjC", package: "rork-sign")Then import the module from Objective-C/Objective-C++:
@import RorkSignObjC;The facade uses RK* Objective-C names, typed option objects, Foundation
inputs, NSError ** failures, and typed reusable wrappers for signing
identities, provisioning profiles, OCSP requests, Mach-O CMS preparation, and
bundle/IPA signing reports.
NSError *error = nil;
RKSigner *signer = [[RKSigner alloc] init];
NSData *profile = [NSData dataWithContentsOfURL:profileURL];
NSData *credential = [NSData dataWithContentsOfURL:credentialURL];
if (!profile || !credential) {
return;
}
NSString *teamID = [signer teamIdentifierForProvisioningProfileData:profile
error:&error];
RKProvisioningProfile *decoded = [signer decodeProvisioningProfileData:profile
error:&error];
NSString *authorizedBundleID = decoded.authorizedBundleIdentifier;Sign a bundle with a provisioning profile and credential:
RKBundleSigningOptions *options = [[RKBundleSigningOptions alloc] init];
options.embedProvisioningProfile = YES;
options.codeDirectoryHashingMode = RKCodeDirectoryHashingModeSha256Only;
RKBundleSigningReport *report =
[signer signBundleWithCredentialAtURL:bundleURL
provisioningProfileData:profile
credentialData:credential
password:@"password"
options:options
error:&error];Sign a framework directly:
RKFrameworkSigningOptions *frameworkOptions = [[RKFrameworkSigningOptions alloc] init];
frameworkOptions.codeDirectoryHashingMode = RKCodeDirectoryHashingModeCompatible;
RKBundleSigningReport *frameworkReport =
[signer signFrameworkWithCredentialAtURL:frameworkURL
provisioningProfileData:profile
credentialData:credential
password:@"password"
options:frameworkOptions
error:&error];Inspect an app before signing:
RKAppInspectionReport *inspection =
[signer inspectAppAtURL:bundleURL
replacementBundleIdentifier:@"com.example.demo"
error:&error];
for (RKAppProvisioningRequirement *requirement
in inspection.provisioningRequirements) {
NSLog(@"%@: %@", requirement.relativePath, requirement.rewrittenBundleIdentifier);
}Sign a hosted bundle:
RKHostedBundleSigningOptions *hostedOptions =
[[RKHostedBundleSigningOptions alloc] initWithHostExecutableURL:hostExecutableURL
hostBundleIdentifier:@"com.example.host"];
RKBundleSigningReport *hostedReport =
[signer signHostedBundleWithCredentialAtURL:guestBundleURL
provisioningProfileData:profile
credentialData:credential
password:@"password"
options:hostedOptions
error:&error];For APIs whose Swift reports contain non-Objective-C value types, RKSigner
returns NSDictionary / NSArray<NSDictionary *> reports with Foundation
values. Mutating/signing APIs return typed report objects when the report is
part of the primary workflow.
The compatibility command implements the ZSign public CLI flag set:
| Area | Flags |
|---|---|
| Signing identity | -k/--pkey, -c/--cert, -p/--password, -m/--prov |
| Signing mode | -a/--adhoc, -2/--sha256_only |
| Output | -o/--output, -z/--zip_level, -t/--temp_folder, -i/--install |
| Bundle edits | -b/--bundle_id, -n/--bundle_name, -r/--bundle_version, -M/--min_version, -S/--enable_docs |
| Entitlements/profile handling | -e/--entitlements, --entitlements-resource, -R/--rm_provision |
| Dylib edits | -l/--dylib, -D/--rm_dylib, -w/--weak |
| Cleanup | -E/--rm_extensions, -W/--rm_watch, -U/--rm_uisd |
| Inspection/debugging | -C/--check, -x/--metadata, -d/--debug, -f/--force, -V/--verbose, -q/--quiet, -v/--version, -h/--help |
The CLI also includes --online-ocsp for explicit OCSP network checks during
-C workflows. This is an additive Swift implementation feature rather than a
ZSign flag.
The package is ZSign-compatible, not a file-by-file port. It keeps the familiar command surface while exposing a Swift-native API and testable internal components.
Bundle and IPA signing can reuse signatures from .zsign_cache. Cache keys
include normalized Mach-O bytes plus entitlements, resource seal, Info.plist,
identity, and CodeDirectory hash mode, so changed signing inputs miss the cache.
# First run creates cache entries.
rorksign -k dev.p12 -p password -m app.mobileprovision -o output.ipa ExtractedArchive/
# Reuses unchanged Mach-O signatures.
rorksign -k dev.p12 -p password -m app.mobileprovision -o output.ipa ExtractedArchive/
# Rebuilds signatures and refreshes cache entries.
rorksign -f -k dev.p12 -p password -m app.mobileprovision -o output.ipa ExtractedArchive/Use -V/--verbose to print a signing preflight header plus signed, cached,
sealed, and archived paths.
The library is silent by default. Swift callers can pass SigningDiagnostics
with a SwiftLog Logger through BundleSigningOptions or
AppSigningOptions:
import Foundation
import Logging
import RorkSign
LoggingSystem.bootstrap(StreamLogHandler.standardOutput)
var logger = Logger(label: "signing")
logger.logLevel = .info
let cacheURL = URL(fileURLWithPath: ".zsign_cache", isDirectory: true)
let options = BundleSigningOptions(
signingCache: SigningCacheOptions(directoryURL: cacheURL),
diagnostics: SigningDiagnostics(logger: logger)
)Objective-C callers configure logging through the facade options:
RKBundleSigningOptions *options = [[RKBundleSigningOptions alloc] init];
options.logLevel = RKSigningLogLevelInfo;
options.logHandler = ^(RKSigningDiagnosticLevel level, NSString *message) {
NSLog(@"[RorkSign] %@", message);
};For object-oriented integrations, assign options.logger to an object that
conforms to RKSigningLogger. Use RKSigningLogLevelDebug to include detailed
path-level events such as sealed bundles and signed Mach-O files.
The CLI wires -V/--verbose to the same diagnostics path and keeps -q/--quiet
silent.
The default command is ZSign-compatible. Additional subcommands expose smaller building blocks for tests, debugging, and integrations:
rorksign inspect <mach-o-path>
rorksign dylibs <mach-o-path>
rorksign metadata <app-or-ipa-path> <output-directory>
rorksign inject-dylib <input-mach-o> <output-mach-o> <install-name> [--weak]
rorksign remove-dylib <input-mach-o> <output-mach-o> <install-name-or-name> [...]
rorksign adhoc-sign <input-mach-o> <output-mach-o> <bundle-id>
rorksign adhoc-sign-bundle <bundle-path>
rorksign adhoc-sign-ipa <input-ipa> <output-ipa>
rorksign identity-sign <input-mach-o> <output-mach-o> <bundle-id> <cert-pem> <private-key-pem> [--password <password>]
rorksign identity-sign-p12 <input-mach-o> <output-mach-o> <bundle-id> <p12-path> <password>
rorksign identity-sign-profile-key <input-mach-o> <output-mach-o> <bundle-id> <profile-path> <credential-path> <password>
rorksign identity-sign-bundle <bundle-path> <cert-pem> <private-key-pem> [--password <password>]
rorksign identity-sign-bundle-p12 <bundle-path> <p12-path> <password>
rorksign identity-sign-bundle-profile-key <bundle-path> <profile-path> <credential-path> <password>
rorksign identity-sign-ipa <input-ipa> <output-ipa> <cert-pem> <private-key-pem> [--password <password>]
rorksign identity-sign-ipa-p12 <input-ipa> <output-ipa> <p12-path> <password>
rorksign identity-sign-ipa-profile-key <input-ipa> <output-ipa> <profile-path> <credential-path> <password>
rorksign sign ipa --input <input-ipa> --output <output-ipa> --bundle-id <bundle-id> --profile-map <profile-map-json> --certificate <cert-path> --key <credential-path> [--password <password>] [--app-groups <group,...>] [--bundle-name <name>] [--entitlements-resource <name>]
rorksign seal-resources <bundle-path>
rorksign verify-resources <bundle-path>
rorksign team-id <profile-path> <credential-path> <password>For *-profile-key commands, credential-path can point to a PEM/DER private
key or a PKCS#12 container. The password unlocks PKCS#12, encrypted PKCS#8, and
traditional encrypted RSA PEM credentials. Pass an empty password for
unencrypted PEM/DER credentials.
sign ipa --profile-map reads a JSON object keyed by the final bundle
identifiers. The root bundle id must be present, and relative profile paths are
resolved from the JSON file directory. The explicit --certificate and --key
inputs define the signing identity, and every selected provisioning profile must
authorize that certificate:
{
"com.example.app": "profiles/App.mobileprovision",
"com.example.app.LiveProcess": "profiles/LiveProcess.mobileprovision"
}- Signing currently supports 64-bit Mach-O slices. 32-bit Mach-O files can be inspected, but not signed.
- The PKCS#12 importer focuses on common signing identities: one RSA, P-256, P-384, or P-521 private key plus a matching X.509 leaf certificate.
- Additional certificates are preserved in generated CMS output, but the signer does not perform platform trust-store evaluation.
- The CLI does not download CRLs or make Apple certificate-policy decisions.
--zip_levelpreserves the ZSign flag shape, but Swift ZIPFoundation exposes stored vs deflated output rather than exact numeric compression tuning.
swift test
swift build -c releaseThe test suite uses synthetic Mach-O fixtures and temporary app/IPA bundles. It asserts load-command mutations, SuperBlob indexes, CodeDirectory hash families, entitlement slots, resource-directory hashes, CMS embedding, universal slice rewrites, bundle signing order, app signing, CodeResources verification, PKCS#12 import, OCSP parsing, and CLI behavior.
Some identity and CMS tests call OpenSSL as an external compatibility oracle when it is available on the development machine. OpenSSL is not part of the production signing path.
rork-sign is licensed under the Apache License 2.0. See LICENSE.