Skip to content

Commit

Permalink
Merge pull request #241 from ForgeRock/SDKS-2761
Browse files Browse the repository at this point in the history
SDKS-2761
  • Loading branch information
jeyanthanperiyasamy authored Oct 25, 2023
2 parents 7cf3bd3 + 3b737d6 commit 4218690
Show file tree
Hide file tree
Showing 15 changed files with 1,524 additions and 20 deletions.
71 changes: 58 additions & 13 deletions FRAuth/FRAuth.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
BlueprintName = "FRAuthTests"
ReferencedContainer = "container:FRAuth.xcodeproj">
</BuildableReference>
<SkippedTests>
<Test
Identifier = "AA_07_AppIntegrityTest">
</Test>
</SkippedTests>
</TestableReference>
</Testables>
</TestAction>
Expand Down
237 changes: 237 additions & 0 deletions FRAuth/FRAuth/AppIntegrity/FRAppAttestDomainModal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
//
// FRAppIntegrityDomainModal.swift
// FRAuth
//
// Copyright (c) 2023 ForgeRock. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
//

import Foundation
import CryptoKit
import DeviceCheck

/// Protocol to override attestation
@available(iOS 14.0, *)
public protocol FRAppAttestation {
/// Handle attestation and assertion
/// - Parameter challenge: Challenge Received from server
/// - Throws: `FRDeviceCheckAPIFailure and Error`
/// - Returns: FRAppIntegrityKeys for attestation and assertion
func requestIntegrityToken(challenge: String) async throws -> FRAppIntegrityKeys

}

