The iOS VTA mobile agent — a login/authentication agent that provides biometric
AAL1→AAL2 step-up. It consumes the vta-mobile-core engine (Rust + UniFFI,
built in OpenVTC/verifiable-trust-infrastructure)
as a precompiled VtaMobileCore.xcframework, distributed via GitHub Releases
and pinned by SwiftPM checksum.
Package.swift SwiftPM manifest (pins the engine release)
Sources/VtaMobileCore/ generated UniFFI Swift bindings (vendored)
Sources/VtaMobileAgent/ agent façade over the engine
Tests/VtaMobileAgentTests/ on-simulator smoke test
App/ reference SwiftUI sources for the app target
.github/workflows/ci.yml runs the smoke test on an iOS Simulator
VtaMobileCoreFFI (the binaryTarget) is the compiled engine + C module.
VtaMobileCore compiles the generated Swift wrapper against it. VtaMobileAgent
is the thin façade the app imports.
VtaMobileCore.xcframework contains iOS slices only (device + simulator).
A plain swift build / swift test targets macOS and will fail to find a
slice — that's expected. Always build/test against an iOS Simulator:
xcodebuild test -scheme VtaMobileAgent-Package \
-destination 'platform=iOS Simulator,name=iPhone 16'(Run xcrun simctl list devices available | grep -i iphone to pick an
installed simulator.) Or open Package.swift in Xcode, choose an iPhone
simulator, and press ⌘U.
Package.swift pins one engine release. The Swift wrapper
(Sources/VtaMobileCore/VtaMobileCore.swift) and the binary checksum are a
matched set — never mix versions. To adopt a new
vta-mobile-core-vX.Y.Z release:
TAG=vta-mobile-core-v0.1.0 # the release to adopt
REPO=OpenVTC/verifiable-trust-infrastructure
# 1. re-vendor the generated wrapper
gh release download "$TAG" -R "$REPO" -p 'VtaMobileCore.swift' \
-D Sources/VtaMobileCore/ --clobber
# 2. read the checksum to paste into Package.swift (engineChecksum)
gh release download "$TAG" -R "$REPO" -p 'VtaMobileCore.xcframework.zip.sha256' -O -Then set engineTag + engineChecksum in Package.swift to match.
On first setup,
engineChecksumis a placeholder — paste the real value from thevta-mobile-core-v0.1.0release (step 2 above) before the remote build will resolve.
To iterate against a locally-built xcframework (no release needed), build it in
the engine repo (vta-mobile-core/scripts/package-ios.sh) and drop a
VtaMobileCore.xcframework at this package's root (it's gitignored). When that
file is present, Package.swift uses it instead of the published release:
ln -s /abs/path/to/target/mobile/ios/VtaMobileCore.xcframework ./VtaMobileCore.xcframework
xcodebuild test -scheme VtaMobileAgent-Package \
-destination 'platform=iOS Simulator,name=iPhone 16'(Remember to also vendor that build's VtaMobileCore.swift into
Sources/VtaMobileCore/ so the wrapper matches the binary.)
The SwiftUI app target (App/, bundle id org.openvtc.vta.agent) is described
declaratively in project.yml and generated with XcodeGen — the .xcodeproj
is not committed (regenerate it any time):
brew install xcodegen # once
xcodegen generate # writes VtaMobileAgentApp.xcodeproj
open VtaMobileAgentApp.xcodeprojPick an installed iPhone simulator (the latest-runtime one — e.g. iPhone 17
— is safest; name=iPhone 16 only resolves if a matching OS is installed) and
Run.
The app is organised as a bottom tab bar so each context is focused:
- Home — at-a-glance status hero + the single primary action. Once configured the agent runs itself; this screen just reflects that.
- Test — manual surfaces for development: self step-up, who am I, live mediator listening, pasted ratification, push-wake registration.
- History — a chronological, color-coded record of authentications, approvals (live / pasted / push), and errors.
- Logs — the engine + app
stdout/stderrstream captured in-app (LogStore), with copy/clear — diagnose on-device without Xcode attached. - Settings — all configuration (VTA DID/URL/mediator/gateway, auto-connect), the device identity, and the theme picker.
Everything is auto and recoverable. Once a VTA is configured the agent auto-connects on launch, refreshes its token ahead of expiry, and supervises the mediator listener with exponential-backoff reconnects — a dropped network/VTA recovers with no user action. A connection status pill is always visible in the nav bar on every tab.
Themes (Theme.swift) are user-selectable at runtime (Vibrant / Neon /
Pastel / Minimal) and persisted; the choice restyles the whole app live.
CLI build/run without opening Xcode:
xcodegen generate
xcodebuild build -scheme VtaMobileAgentApp \
-destination 'platform=iOS Simulator,name=iPhone 17'The agent can be woken by a push to ratify a step-up while backgrounded,
instead of holding a live mediator connection. The contentless push is a
doorbell (push wake-up binding
/binding/push/0.1); the real
(encrypted) approve-request is pulled from the mediator after the app wakes.
On-device flow (PushRegistration.swift + AppDelegate.swift):
- Enable push wake → request notification authorization +
registerForRemoteNotifications. - APNs returns the device token → engine
build_push_register→ gateway (push/register, the token is held by the gateway only) → opaqueWakeHandle. - Engine
build_device_set_wake(holder-signed) → VTA (device/set-wake) — the VTA owns the trigger allowlist and provisions the gateway. - On a delegated step-up the VTA sends
push/wake→ the gateway delivers a contentless APNs push → the app wakes → drains its mediator → ratifies the approve-request with the holder key (receiveStepUpOnce).
Apple setup (token-based APNs):
- An App ID for
org.openvtc.vta.agentwith Push Notifications enabled, and Xcode automatic signing (it provisions the device + push entitlement). The app declares theaps-environmententitlement + theremote-notificationbackground mode (both viaproject.yml). - An APNs Auth Key (
.p8) + its Key ID + your Team ID — these go in the gateway (GATEWAY_APNS_KEY_FILE/GATEWAY_APNS_KEY_ID/GATEWAY_APNS_TEAM_ID), never in the app. - A real device — APNs doesn't deliver remote pushes to the Simulator. Dev
builds get a sandbox APNs token, so the gateway routes via the APNs sandbox
host automatically (the registration's
environmentis.sandbox).
Quick delivery check (no VTA). Tap Enable push wake; the app shows its
APNs token in the UI (also printed to the Xcode console). Run the gateway
with the APNs creds and fire a wake straight at the token — no did:webvh
gateway identity, no VTA:
cargo run -- test-wake-apns http://<gateway-host>:8300 <apns-token> org.openvtc.vta.agentThe phone wakes and drains its mediator. (For the full VTA-triggered loop, register the wake channel from the app instead and trigger a delegated step-up.)
Apache-2.0.