Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Device Check - Securing your App’s Communication

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Device Check - Securing your App’s Communication

In this talk we take a closer look at the DeviceCheck framework and the services it provides to make our apps and backend services more resilient against unauthorised use. We will compare device identification and app attestation and show how we can validate the integrity of our app.

The DeviceCheck Framework has been around since iOS11 but nobody has ever really heard about it. At least not me… I stumbled opon this when playing around with some new Firebase Service. They enforce their own version of this called AppCheck for some services by now. So I wanted to find out how it works and dug deeper.

In this talk I take a look at the two services the framework provides: DeviceIdentification and AppAttestation. I will show client and server implementation for iOS and Vapor and show how to make your network communication more secure.

It’s not a silver bullet, but an alternative or addition to certificate pinning and another tool in our iOS Dev tool belt.

Avatar for Peter Kurzok

Peter Kurzok

November 13, 2025

More Decks by Peter Kurzok

Other Decks in Programming

Transcript

  1. Device Check Framework Device Identification Identify devices without revealing personally

    identifiable information App Attest Verify the integrity of your app
  2. Device Check Framework Device Identification Identify devices without revealing personally

    identifiable information App Attest Verify the integrity of your app App Check Firebase Service
  3. Device Identification Two bits as device state stored by Apple

    Per device, per developer 00 01 11 10 W W D C 2017
  4. Device Identification Two bits as device state stored by Apple

    Per device, per developer Persistent across device resets 00 01 11 10 W W D C 2017
  5. Device Identification Two bits as device state stored by Apple

    Per device, per developer Persistent across device resets Query 00 01 11 10 W W D C 2017
  6. Device Identification Two bits as device state stored by Apple

    Per device, per developer Persistent across device resets Query Update 00 01 11 10 W W D C 2017
  7. Device Identification Two bits as device state stored by Apple

    Per device, per developer Persistent across device resets Query Update Validate Device 00 01 11 10 W W D C 2017
  8. Device Check Device Identification DeviceCheck Service ! Your Service !

    Device Token Request incl. DeviceToken Your App
  9. Device Check Device Identification DeviceCheck Service ! Your Service !

    Device Token Request incl. DeviceToken Your App Token Validation Request
  10. Device Check Device Identification DeviceCheck Service ! Your Service !

    Device Token Request incl. DeviceToken Your App Token Validation Request 200 OK
  11. Device Check Device Identification DeviceCheck Service ! Your Service !

    Device Token Request incl. DeviceToken Your App Token Validation Request 200 OK
  12. Device Check Device Identification import DeviceCheck iO S let token

    = try await DCDevice.current.generateToken() guard DCDevice.current.isSupported else { // fail gracefully return }
  13. Device Check Device Identification import DeviceCheck iO S let token

    = try await DCDevice.current.generateToken() guard DCDevice.current.isSupported else { // fail gracefully return } var request = URLRequest(url: url) request.setValue(apiKey, forHTTPHeaderField: "apiKey") request.setValue(token.base64EncodedString(), forHTTPHeaderField: "deviceToken")
  14. Device Check Device Identification public func configure(_ app: Application) async

    throws { app.middleware.use(TokenAuthenticator()) … } Vapor import Vapor
  15. struct TokenAuthenticator: AsyncRequestAuthenticator { func authenticate(request: Vapor.Request) async throws {

    } } Device Check Device Identification public func configure(_ app: Application) async throws { app.middleware.use(TokenAuthenticator()) … } Vapor import Vapor
  16. struct TokenAuthenticator: AsyncRequestAuthenticator { func authenticate(request: Vapor.Request) async throws {

    } } Device Check Device Identification public func configure(_ app: Application) async throws { app.middleware.use(TokenAuthenticator()) … } Vapor guard request.hasValidApiToken else { throw Abort(.unauthorized) } import Vapor
  17. struct TokenAuthenticator: AsyncRequestAuthenticator { func authenticate(request: Vapor.Request) async throws {

    } } Device Check Device Identification public func configure(_ app: Application) async throws { app.middleware.use(TokenAuthenticator()) … } Vapor guard request.hasValidApiToken else { throw Abort(.unauthorized) } import Vapor guard try await request.hasValidDeviceToken else { throw Abort(.unauthorized) }
  18. Device Check Device Identification extension Vapor.Request { } Vapor var

    hasValidApiToken: Bool { headers.first(name: "apiKey") == "HighlySecretAPIKey" }
  19. Device Check Device Identification extension Vapor.Request { } Vapor var

    hasValidApiToken: Bool { headers.first(name: "apiKey") == "HighlySecretAPIKey" } var hasValidDeviceToken: Bool { get async throws { guard let deviceToken = headers.first(name: "deviceToken") else { return false } return try await validateToken(deviceToken) } }
  20. Device Check Device Identification Vapor func validateToken(_ token: String) async

    throws -> Bool { } let jwtToken = try await generateJWT()
  21. Device Check Device Identification Vapor func validateToken(_ token: String) async

    throws -> Bool { } let jwtToken = try await generateJWT() let response = try await client.post(„https://api.development.devicecheck.apple.com/v1/ validate_device_token") { req in }
  22. Device Check Device Identification Vapor func validateToken(_ token: String) async

    throws -> Bool { } let jwtToken = try await generateJWT() let response = try await client.post(„https://api.development.devicecheck.apple.com/v1/ validate_device_token") { req in } try req.content.encode(DeviceValidationRequest(deviceToken: token)) req.headers.add(name: "Authorization", value: "Bearer \(jwtToken)")
  23. Device Check Device Identification Vapor func validateToken(_ token: String) async

    throws -> Bool { } let jwtToken = try await generateJWT() return response.status == .ok let response = try await client.post(„https://api.development.devicecheck.apple.com/v1/ validate_device_token") { req in } try req.content.encode(DeviceValidationRequest(deviceToken: token)) req.headers.add(name: "Authorization", value: "Bearer \(jwtToken)")
  24. App Attestation Establishing your app’s integrity Ensure that requests your

    server receives come from legitimate instances of your app. W W D C 2021
  25. Device Check App Attest private func generateKey() async throws {

    } iO S guard DCAppAttestService.shared.isSupported else { // Fail gracefully return } import DeviceCheck
  26. Device Check App Attest private func generateKey() async throws {

    } iO S let keyId = try await DCAppAttestService.shared.generateKey() guard DCAppAttestService.shared.isSupported else { // Fail gracefully return } import DeviceCheck
  27. Device Check App Attest private func generateKey() async throws {

    } iO S let keyId = try await DCAppAttestService.shared.generateKey() guard DCAppAttestService.shared.isSupported else { // Fail gracefully return } keychain.attestationKeyId = keyId import DeviceCheck
  28. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge Challenge Request
  29. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Challenge Request
  30. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Attest Key !" Challenge Request
  31. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Attest Key !" # Challenge Request
  32. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Attest Key !" # Challenge Request # " Post Attestation
  33. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Attest Key !" Validate and Persist Attestation # Challenge Request # " Post Attestation
  34. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Attest Key !" Validate and Persist Attestation # Challenge Request # " Post Attestation
  35. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Attest Key !" Validate and Persist Attestation # Challenge Request # " Post Attestation
  36. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Attest Key !" Validate and Persist Attestation # Challenge Request # " Post Attestation
  37. Device Check App Attest Your App App Attest Service !

    Your Service ! Persist Challenge " Attest Key !" Validate and Persist Attestation # Challenge Request # " Post Attestation
  38. Device Check App Attest let keyId = keychain.attestationKeyId iO S

    let hash = Data(SHA256.hash(data: challenge)) let challenge = await fetchChallenge()
  39. Device Check App Attest let keyId = keychain.attestationKeyId iO S

    let attestation = try await DCAppAttestService.shared.attestKey(keyId, clientDataHash: hash) let hash = Data(SHA256.hash(data: challenge)) let challenge = await fetchChallenge()
  40. Device Check App Attest Vapor import Crypto let challenge =

    Data(AES.GCM.Nonce()) var challengeStorage: [UUID: Data] = [:]
  41. Device Check App Attest Vapor import Crypto let challengeID =

    UUID() challengeStorage[challengeID] = challenge let challenge = Data(AES.GCM.Nonce()) var challengeStorage: [UUID: Data] = [:]
  42. Device Check App Attest Vapor import AppAttest let attestation: Data

    = ... let keyID: Data = ... let challengeID: UUID = ...
  43. Device Check App Attest Vapor import AppAttest let attestation: Data

    = ... let keyID: Data = ... let challengeID: UUID = ... let challenge = challengeStorage[challengeID]
  44. Device Check App Attest Vapor import AppAttest let attestation: Data

    = ... let keyID: Data = ... let challengeID: UUID = ... let challenge = challengeStorage[challengeID] let request = AttestationRequest(attestation: attestation, keyID: keyID) let appID = AppID(teamID: "83Z139DVZ2", bundleID: "com.example.myapp")
  45. Device Check App Attest Vapor import AppAttest let attestation: Data

    = ... let keyID: Data = ... let challengeID: UUID = ... let challenge = challengeStorage[challengeID] let request = AttestationRequest(attestation: attestation, keyID: keyID) let appID = AppID(teamID: "83Z139DVZ2", bundleID: "com.example.myapp") let result = try AppAttest.verifyAttestation(challenge: challenge, request: request, appID: appID) attestationStorage[keyID] = result
  46. Attestation vs. Assertion After successfully verifying a key’s attestation, your

    server can require the app to assert its legitimacy for any or all future server requests.
  47. Device Check Assertions Your App App Attest Service ! Your

    Service ! Persist Challenge Challenge Request
  48. Device Check Assertions Your App App Attest Service ! Your

    Service ! Persist Challenge " Challenge Request
  49. Device Check Assertions Your App App Attest Service ! Your

    Service ! Persist Challenge " Generate Assertion !" Challenge Request
  50. Device Check Assertions Your App App Attest Service ! Your

    Service ! Persist Challenge " Generate Assertion !" # Challenge Request
  51. Device Check Assertions Your App App Attest Service ! Your

    Service ! Persist Challenge " Generate Assertion !" # Challenge Request # " Request including Assertion
  52. Device Check Assertions Your App App Attest Service ! Your

    Service ! Persist Challenge " Generate Assertion !" # Challenge Request # " Request including Assertion
  53. Device Check Assertions Your App App Attest Service ! Your

    Service ! Persist Challenge " Generate Assertion !" Validate and Persist Assertion # Challenge Request # " Request including Assertion
  54. Device Check Assertions Your App App Attest Service ! Your

    Service ! Persist Challenge " Generate Assertion !" Validate and Persist Assertion # Challenge Request # " Request including Assertion
  55. Device Check Assertions iO S let keyId = keychain.attestationKeyId let

    challenge = await fetchChallenge() let request = [ "action": "getGameLevel", "levelId": "1234", "challenge": challenge ]
  56. Device Check Assertions iO S let keyId = keychain.attestationKeyId let

    challenge = await fetchChallenge() let request = [ "action": "getGameLevel", "levelId": "1234", "challenge": challenge ] guard let clientData = try? JSONEncoder().encode(request) else { return } let hash = Data(SHA256.hash(data: clientData))
  57. Device Check Assertions iO S let assertion = try await

    DCAppAttestService.shared.generateAssertion(keyId, clientDataHash: hash) let keyId = keychain.attestationKeyId let challenge = await fetchChallenge() let request = [ "action": "getGameLevel", "levelId": "1234", "challenge": challenge ] guard let clientData = try? JSONEncoder().encode(request) else { return } let hash = Data(SHA256.hash(data: clientData))
  58. Device Check Assertion Vapor let assertion: Data = ... let

    keyID: Data = ... let challengeID: UUID = ...
  59. Device Check Assertion Vapor let assertion: Data = ... let

    keyID: Data = ... let challengeID: UUID = ... let challenge = challengeStorage[challengeID] let attestation = attestationStorage[keyID] let previousAssertion = assertionStorage[keyID]
  60. Device Check Assertion Vapor let assertion: Data = ... let

    keyID: Data = ... let challengeID: UUID = ... let challenge = challengeStorage[challengeID] let attestation = attestationStorage[keyID] let previousAssertion = assertionStorage[keyID] let request = AppAttest.AssertionRequest( assertion: assertion, clientData: clientData, challenge: challenge )
  61. Device Check Assertion Vapor let assertion: Data = ... let

    keyID: Data = ... let challengeID: UUID = ... let challenge = challengeStorage[challengeID] let attestation = attestationStorage[keyID] let previousAssertion = assertionStorage[keyID] let request = AppAttest.AssertionRequest( assertion: assertion, clientData: clientData, challenge: challenge ) let appID = AppAttest.AppID( teamID: "99ED34GJ9X", bundleID: "com.peterkurzok.AppAttestationClient" )
  62. Device Check Assertion Vapor let assertion: Data = ... let

    keyID: Data = ... let challengeID: UUID = ... let result = try AppAttest.verifyAssertion( challenge: challenge, request: request, previousResult: previousAssertion, publicKey: attestation.publicKey, appID: appID ) appAttestStorage.store(assertion: result, for: keyID) let challenge = challengeStorage[challengeID] let attestation = attestationStorage[keyID] let previousAssertion = assertionStorage[keyID] let request = AppAttest.AssertionRequest( assertion: assertion, clientData: clientData, challenge: challenge ) let appID = AppAttest.AppID( teamID: "99ED34GJ9X", bundleID: "com.peterkurzok.AppAttestationClient" )
  63. Device Check When to use App Attest Your App Secure

    Enclave App Attest Your Service ! ! !!
  64. Device Check When to use App Attest Your App Secure

    Enclave App Attest Your Service ! ! !! ! Establish sessions
  65. Device Check When to use App Attest Your App Secure

    Enclave App Attest Your Service ! ! !! ! Establish sessions ! State synchronisation
  66. Device Check When to use App Attest Your App Secure

    Enclave App Attest Your Service ! ! !! ! Establish sessions ! State synchronisation ! Valuable state transitions
  67. Device Check When to use App Attest Your App Secure

    Enclave App Attest Your Service ! ! !! ! Establish sessions ! State synchronisation ! Valuable state transitions ! Real time operations
  68. Device Check When to use App Attest Your App Secure

    Enclave App Attest Your Service ! ! !! ! Establish sessions ! State synchronisation ! Valuable state transitions ! Real time operations ! Low latency operations
  69. Device Check Gotchas ⚠ If your server fails to verify

    the attestation object, discard the key identifier!
  70. Conclusion ✅ Prevents replay attacks or bot abuse ⚠ Protects

    your tokens not your payload # This is not the silver bullet ⚒ Another tool in your tool belt % Use it wisely (not for every request)