Swift SDK
ReadyThe Swift device SDK puts the whole device side of the ceremony inside your own iOS app: enroll a phone, receive challenges, and sign approvals with a key that lives in the Secure Enclave and never leaves it. Add one Swift Package, and you write UI — the SDK owns the cryptography. It is the same library the Ydentifi authenticator itself runs, free and public under Apache 2.0.
Who this is for: the iOS engineer embedding Ydentifi into an existing app. This page is the fast path — install, connect, enroll, approve — with the four calls you actually make. The full symbol reference lives in the SDK’s DocC; here we show the shape of a real integration.
Install#
One Swift Package, no third-party dependencies of its own beyond Apple’s own crypto and gRPC libraries.
.package(url: "https://github.com/ydentifi/ydentifi-device-swift", from: "1.0.0")File ▸ Add Package Dependencies… ▸ paste the URL aboveThen, wherever you drive the ceremony:
import YdentifiDeviceSDK
This page is the guided path. The complete per-symbol reference — every method, type, and error, generated straight from the SDK source with DocC — lives at Swift SDK API referenceAt launch.
Connect to a deployment#
A deployment exposes two endpoints — an onboarding endpoint for enrollment, and a mutually-authenticated gateway for everything after. You describe both once, with the pin from the setup code the user scanned, and hand the config to a transport.
// The user scanned the deployment's setup code. It carries the pin // of the deployment's own root CA. guard case .invite(let invite) = QRPayloadParser.parse(scannedString) else { return } // The signing key is generated inside the Secure Enclave and is // non-exportable — it never leaves the phone. let keyring = SecureEnclaveKeyring() var config = YdentifiTransportConfig( onboardingEndpoint: YdentifiEndpoint(host: "auth.acme.example", port: 443), gatewayEndpoint: YdentifiEndpoint(host: "auth.acme.example", port: 443), appVersion: try .fromBundle(), serverCertSPKIPins: [invite.serverCertSPKIPin] // the setup code's ?p= ) let device = YdentifiDevice(keyring: keyring, transport: YdentifiTransport(config: config))
Ydentifi always issues its own private root CA, never a publicly-trusted one. Every connection checks the server’s certificate chain against the pin in the setup code (?p=): a SHA-256 hash of that root’s public key. No system trust store is consulted, so a publicly-trusted certificate for your hostname cannot stand in for your server. A setup code without a pin does not parse. The pin is of the root, so renewing the server’s certificate leaves enrolled phones working. A deployment with a backup root lists it at enrolment, in challenge.deploymentSPKIPins, over the connection pinned to the code’s pin; add it to the account’s pins and the phone keeps working when the deployment moves to the backup. The SDK does not connect from the iOS Simulator: test on a device. See Certificates & PKI.
Enroll a device#
Enrollment binds one user to one phone. The user scans your enrollment QR; you decode it and run two moves. The SDK mints the keys, builds the certificate request, and proves possession under a single biometric prompt.
// `invite` is the setup code parsed above. // Move 1 — exchange the token for an attestation nonce + the // deployment's root-CA chain. let challenge = try await device.beginEnrollment(inviteToken: invite.token) // Move 2 — attest, generate keys, and complete. `platformAttestation` // is an App Attest assertion over challenge.attestationNonce. let enrolled = try await device.completeEnrollment( inviteToken: invite.token, permanentID: deviceUUIDString, platform: .iOS, platformAttestation: appAttestBlob, challenge: challenge ) // Promote the issued certificate + the hardware key into the mTLS // identity the gateway will use from here on. try config.setMTLSIdentity( certificateChain: [enrolled.mtlsCertDER] + enrolled.mtlsCertChain, keyring: keyring, deviceKeyID: enrolled.deviceKeyID ) let gateway = YdentifiDevice(keyring: keyring, transport: YdentifiTransport(config: config))
Store enrolled.userKeyID, enrolled.deviceKeyID and enrolled.mtlsCertDER on your account record. The Secure Enclave keys persist themselves in the Keychain; on next launch you reload the certificate, reconstruct SecureEnclaveKeyring(), and call setMTLSIdentity(…) before any gateway call. The userKeyID is the slot you sign approvals with; the deviceKeyID is the key the connection presents.
Approve a challenge#
A challenge reaches the phone as a 26-character ULID — scanned, deep-linked, or streamed (below). You fetch the full challenge, show it, and on the user’s tap the SDK signs the canonical payload with the key held behind the biometric check. iOS raises the Face ID prompt at sign time.
let fetched = try await gateway.fetchChallenge(challengeID: scannedULID) // Present fetched.context to the user. On "Approve": let ack = try await gateway.submitChallengeResponse( challengeID: fetched.challengeID, userKeyID: account.userKeyID, // persisted at enrollment opaqueID: fetched.opaqueID, rpID: fetched.rpID, expiresAtUnix: Int64(fetched.expiresAt.timeIntervalSince1970), context: fetched.context, locationConstraint: fetched.locationConstraint, expression: .approved, deviceObservedTimeMs: Int64(Date().timeIntervalSince1970 * 1000), authStrength: .biometric // the strength you actually performed ) // ack.finalStatus == .approved
To decline, pass expression: .declined with the declineReason the user chose. When fetched.requiresNumberMatching is true, pass the number the user picked as selectedMatchCode. If the challenge carried a location constraint, you attach a locationResult on the same call — see Authentication factors.
ChallengeSession wraps the fetch → submit lifecycle in a small state machine that traps the two classic mistakes — submitting twice, or submitting before fetching — and returns a typed error instead of a server round-trip. Construct one per challenge; it is single-use.
Receive challenges without push#
You don’t need APNs. While the app is foreground, open a server stream and challenges arrive over the same direct, mutually-authenticated gRPC connection — no Apple or Google intermediary in the path.
for try await notification in gateway.subscribeChallenges() {
// Each frame carries only the challengeID; fetch the full payload.
let challenge = try await gateway.fetchChallenge(
challengeID: notification.challengeID)
present(challenge)
}The stream stays open for the life of the connection; cancel the task to close it. For a long-lived, self-reconnecting subscription with a lifecycle you can render as a status dot, wrap it in ChallengeSubscription. Why this matters for sovereignty is covered in Why gRPC-only.
Handle errors#
Every failure surfaces as one YdentifiDeviceError enum, so a single catch can route the whole surface. The cases you handle most:
.cryptoFailure(underlying:)— the Face ID or Touch ID prompt ended without a signature: cancelled, failed, or locked out.underlyingis the system’sLAErrororTKError. A cancel is not an error to log; let them retry. A lockout (LAError.biometryLockout) clears once the device is unlocked with its passcode. ATKError.corruptedDatameans iOS invalidated the key after a face or finger changed: the account needs a new enrolment..server(reason:message:traceID:subject:autoExpiresAt:)— a typed server rejection (expired, replayed, device banned). Thereasonis aServerErrorReasonyou can switch on; thetraceIDis what a customer quotes to their operator..network(message:)— the deployment was unreachable within the RPC deadline.
Keys are generated and held in the Secure Enclave and are never exportable; signing is routed through the hardware. Nothing about that moves into your code — you build the enrollment and approval UI, and decide where in the app they appear.
- Secure-Enclave keys and every signature.
- Enrollment, challenge signing, cert renewal.
- Mutual TLS, server-cert pinning, the challenge stream.
- Canonical payloads that match the server byte-for-byte.
- The QR scan, the “add device” and approval screens.
- Persisting the certificate,
userKeyIDanddeviceKeyIDper account. - Checking the device for signs of modification, if your app refuses modified devices.
- Your app’s store release and lifecycle.