LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

Ensuring device and app integrity and protecting service requests: LINE device attestation service (3)

1.1. Introduction

Hello, I am Kaixin Lian from the LINE App Dev Division. I'm responsible for various authentication features for the LINE messaging app.

In an era where application security is paramount, implementing robust authentication mechanisms is a crucial part of mobile application development. Our iOS development team collaborated with the Security R&D team to integrate their Device Attestation SDK into our app to enhance our security posture. As part of this collaboration, our team focused on the implementation and integration challenges specific to the iOS platform.

This blog post details our journey implementing iOS device attestation, the challenges we encountered specifically in the iOS environment, and our ongoing efforts to enhance reliability in production. By sharing our experiences with the iOS integration of the Device Attestation SDK, we hope to provide valuable insights for other iOS development teams undertaking similar implementations.

This article is the third part of our Device Attestation series. For a more comprehensive understanding of the Device Attestation, you may want to check out the previous articles in this series:

1.2. Understanding iOS device attestation

Device Attestation on iOS leverages Apple's App Attest API, which is part of Apple's DeviceCheck framework. It provides a cryptographic mechanism to verify that requests are coming from legitimate instances of your app running on genuine Apple devices. When integrated properly, it helps protect against:

  • App cloning or tampering
  • API abuse and scraping
  • Unauthorized access from jailbroken devices
  • Automated attacks from simulators or emulators

Our Security R&D team's Device Attestation SDK builds upon this foundation to create a consistent cross-platform security protocol, allowing developers to implement similar security measures across both Android and iOS platforms.

1.3. Our iOS integration architecture

To effectively integrate the Device Attestation SDK into our iOS application, we developed an architecture that bridges our app's needs with the functionality provided by the SDK. Our integration follows a layered approach that separates the client-side implementation from the logic for server communication:

1.3.1. Core components

  • AttestationCoordinator: A central component that orchestrates attestation and assertion processes by coordinating interactions with the SDK.
  • Relying Party Classes: Handler classes that manage communication between our app and the relying party servers.
  • Key Storage: A secure key management layer built upon the iOS keychain.
  • Configuration Provider: A controller that manages feature flags and timing configurations for graceful degradation if needed.
  • Repository: A manager that handles persistence of attestation state information.
  • Logger: A tool that provides detailed monitoring of the attestation process for debugging and analysis.

Let's examine a few of these components in more detail.

1.3.2. AttestationCoordinator

This is the main class that our application interacts with to initiate attestation and assertions:

public class AttestationCoordinator<AssertionRelyingParty: AssertionInterface, KeyStore: SecureStorage> {

    // The core attestation instance utilizing the SDK

    typealias AttestationInstance = AttestationService<

        AttestationRelyingParty,

        AssertionRelyingParty,

        KeyStore

    >



    private var attestationService: AttestationInstance?

    private let assertionRelyingParty: AssertionRelyingParty  // Instance of AssertionRelyingParty for assertion operations

    private let dataRepository: any StateRepository          // Repository for managing state data

    private let configManager: any ConfigurationInterface    // Manager for configuration settings

    private let logService: any LoggerInterface              // Service for logging operations

    private let secureStore: KeyStore                        // Secure storage for key management



    // Check if a new assertion is needed

    var needsAssertion: Bool {

        // Verify key existence and time-based refresh requirements

        let hasKey = (try? secureStore.retrieveKey().get()) != nil

        return !hasKey || shouldRefreshAssertion

    }



    // Determine if the assertion needs to be refreshed

    private var shouldRefreshAssertion: Bool {

        guard let previousAssertionTime = dataRepository.assertionTimestamp else {

            return true  // Refresh if no previous timestamp exists

        }



        // Check if enough time has passed since the last assertion

        let timeSinceLastAssertion = Date().timeIntervalSince(previousAssertionTime)

        return timeSinceLastAssertion > configManager.refreshThreshold

    }



    // Public method to create an assertion if required