/// Attestation modal to fetch the result for the given callback
@available(iOS 14.0, *)
struct FRAppAttestDomainModal: FRAppAttestation {

private let service: FRAppAttestService
private let bundleIdentifier: String?
private let encoder: JSONEncoder
private var appIntegrityKeys: FRAppIntegrityKeys
private let challengeKey = "challenge"
private let bundleIdKey = "bundleId"
private let delimiter = "::";

// Create a static property to hold the shared instance
static var shared: FRAppAttestation = {
return FRAppAttestDomainModal()
}()


// MARK: - Init

/// Initializes FRAppAttestDomainModal
///
/// - Parameter service: FRAppAttestService to connect AppAttestation server
/// - Parameter appIntegrityKeys: FRAppIntegrityKeys to fetch the keys result
/// - Parameter bundleIdentifier: BundleId of the application
/// - encoder encoder: Encoder to encode the Data
init(service: FRAppAttestService = FRAppAttestServiceImpl(),
appIntegrityKeys: FRAppIntegrityKeys = FRAppIntegrityKeys(),
bundleIdentifier: String? = Bundle.main.bundleIdentifier,
encoder: JSONEncoder = JSONEncoder()) {
self.service = service
self.bundleIdentifier = bundleIdentifier
self.encoder = encoder
self.appIntegrityKeys = appIntegrityKeys
}

/// Handle attestation and assertion
/// - Parameter challenge: Challenge Received from server
/// - Throws: `FRDeviceCheckAPIFailure and Error`
/// - Returns: FRAppIntegrityKeys for attestation and assertion
func requestIntegrityToken(challenge: String) async throws -> FRAppIntegrityKeys {
do {
let result = try validate(challenge: challenge)
guard let unwrapIdentifier = self.appIntegrityKeys.getKey() else {
return try await attestation(challenge: result.0, jsonData: result.1)
}
FRLog.i("AppIntegrityCallback::Key already exist, Do assertion")
let seperatedObject = unwrapIdentifier.components(separatedBy: delimiter)
if seperatedObject.count > 1 {
let keyId = seperatedObject[0]
let attestation = seperatedObject[1]
return try await assertion(challenge: result.0,
jsonData: result.1,
keyIdValue: keyId,
attestationValue: attestation)
} else {
FRLog.e("AppIntegrityCallback::KeyChain value is nil/Empty and the key exist")
throw FRDeviceCheckAPIFailure.keyChainError
}
}
catch let error as DCError {
throw FRDeviceCheckAPIFailure.error(code: error.errorCode)
}
catch {
throw error
}
}

/// Handle validation
///
/// - Parameter challenge: Challenge Received from server
/// - Throws: `FRDeviceCheckAPIFailure and Error`
/// - Returns: Challenge and userClientData
private func validate(challenge: String) throws -> (Data, Data) {

if !service.isSupported() {
throw FRDeviceCheckAPIFailure.featureUnsupported
}

guard let bundleIdentifier = bundleIdentifier, !bundleIdentifier.isEmpty else {
throw FRDeviceCheckAPIFailure.invalidBundleIdentifier
}

guard let challengeUtf8 = challenge.data(using: .utf8), !challengeUtf8.isEmpty else {
throw FRDeviceCheckAPIFailure.invalidChallenge
}

let userClientData = [challengeKey: challenge,
bundleIdKey: bundleIdentifier]

guard let jsonData = try? encoder.encode(userClientData) else {
throw FRDeviceCheckAPIFailure.invalidClientData
}

return (challengeUtf8, jsonData)

}

/// attestation
///
/// - Parameter challenge: Challenge Received from server
/// - Parameter jsonData: jsonData Received from server
/// - Throws: `FRDeviceCheckAPIFailure and Error`
/// - Returns: FRAppIntegrityKeys
private func attestation(challenge: Data,
jsonData: Data) async throws -> FRAppIntegrityKeys {
let keyId = try await service.generateKey()
let result = try await service.attest(keyIdentifier: keyId, clientDataHash: Data(SHA256.hash(data: challenge)))
let attestation = result.base64EncodedString()
return FRAppIntegrityKeys(attestKey: attestation,
assertKey: nil,
keyIdentifier: keyId,
clientDataHash: jsonData.base64EncodedString())
}

/// assertion
///
/// - Parameter challenge: Challenge Received from server
/// - Parameter jsonData: jsonData Received from server
/// - Parameter keyIdValue: keyIdValue from keychain
/// - Parameter attestationValue: attestationValue from keychain
/// - Throws: `FRDeviceCheckAPIFailure and Error`
/// - Returns: FRAppIntegrityKeys
private func assertion(challenge: Data,
jsonData: Data,
keyIdValue: String,
attestationValue: String) async throws -> FRAppIntegrityKeys {
do {
let assertion = try await withRetry {
try await service.generateAssertion(keyIdentifier: keyIdValue, clientDataHash: Data(SHA256.hash(data: challenge))).base64EncodedString()
}
return FRAppIntegrityKeys(attestKey: attestationValue,
assertKey: assertion,
keyIdentifier: keyIdValue,
clientDataHash: jsonData.base64EncodedString())

} catch {
FRLog.e("AppIntegrityCallback::Error Recovering \(error.localizedDescription)")
self.appIntegrityKeys.deleteKey()
return try await attestation(challenge: challenge, jsonData: jsonData)
}

}

/// withRetry
///
/// - Parameter maxRetries: Challenge Received from server
/// - Parameter operation: execute the operation
/// - Throws: `Error`
/// - Returns: T genric operation
private func withRetry<T>(maxRetries: Int = 2, operation: @escaping () async throws -> T) async throws -> T {
var currentRetry = 0
var lastError: Error = FRDeviceCheckAPIFailure.unknownError
repeat {
do {
return try await operation()
}
catch {
lastError = error
currentRetry += 1
}
} while currentRetry < maxRetries
throw lastError
}
}

/// Results of AppIntegrity Failures
/// We need to return some of failures for iOS12, iOS13 devices as well.
public enum FRDeviceCheckAPIFailure: String, Error {
case unknownSystemFailure
case featureUnsupported
case invalidInput
case invalidKey
case serverUnavailable
case invalidChallenge
case invalidBundleIdentifier
case invalidClientData
case unknownError
case keyChainError

var clientError: String {
switch self {
case FRDeviceCheckAPIFailure.featureUnsupported:
return FRAppIntegrityClientError.unSupported.rawValue
default:
return FRAppIntegrityClientError.clientDeviceErrors.rawValue
}
}

static func error(code: Int) -> FRDeviceCheckAPIFailure {
switch code {
case DCError.unknownSystemFailure.rawValue:
return .unknownSystemFailure
case DCError.featureUnsupported.rawValue:
return .featureUnsupported
case DCError.invalidInput.rawValue:
return .invalidInput
case DCError.invalidKey.rawValue:
return .invalidKey
case DCError.serverUnavailable.rawValue:
return .serverUnavailable
default:
return .unknownError
}
}
}

/// List of clientErrors sent to AM
public enum FRAppIntegrityClientError: String {
case unSupported = "Unsupported"
case clientDeviceErrors = "ClientDeviceErrors"
}
97 changes: 97 additions & 0 deletions FRAuth/FRAuth/AppIntegrity/FRAppAttestServiceImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// FRAppIntegrityService.swift
// FRAuth
//
// Copyright (c) 2023 ForgeRock. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
//


