Skip to content

Attest

Register this device's signing key with DEA. The SDK creates the key the first time it is needed, inside the Secure Enclave where the device has one, so the private half cannot be read by your app or by the SDK. Where the device supports it, the registration also carries an Apple App Attest token, so DEA can check that the device and the app are genuine. It also carries, as short codes, the signs of a jailbroken or modified device the SDK found, such as a jailbreak's files or a debugger; never a file path or an app name.

When to use it

  • Before a sensitive step, to make sure the device is registered
  • To show attestation status in a diagnostics screen

Prerequisites

  • The SDK is installed and started. See the Quick Start.
  • The appKey DEA issued for this iOS app, passed as appKey. Without it, attest() answers .unsupported and contacts nobody. See Accounts and keys.
  • The platform setup below.

Platform setup

App Attest

Add the App Attest capability to your app:

  1. Open your project in Xcode.
  2. Select your app target, then Signing & Capabilities.
  3. Click + Capability and add App Attest.

This adds the com.apple.developer.devicecheck.appattest-environment entitlement. App Attest needs iOS 14 or later and is not available on the iOS Simulator. There, on older devices, and on devices without App Attest, the SDK sends the key without an App Attest token.

Usage

Attest

attest() makes network requests and blocks the calling thread until they finish (up to about 15 seconds per request, and as long again for App Attest). Never call it on the main thread:

import DeaSDK

Task.detached {
    guard let attestation = Dea.sdk?.attest() else { return }

    switch attestation.verdict {
    case .passed:
        // DEA accepted this device's key.
        break
    case .unsupported:
        // No key could be created on this device, or no appKey was given.
        print("DEA attestation unsupported: \(attestation.detail ?? "no device key")")
    case .failed:
        // DEA refused, could not be reached, or App Attest refused. See attestation.detail.
        print("DEA attestation failed: \(attestation.detail ?? "")")
    }
}
  • attest() never throws.
  • A passed result is cached, across launches too, until shortly before it expires, so calling it again costs nothing.
  • After the first attest(), the SDK renews the registration by itself shortly before it expires, including when the app returns to the foreground. An app that never calls attest() never registers a key and makes no attestation requests.
  • After DEA refuses a registration that used an App Attest token, attest() answers .failed without contacting DEA for a while — one hour at first, doubling with each refusal in a row, up to seven days — and detail says until when. Apple counts every App Attest attestation for this app on the device, so the SDK does not spend them on a registration DEA keeps refusing.

Check whether the platform vouched for the device

A passed verdict means DEA accepted the key. It is a statement about the device only when platformAttested is true:

if attestation.verdict == .passed && attestation.platformAttested {
    // Apple confirmed the device and app.
}

platformAttested is false when the registration went up without an App Attest token: on the simulator, on devices older than iOS 14 or without App Attest, and when App Attest did not answer in time.

Read the last result without attesting again

let last = Dea.sdk?.lastAttestation // nil before the first attest()

Check for a registration without contacting DEA

Task.detached {
    if let stored = Dea.sdk?.storedAttestation() {
        print("Already registered until \(stored.expiresAt ?? 0)")
    }
}

storedAttestation() returns the registration this install already holds, if it is still valid, without any network request and without using App Attest; nil means attest() would have to register again. It reads the keychain, so call it off the main thread. A registration found this way is renewed automatically, as after attest().

Renew now

Task.detached {
    if let renewed = Dea.sdk?.renewAttestation() {
        print("DEA attestation renewed: \(renewed.verdict)")
    }
}

renewAttestation() registers again even while the current result is still valid — for example right before a sensitive step. If the renewal fails, the previous valid result is kept. Like attest(), it blocks; call it off the main thread.

Start over

Task.detached {
    Dea.sdk?.forgetAttestation()
}

forgetAttestation() forgets this install's registration and any wait after refused attempts, so the next attest() registers from the start. The device key is kept. The automatic renewal stops until the next attest(). It waits for an attest() already in progress, so call it off the main thread.

Examples

Attest before a payment step (SwiftUI)

import DeaSDK
import SwiftUI

struct CheckoutButton: View {
    @State private var showPayment = false

    var body: some View {
        Button("Pay") {
            Task {
                let attestation = await Task.detached { Dea.sdk?.attest() }.value
                if attestation?.verdict != .passed {
                    print("DEA attestation: \(String(describing: attestation))")
                }
                showPayment = true
            }
        }
        .sheet(isPresented: $showPayment) {
            PaymentView()
        }
    }
}

Attest at launch (UIKit)

import DeaSDK
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        Dea.start()
        DispatchQueue.global(qos: .utility).async {
            _ = Dea.sdk?.attest()
        }
        return true
    }
}

API

Member Returns Description
attest() Attestation Registers the device key and returns the result. Blocking
renewAttestation() Attestation Registers again now, keeping the previous result on failure. Blocking
storedAttestation() Attestation? The valid registration this install holds, without a network request
forgetAttestation() Void Forgets the registration, so the next attest() registers from the start
lastAttestation Attestation? The last result, without attesting again
nextAttestationRefresh Date? When the automatic renewal will next run
appKey String? The app key the SDK sends; nil when none was given

Attestation:

Field Type Description
supported Bool Whether a key could be created on this device and an appKey was given
verdict Attestation.Verdict .passed, .failed or .unsupported
platformAttested Bool Whether an App Attest token went up with the key and DEA accepted it
platformTokenSpent Bool Whether this attempt used an App Attest attestation, whatever the result
expiresAt Int? Expiry, in epoch seconds
kid String? The key's identifier
tier String? The trust tier DEA assigned
integrity String? DEA's reading of the device's integrity, when its answer carries one
backing String? Where the key is kept: secure_enclave or software
detail String? Why attestation did not pass, when it did not