    public func createAssertionIfRequired() {

        // Execute assertion process asynchronously

        executeAssertion()

    }



    // Implementation of assertion generation

    private func executeAssertion(with context: AssertionRelyingParty.Context? = nil) {

        // Set flag to indicate assertion process is active

        dataRepository.markAssertionActive(true)



        do {

            guard let attestationService = getAttestationService() else {

                throw AttestationError.notAvailable  // Throw error if service is unavailable

            }



            // Request the attestation service to create a new assertion

            attestationService.createAssertion(with: context)



            // Record successful assertion timestamp

            dataRepository.saveAssertionTimestamp(Date())

        }

        catch let error as AssertionError {

            let diagnosticInfo = createErrorReport(error: error, context: context)

            logService.recordError(error, information: diagnosticInfo)  // Log assertion-specific errors

        }

        catch { /* handle other errors */ }



        // Reset flag when process completes

        dataRepository.markAssertionActive(false)

    }

}

1.3.3. AssertionRelyingParty

Our AssertionRelyingParty class handles communication between our iOS app and the relying party servers, translating the standard Device Attestation protocol to iOS-specific requirements. We intentionally separate attestation and assertion relying parties to allow for greater flexibility in our architecture. While attestation is performed only within the LINE app for security reasons, assertions are designed to be used across various services:

public class AssertionRelyingParty: AssertionInterface {

    public typealias SessionData = String

    public typealias Context = String



    /// Represents the data used in the assertion process

    /// - sessionIdentifier: Unique identifier for the current assertion session

    /// - challengeResponse: Client data containing the challenge response

    public struct AssertionData {

        let sessionIdentifier: String

        let challengeResponse: Data

    }




    public init() {}




    /// Fetches assertion data, including session identifier and challenge response, from the server.

    public func getAssertionData(

        _ keyIdentifier: String?,

        with contextData: String?

    ) async throws -> AssertionData {

        logService.debug("Requesting challenge for assertion")



        // Fetch challenge from server

        let serverResponse = try await NetworkService.fetchAssertionChallenge()



        guard let sessionIdentifier = serverResponse.sessionIdentifier else {

            throw AssertionError.sessionIdentifierMissing

        }



        guard let challengeData = serverResponse.challenge else {

            throw AssertionError.challengeDataMissing

        }



        // Prepare client data with challenge

        let clientDataObject = ["challenge": challengeData]

        guard let challengeResponse = try? JSONSerialization.data(withJSONObject: clientDataObject) else {

            throw AssertionError.dataFormatInvalid

        }



        logService.debug("Challenge successfully received")

        return AssertionData(sessionIdentifier: sessionIdentifier, challengeResponse: challengeResponse)

    }




    /// Submits assertion data to the server for verification.

    public func verifyAssertion(

        _ keyIdentifier: String,

        with clientData: Data,

        session sessionIdentifier: String?,

        assertion: Data

    ) async throws -> Void {

        logService.debug("Submitting assertion for verification")



        guard let sessionId = sessionIdentifier else {

            throw AssertionError.sessionIdentifierMissing

        }



        // Send data to server for verification

        _ = try await NetworkService.submitAssertionVerification(

            sessionId: sessionId,

            keyIdentifier: keyIdentifier,

            assertionData: assertion.base64EncodedString(),

            clientData: clientData.base64EncodedString()

        )



        logService.debug("Assertion verification successful")

    }

}

1.3.4. Key Storage

For secure storage of key identifiers on iOS, we leveraged iOS's keychain capabilities through a secure storage implementation:

public class SecureKeyManager: SecureStorage {

    private let keychainService = SecurityManager.shared

    private let storageKey = "app.security.attestation.key"



    public init() {}




    public func storeKey(_ keyIdentifier: String) -> Result<Void, StorageError> {

        do {

            try keychainService.storeSecureData(keyIdentifier, withKey: storageKey)

            logService.debug("Key storage successful")

            return .success(())

        }

        catch {

            logService.error("Key storage failed: \(error.localizedDescription)")

            return .failure(.writeFailure(error: error))

        }

    }