import Foundation
import DeviceCheck

/// Protocol to assert the legitimacy of a particular instance of your app to your server.
@available(iOS 14.0, *)
protocol FRAppAttestService {
/// Creates a new cryptographic key for use with the App Attest service.
/// - Returns: KeyId An identifier that you use to refer to the key. The framework securely
/// stores the key in the Secure Enclave.
/// - Throws: `DCError`
func generateKey() async throws -> String
/// Asks Apple to attest to the validity of a generated cryptographic key.
///
/// - Parameters:
/// - keyIdentifier: The identifier you received when generating a cryptographic key by calling the generateKey(completionHandler:) method.
/// - clientDataHash: A SHA256 hash of a unique, single-use data block that embeds a challenge from your server.
/// - Throws: `DCError`
/// - Returns: A statement from Apple about the validity of the key associated with keyId. Send this to your server for processing OR DCError instance that indicates the reason for failure, or nil on success.
func attest(keyIdentifier: String, clientDataHash: Data) async throws -> Data
/// Creates a block of data that demonstrates the legitimacy of an instance of your app running on a device.
///
/// - Parameters:
/// - keyIdentifier: The identifier you received when generating a cryptographic key by calling the generateKey(completionHandler:) method.
/// - clientDataHash: A SHA256 hash of a unique, single-use data block that represents the client data to be signed with the attested private key.
/// - Throws: `DCError`
/// - Returns: A data structure that you send to your server for processing OR A DCError instance that indicates the reason for failure, or nil on success.
func generateAssertion(keyIdentifier: String, clientDataHash: Data) async throws -> Data
/// Not all device types support the App Attest service, so check for support
/// before using the service.
/// - Returns: A Boolean value that indicates whether a particular device provides the App Attest service.
func isSupported() -> Bool
}

/// Attestation service wrapper directly communicates to DeviceCheck server
@available(iOS 14.0, *)
struct FRAppAttestServiceImpl: FRAppAttestService {

private let dcAppAttestService: DCAppAttestService

/// The service that you use to validate the instance of your app running on a device.
///
/// - Parameters:
/// - service: The shared App Attest service that you use to validate your app.
init(service: DCAppAttestService = DCAppAttestService.shared) {
self.dcAppAttestService = service
}

/// Not all device types support the App Attest service, so check for support
/// before using the service.
/// - Returns: A Boolean value that indicates whether a particular device provides the App Attest service.
func isSupported() -> Bool {
return dcAppAttestService.isSupported
}

/// Creates a new cryptographic key for use with the App Attest service.
/// - Returns: KeyId An identifier that you use to refer to the key. The framework securely
/// stores the key in the Secure Enclave.
/// - Throws: `DCError`
func generateKey() async throws -> String {
return try await dcAppAttestService.generateKey()
}

/// Asks Apple to attest to the validity of a generated cryptographic key.
///
/// - Parameters:
/// - keyIdentifier: The identifier you received when generating a cryptographic key by calling the generateKey(completionHandler:) method.
/// - clientDataHash: A SHA256 hash of a unique, single-use data block that embeds a challenge from your server.
/// - Throws: `DCError`
/// - Returns: A statement from Apple about the validity of the key associated with keyId. Send this to your server for processing OR DCError instance that indicates the reason for failure, or nil on success.
func attest(keyIdentifier: String, clientDataHash: Data) async throws -> Data {
return try await dcAppAttestService.attestKey(keyIdentifier, clientDataHash: clientDataHash)
}

/// Creates a block of data that demonstrates the legitimacy of an instance of your app running on a device.
///
/// - Parameters:
/// - keyIdentifier: The identifier you received when generating a cryptographic key by calling the generateKey(completionHandler:) method.
/// - clientDataHash: A SHA256 hash of a unique, single-use data block that represents the client data to be signed with the attested private key.
/// - Throws: `DCError`
/// - Returns: A data structure that you send to your server for processing OR A DCError instance that indicates the reason for failure, or nil on success.
func generateAssertion(keyIdentifier: String, clientDataHash: Data) async throws -> Data {
return try await dcAppAttestService.generateAssertion(keyIdentifier, clientDataHash: clientDataHash)
}
}


Loading

0 comments on commit 4218690

Please sign in to comment.