Skip to content
SDKs · iOSIn development

RootHeraldKit Swift Package

Swift package built on Apple App Attest (DCAppAttestService) for hardware-backed device claims on iOS 14+ / iPadOS 14+. Like every other platform SDK it is a keyless evidence collector: it holds no RootHerald key, opens no socket to RootHerald, and never renders a verdict. It collects an opaque evidence blob over a nonce your backend issued; your backend relays that blob to the same server→server verify API every SDK uses, which appraises it and returns the verdict.

In development

This SDK is implemented but not yet published to its package registry. Until it ships, collect an opaque evidence blob on the device and appraise it server-side with @rootherald/node (or any available server SDK). The API shown below is the planned surface and may change before release.

Availability: TestFlight preview, not yet published

RootHeraldKit is in development and is not published to any package registry yet — there is no Swift Package Manager release to pin and no App Store build. The package currently ships only as a TestFlight preview alongside the RootHerald companion demo app. The Swift Package Manager coordinates below are the planned distribution and the repository URL is a placeholder until the package goes public. The server→server rh.verify() half, however, is real and identical to every other SDK today.

How the integration is shaped

It is two server calls, never “one call.” Your backend mints a challenge, then — after the device collects evidence over that challenge — your backend verifies it. The verdict comes back directly from the verify call; there is no token for you to parse and no RootHerald key on the device.

  • Call 1 — mint a challenge (server). Your backend calls POST /api/v1/attestations/challenge with its rh_sk_ secret and relays the returned { challengeId, nonce } to the app.
  • Collect (device, keyless). The app calls RootHeraldKit over that nonce and gets back an opaque evidence blob. No verdict, no call to RootHerald.
  • Call 2 — verify (server). The app relays the blob to your backend, which calls rh.verify(evidence, { challengeId }) (the same server→server verify as every other SDK) and enforces the returned verdict.

Install (Swift Package Manager)

Planned distribution — the URL is a placeholder until the package is published. Add RootHeraldKit as a dependency and link the RootHeraldKit product into your app target.

Package.swiftswift
dependencies: [
    // Placeholder URL — the package is not published yet (TestFlight preview).
    .package(url: "https://github.com/RootHerald/rootherald-ios.git", branch: "main")
],
targets: [
    .target(
        name: "MyApp",
        dependencies: [
            .product(name: "RootHeraldKit", package: "rootherald-ios")
        ]
    )
]

Collect evidence on the device (keyless)

The client takes no key and no endpoint — there is nothing for it to connect to. It runs App Attest over the backend-issued nonce and returns an opaque Evidence value that your app relays to your own backend. The evidence carries exactly two fields, matching the platform-agnostic verify contract: attestationObject (base64 of the CBOR App Attest attestation object) and keyId (base64 of the App Attest key identifier).

SignupViewModel.swiftswift
import RootHeraldKit

@MainActor
final class SignupViewModel: ObservableObject {
    // Keyless: no api_key, no endpoint — nothing to connect to.
    private let client = RootHeraldClient()

    func submit(email: String, password: String) async {
        do {
            // Call 1 (server): your backend called
            // POST /api/v1/attestations/challenge with its rh_sk_ secret.
            // It returns a challengeId + nonce; relay them to the app.
            let challenge = try await api.mintChallenge(action: "signup")

            // Collect (device, keyless): run App Attest over the nonce and get
            // back an opaque evidence blob. No verdict, no call to RootHerald.
            let evidence = try await client.collectEvidence(nonce: challenge.nonce)
            // evidence.attestationObject : String   // base64 CBOR attestation object
            // evidence.keyId             : String   // base64 App Attest key id

            // Call 2 (server): relay the challengeId + opaque blob to YOUR
            // backend, which calls rh.verify() and enforces the verdict.
            try await api.signup(
                email: email,
                password: password,
                challengeId: challenge.challengeId,
                evidence: evidence
            )
        } catch {
            showError("Evidence collection failed: \(error)")
        }
    }
}

Verify on your backend (the same server→server call)

The device never talks to RootHerald. Your backend relays the opaque blob to POST /api/v1/attestations/verify — the identical server→server call desktop, Android, and web evidence all flow through — authenticated with your rh_sk_ secret via a server SDK such as @rootherald/node. The verdict returns directly in the response; nothing for the app to parse.

app/api/signup/route.ts (your backend)ts
import { RootHerald } from "@rootherald/node";

const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_…

// Call 1: mint the challenge the iOS app attests over.
export async function GET() {
  const { challengeId, nonce } = await rh.issueChallenge();
  return Response.json({ challengeId, nonce });
}

// Call 2: the app posts back { challengeId, evidence }. Appraise it — the
// verdict comes back directly; iOS evidence uses the SAME verify endpoint.
export async function POST(req: Request) {
  const { challengeId, evidence, email, password } = await req.json();

  // evidence = { attestationObject, keyId } straight from RootHeraldKit.
  const verdict = await rh.verify(evidence, {
    challengeId,
    policy: "rootherald:builtin:strict-hardware-permissive-mobile",
  });

  if (verdict.device.verdict !== "pass") {
    return Response.json({ error: "device_rejected" }, { status: 403 });
  }

  // verdict.device.ueid is a stable per-app-per-device pseudonym derived from
  // the App Attest key. Use it for one-device-one-account enforcement.
  const user = await usersRepo.create({ email, password, deviceId: verdict.device.ueid });
  return Response.json({ ok: true, userId: user.id });
}
App Attest entitlement

Your app must enable the App Attest capability under Signing & Capabilities. Without the entitlement, DCAppAttestService.isSupported returns false and collectEvidence throws RootHeraldError.unsupported. App Attest is unavailable on the iOS Simulator — collect evidence on a physical device.

iOS assurance tier

App Attest proves a genuine, unmodified build of your app running on genuine Apple hardware, keyed to a per-app-per-device secure-enclave key. There is no cross-app device identifier — the App Attest key is itself a privacy-preserving per-relying-party pseudonym, which is what verdict.device.ueid is derived from. iOS attestations resolve to the urn:rootherald:device:any ACR (there is no device:high tier on iOS), so a mobile-permissive policy such as strict-hardware-permissive-mobile is the right default rather than a desktop-TPM policy that expects a quote and boot log.