    public func retrieveKey() -> Result<String?, StorageError> {

        do {

            let keyData = try keychainService.retrieveSecureData(withKey: storageKey)

            logService.debug("Key retrieval successful")

            return .success(keyData)

        }

        catch {

            logService.error("Key retrieval failed: \(error.localizedDescription)")

            return .failure(.readFailure(error: error))

        }

    }




    public func removeKey() -> Result<Void, StorageError> {

        do {

            try keychainService.removeSecureData(withKey: storageKey)

            logService.debug("Key removal successful")

            return .success(())

        }

        catch {

            logService.error("Key removal failed: \(error.localizedDescription)")

            return .failure(.deleteFailure(error: error))

        }

    }

}

1.4. iOS-specific implementation challenges

While the Device Attestation SDK provides a robust security foundation, we encountered several challenges specific to the iOS environment that required careful analysis and troubleshooting.

1.4.1. Identified iOS error patterns

After analyzing our logs from the iOS implementation, we identified several patterns of errors that occur during attestation and assertion processes:

1.4.1.1. During attestation

  • invalidKey (DCError Code 3):
    • Occurs when the App Attest service on iOS rejects a key for platform-specific reasons
    • May appear when attempting to create an attestation object with an invalid key
  • unknownSystemFailure (DCError Code 0):
    • Appears across various iOS device models, from older models to the latest iPhone 16 / iPhone 16 Pro models
    • May relate to iOS-specific memory management or background process handling
    • This is a general error within the App Attest framework, often related to key generation or validation failures

1.4.1.2. During assertion

  • invalidInput (DCError Code 2):
    • This error typically occurs during the assertion process
    • Based on our analysis of logs, this may happen when assertion parameters are not correctly formatted
  • invalidKey (DCError Code 3):
    • Occurs when calling assertion on a key that's already been attested in iOS
    • May appear when the key used for assertion is no longer valid

To mitigate these errors, we implemented:

  • Exponential backoff for retrying failed attestation attempts
  • Graceful fallback mechanisms when attestation repeatedly fails

1.4.2. iOS-specific error handling and logging

To better understand and troubleshoot these iOS-specific issues, we implemented comprehensive error reporting tailored to iOS:

private func createErrorReport(

    error: AssertionError,

    context: Context?

) -> String {

    var report = "[Error] \(error)"




    // Include timing information

    if let timestamp = dataRepository.assertionTimestamp {

        report += " | [lastAssertionTime] \(timestamp)"

    } else {

        report += " | [lastAssertionTime] None"

    }




    // Include feature state

    report += " | [featureEnabled] \(configManager.isFeatureEnabled)"




    // Include key information (hashed for privacy)

    if let keyData = try? secureStore.retrieveKey().get() {

        let keySize = keyData.count

        // Create a privacy-preserving hash of the key

        // We only include a partial hash to ensure user privacy while still allowing us to identify issues

        let dataBytes = Data(keyData.utf8)

        let hashValue = SecurityUtils.hashData(dataBytes)

        let truncatedHash = hashValue.prefix(8).hexEncodedString



        report += " | [keyLength] \(keySize)"

        report += " | [keyHash] \(truncatedHash)"

    } else {

        report += " | [keyHash] None"

    }




    // Include iOS-specific error details

    switch error {

    case let .systemFailure(errorCode):

        report += " | [SystemCode] \(errorCode)"

    case .keyInvalid:

        report += " | [ErrorType] InvalidKey"

    case let .invalidState(currentState):

        report += " | [CurrentState] \(currentState)"

    case let .connectionError(responseCode):

        report += " | [StatusCode] \(responseCode)"

    default:

        report += " | [ErrorType] \(type(of: error))"

    }




    return report

}

1.5. iOS reliability design strategies

Our iOS implementation included several key strategies to ensure reliability in the iOS environment. These design choices have proven valuable in addressing the iOS-specific challenges we've encountered:

1.5.1. Configuration management

We added a configuration provider to control the feature in our iOS app and allow for remote adjustments if needed:

public protocol ConfigurationInterface {

    var isFeatureEnabled: Bool { get }

   var refreshThreshold: TimeInterval { get }

    var maxRetryAttempts: Int { get }

}

1.5.2. iOS state tracking and recovery

We implemented iOS-specific state tracking to detect and recover from interrupted attestation/assertion attempts, which is particularly important given how iOS manages application lifecycle:

public protocol StateRepository {

    var isAssertionActive: Bool { get }

    var assertionTimestamp: Date? { get }




    func markAssertionActive(_ active: Bool)

    func saveAssertionTimestamp(_ timestamp: Date?)

}

1.5.3. iOS background handling with retry mechanism and jitter

To address the iOS version of the LINE app's lifecycle issues and ensure reliable operation, we implemented a smart retry mechanism that works well with iOS app state transitions:

// In AttestationCoordinator

private func shouldAttemptAssertion() -> Bool {

    // Only attempt assertion when app is in foreground

    // and sufficient time has passed since last attempt

    return isAppActive && !hasRecentlyFailed && !isCurrentlyAttempting

}

This approach is complemented by our jitter-based refreshing strategy. Jitter is crucial in distributed systems to prevent the "thundering herd" problem, where many clients retry operations simultaneously after a service disruption. By adding randomized delays (in our case, between 1-30 minutes), we ensure that:

  • Client requests are naturally distributed over time
  • Network congestion is reduced during app launches after an iOS update or Apple service restoration

Our production monitoring has shown this approach results in significantly-reduced peak loads on our authentication infrastructure during high-volume events, helping maintain system stability even when millions of devices are attempting to refresh their attestations.

1.5.4. Working with Apple Developer Technical Support

For iOS-specific issues, we've engaged with Apple's Developer Technical Support (DTS) team with detailed logs and reproduction steps. This iOS-focused approach complements the broader platform support offered by our company's Security R&D team.

1.6. Lessons learned from iOS implementation

Our journey implementing Device Attestation on iOS has taught us several valuable lessons specific to the platform:

  • iOS App Lifecycle Awareness: Account for the iOS app lifecycle and how it affects long-running security operations.
  • Version-Specific Behavior: Different iOS versions exhibit different behaviors with App Attest, requiring careful testing across multiple iOS versions.
  • Securely Store Keys in iOS Keychain: The iOS keychain has specific behaviors that differ from other platforms, requiring dedicated handling.
  • iOS Background Task Management: Attestation processes may be impacted by the aggressive background task management of iOS.
  • Progressive Enhancement for iOS: Implement the feature in a way that allows for graceful degradation on iOS devices that may not fully support all attestation capabilities.

1.7. Next steps

As we continue to enhance our iOS implementation of Device Attestation, we're exploring several iOS-specific improvements:

  • iOS version-specific configurations: Tailoring our approach based on known iOS version-specific behaviors.
  • Integration with iOS App Clips: Exploring how device attestation interacts with lightweight App Clips.
  • Enhanced iOS performance monitoring: Implementing more granular performance monitoring for iOS-specific attestation processes.
  • iOS 16+ optimizations: Taking advantage of newer iOS capabilities for improved security assertions.

1.8. Conclusion

Implementing our Device Attestation SDK on iOS requires careful consideration of platform-specific behaviors and challenges. Through our collaboration with the Security R&D team, we've been able to successfully integrate the device attestation technology into our iOS app while developing solutions to the iOS-specific challenges we encountered.

By sharing our iOS implementation experiences, we hope to help other iOS developers navigate similar integration processes. This ongoing collaboration between our teams ensures that we can provide robust security measures for our users while maintaining a seamless user experience on iOS.

Have you implemented Device Attestation in your iOS app? We'd love to hear about your experiences and approaches to solving iOS-specific challenges.