From f3709f1bb83bf102a45c174d45cad5edbb62120f Mon Sep 17 00:00:00 2001 From: jey Date: Fri, 13 Oct 2023 14:55:37 -0500 Subject: [PATCH] appintegrity keymanagement --- FRAuth/FRAuth.xcodeproj/project.pbxproj | 8 + .../AppIntegrity/FRAppAttestDomainModal.swift | 139 ++++++++++++++---- .../AppIntegrity/FRAppIntegrityKeys.swift | 49 ++++++ .../Callbacks/FRAppIntegrityCallback.swift | 65 +++++--- .../FRAuth/Constants/CallbackConstants.swift | 2 +- .../AA_07_AppIntegrityTest.swift | 4 +- .../FRAppAttestDomainModalTests.swift | 131 +++++++++++++++-- .../FRAppIntegrityKeysTests.swift | 47 ++++++ .../FRAppIntegrityCallbackTests.swift | 41 +++++- .../AuthStepViewController.swift | 8 +- 10 files changed, 420 insertions(+), 74 deletions(-) create mode 100644 FRAuth/FRAuth/AppIntegrity/FRAppIntegrityKeys.swift create mode 100644 FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/AppIntegrity/FRAppIntegrityKeysTests.swift diff --git a/FRAuth/FRAuth.xcodeproj/project.pbxproj b/FRAuth/FRAuth.xcodeproj/project.pbxproj index 40287b64..b0887437 100644 --- a/FRAuth/FRAuth.xcodeproj/project.pbxproj +++ b/FRAuth/FRAuth.xcodeproj/project.pbxproj @@ -17,7 +17,9 @@ 3A4B46DE2AB38BA6009E7171 /* FRAppIntegrityCallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4B46DD2AB38BA6009E7171 /* FRAppIntegrityCallbackTests.swift */; }; 3A4B46E42AB95B3D009E7171 /* AA_07_AppIntegrityTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4B46E32AB95B3D009E7171 /* AA_07_AppIntegrityTest.swift */; }; 3A53E4DA2A153AF200E17DDF /* PolicyAdviceCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A53E4D92A153AF200E17DDF /* PolicyAdviceCreatorTests.swift */; }; + 3A67B8832AD83946003331C5 /* FRAppIntegrityKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A67B8822AD83946003331C5 /* FRAppIntegrityKeys.swift */; }; 3A6D26672A1345400099D877 /* PolicyAdviceCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D26662A1345400099D877 /* PolicyAdviceCreator.swift */; }; + 3AB062FA2AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB062F92AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift */; }; 959D7D98290B4B9200A1F22F /* AA-05-DeviceBindingCallbackTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959D7D97290B4B9200A1F22F /* AA-05-DeviceBindingCallbackTest.swift */; }; 95E180B42992A6F20087457D /* AA-06-DeviceSigningVerifierCallbackTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E180B32992A6F20087457D /* AA-06-DeviceSigningVerifierCallbackTest.swift */; }; A5950A2A27EA205B00EDEFE4 /* SSLPinningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5950A2927EA205B00EDEFE4 /* SSLPinningTests.swift */; }; @@ -340,7 +342,9 @@ 3A4B46DD2AB38BA6009E7171 /* FRAppIntegrityCallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRAppIntegrityCallbackTests.swift; sourceTree = ""; }; 3A4B46E32AB95B3D009E7171 /* AA_07_AppIntegrityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AA_07_AppIntegrityTest.swift; sourceTree = ""; }; 3A53E4D92A153AF200E17DDF /* PolicyAdviceCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyAdviceCreatorTests.swift; sourceTree = ""; }; + 3A67B8822AD83946003331C5 /* FRAppIntegrityKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRAppIntegrityKeys.swift; sourceTree = ""; }; 3A6D26662A1345400099D877 /* PolicyAdviceCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyAdviceCreator.swift; sourceTree = ""; }; + 3AB062F92AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRAppIntegrityKeysTests.swift; sourceTree = ""; }; 959D7D97290B4B9200A1F22F /* AA-05-DeviceBindingCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AA-05-DeviceBindingCallbackTest.swift"; sourceTree = ""; }; 95E180B32992A6F20087457D /* AA-06-DeviceSigningVerifierCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AA-06-DeviceSigningVerifierCallbackTest.swift"; sourceTree = ""; }; A5950A2927EA205B00EDEFE4 /* SSLPinningTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLPinningTests.swift; sourceTree = ""; }; @@ -638,6 +642,7 @@ children = ( 3A3E78622AAF764F007962B7 /* FRAppAttestDomainModal.swift */, 3A3E78642AAF9919007962B7 /* FRAppAttestServiceImpl.swift */, + 3A67B8822AD83946003331C5 /* FRAppIntegrityKeys.swift */, ); path = AppIntegrity; sourceTree = ""; @@ -647,6 +652,7 @@ children = ( 3A3E78672AB0D642007962B7 /* FRAppAttestDomainModalTests.swift */, 3A4B46DB2AB2D149009E7171 /* FRAppAttestServiceImplTests.swift */, + 3AB062F92AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift */, ); path = AppIntegrity; sourceTree = ""; @@ -1784,6 +1790,7 @@ D586CF9623358EE0007A2194 /* PollingWaitCallback.swift in Sources */, D53A8056262789E40093B1CA /* WebAuthnAuthenticationCallback.swift in Sources */, D5B2A37E23FC951700764370 /* HiddenValueCallback.swift in Sources */, + 3A67B8832AD83946003331C5 /* FRAppIntegrityKeys.swift in Sources */, D53A8040262789BD0093B1CA /* PlatformAuthenticatorGetAssertionSession.swift in Sources */, D5B2A38923FCB25E00764370 /* ActionCallback.swift in Sources */, D5B2061425FFE0E500DABB9B /* SelectIdPCallback.swift in Sources */, @@ -1909,6 +1916,7 @@ D5791BC625F87DE8004B487A /* ConfirmationCallbackTests.swift in Sources */, D5791BC725F87DE8004B487A /* DeviceProfileCallbackTests.swift in Sources */, D53A806F26278A5C0093B1CA /* WebAuthnRegistrationCallbackTests.swift in Sources */, + 3AB062FA2AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift in Sources */, D5791BC825F87DE8004B487A /* MetadataCallbackTests.swift in Sources */, D5791BC925F87DE8004B487A /* FailedPolicyTests.swift in Sources */, D5791BCA25F87DE8004B487A /* BooleanAttributeInputCallbackTests.swift in Sources */, diff --git a/FRAuth/FRAuth/AppIntegrity/FRAppAttestDomainModal.swift b/FRAuth/FRAuth/AppIntegrity/FRAppAttestDomainModal.swift index a6827c4c..874c9a52 100644 --- a/FRAuth/FRAuth/AppIntegrity/FRAppAttestDomainModal.swift +++ b/FRAuth/FRAuth/AppIntegrity/FRAppAttestDomainModal.swift @@ -20,6 +20,7 @@ public protocol FRAppAttestation { /// - Throws: `FRDeviceCheckAPIFailure and Error` /// - Returns: FRAppIntegrityKeys for attestation and assertion func attest(challenge: String) async throws -> FRAppIntegrityKeys + } /// Attestation modal to fetch the result for the given callback @@ -29,8 +30,10 @@ 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 = { @@ -46,11 +49,13 @@ struct FRAppAttestDomainModal: FRAppAttestation { /// - 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 @@ -58,12 +63,52 @@ struct FRAppAttestDomainModal: FRAppAttestation { /// - Throws: `FRDeviceCheckAPIFailure and Error` /// - Returns: FRAppIntegrityKeys for attestation and assertion func attest(challenge: String) async throws -> FRAppIntegrityKeys { - guard let challengeUtf8 = challenge.data(using: .utf8), !challengeUtf8.isEmpty else { - throw FRDeviceCheckAPIFailure.invalidChallenge + 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] @@ -71,27 +116,76 @@ struct FRAppAttestDomainModal: FRAppAttestation { throw FRDeviceCheckAPIFailure.invalidClientData } - if !service.isSupported() { - throw FRDeviceCheckAPIFailure.featureUnsupported - } + 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 keyIdentifier = try await service.generateKey() - let attest = try await service.attest(keyIdentifier: keyIdentifier, clientDataHash: Data(SHA256.hash(data: challengeUtf8))) - let assert = try await service.generateAssertion(keyIdentifier: keyIdentifier, clientDataHash: Data(SHA256.hash(data: jsonData))) - return FRAppIntegrityKeys(attestKey: attest.base64EncodedString(), - assertKey: assert.base64EncodedString(), - keyIdentifier: keyIdentifier, + 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) } - catch let error as DCError { - throw FRDeviceCheckAPIFailure.error(code: error.errorCode) - } - catch { - throw error - } + } + /// withRetry + /// + /// - Parameter maxRetries: Challenge Received from server + /// - Parameter operation: execute the operation + /// - Throws: `Error` + /// - Returns: T genric operation + private func withRetry(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 @@ -106,6 +200,7 @@ public enum FRDeviceCheckAPIFailure: String, Error { case invalidBundleIdentifier case invalidClientData case unknownError + case keyChainError var clientError: String { switch self { @@ -139,13 +234,3 @@ public enum FRAppIntegrityClientError: String { case unSupported = "Unsupported" case clientDeviceErrors = "ClientDeviceErrors" } - -/// Result of Attestaion and Assertion keys that needs to send to server -@available(iOS 14.0, *) -public struct FRAppIntegrityKeys { - let attestKey: String - let assertKey: String - let keyIdentifier: String - let clientDataHash: String -} - diff --git a/FRAuth/FRAuth/AppIntegrity/FRAppIntegrityKeys.swift b/FRAuth/FRAuth/AppIntegrity/FRAppIntegrityKeys.swift new file mode 100644 index 00000000..38ba3899 --- /dev/null +++ b/FRAuth/FRAuth/AppIntegrity/FRAppIntegrityKeys.swift @@ -0,0 +1,49 @@ +// +// FRAppAuthKeys.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 + +public struct FRAppIntegrityKeys { + + public private(set) var appAttestKey: String + public private(set) var assertKey: String? = nil + public private(set) var keyIdentifier: String + public private(set) var clientDataHash: String + private let key = "com.forgerock.ios.appattest.keychainservice" + private let keychain: KeychainManager? = FRAuth.shared?.keychainManager + + public init(attestKey: String = String(), + assertKey: String? = nil, + keyIdentifier: String = String(), + clientDataHash: String = String()) { + self.appAttestKey = attestKey + self.assertKey = assertKey + self.keyIdentifier = keyIdentifier + self.clientDataHash = clientDataHash + } + + internal func updateKey(value: String) { + self.keychain?.privateStore.set(value, key: key) + } + + internal func getKey() -> String? { + return self.keychain?.privateStore.getString(key) + } + + public func isAttestationCompleted() -> Bool { + return self.getKey() != nil + } + + public func deleteKey() { + self.keychain?.privateStore.delete(key) + } + +} diff --git a/FRAuth/FRAuth/Callbacks/FRAppIntegrityCallback.swift b/FRAuth/FRAuth/Callbacks/FRAppIntegrityCallback.swift index 35e7068d..7c505d16 100644 --- a/FRAuth/FRAuth/Callbacks/FRAppIntegrityCallback.swift +++ b/FRAuth/FRAuth/Callbacks/FRAppIntegrityCallback.swift @@ -15,20 +15,25 @@ public class FRAppIntegrityCallback: MultipleValuesCallback { /// The Challenge received from server in Output key public private(set) var challenge: String - /// Attestation token input key + /// The attest token received from server in Output key public private(set) var attestToken: String + /// Attestation token input key + private var attestTokenKey: String + + /// Assertion token input key + private var tokenKey: String + /// Client Error input key - public private(set) var clientErrorKey: String + private var clientErrorKey: String /// Key Identifier input key - public private(set) var keyId: String + private var keyIdKey: String /// Client Data input key - public private(set) var clientData: String + private var clientDataKey: String - /// Assertion token input key - public private(set) var assertToken: String + public private(set) var appIntegritykeys: FRAppIntegrityKeys = FRAppIntegrityKeys() // MARK: - Init @@ -60,6 +65,17 @@ public class FRAppIntegrityCallback: MultipleValuesCallback { } self.challenge = challenge + guard let outputToken = outputDictionary[CBConstants.attest] as? String else { + throw AuthError.invalidCallbackResponse("Missing Token") + } + + self.attestToken = outputToken + + if !outputToken.isEmpty { + FRLog.e("Persist the attestation reference") + appIntegritykeys.updateKey(value: outputToken) + } + //parse inputs var inputNames = [String]() for input in inputs { @@ -69,10 +85,10 @@ public class FRAppIntegrityCallback: MultipleValuesCallback { inputNames.append(inputName) } - guard let attestKey = inputNames.filter({ $0.contains(CBConstants.token) }).first else { + guard let attestKey = inputNames.filter({ $0.contains(CBConstants.attest) }).first else { throw AuthError.invalidCallbackResponse("Missing deviceIdKey") } - self.attestToken = attestKey + self.attestTokenKey = attestKey guard let clientErrorKey = inputNames.filter({ $0.contains(CBConstants.clientError) }).first else { throw AuthError.invalidCallbackResponse("Missing clientErrorKey") @@ -82,17 +98,17 @@ public class FRAppIntegrityCallback: MultipleValuesCallback { guard let keyId = inputNames.filter({ $0.contains(CBConstants.keyId) }).first else { throw AuthError.invalidCallbackResponse("Missing keyId") } - self.keyId = keyId + self.keyIdKey = keyId - guard let assertKey = inputNames.filter({ $0.contains(CBConstants.assert) }).first else { + guard let assertKey = inputNames.filter({ $0.contains(CBConstants.token) }).first else { throw AuthError.invalidCallbackResponse("Missing appVerification") } - self.assertToken = assertKey + self.tokenKey = assertKey guard let clientData = inputNames.filter({ $0.contains(CBConstants.clientData) }).first else { throw AuthError.invalidCallbackResponse("Missing clientData") } - self.clientData = clientData + self.clientDataKey = clientData try super.init(json: json) type = callbackType @@ -105,7 +121,7 @@ public class FRAppIntegrityCallback: MultipleValuesCallback { /// Sets `token` value in callback response /// - Parameter token: Base64 String value of attestation public func setAttestation(_ token: String) { - self.inputValues[self.attestToken] = token + self.inputValues[self.attestTokenKey] = token } /// Sets `error` value in callback response @@ -117,32 +133,33 @@ public class FRAppIntegrityCallback: MultipleValuesCallback { /// Sets `keyId` value in callback response /// - Parameter keyId: Base64 String value of keyId public func setkeyId(_ keyId: String) { - self.inputValues[self.keyId] = keyId + self.inputValues[self.keyIdKey] = keyId } /// Sets `token` value in callback response /// - Parameter token: Base64 String value of verification - public func setVerification(_ token: String) { - self.inputValues[self.assertToken] = token + public func setAssertion(_ token: String) { + self.inputValues[self.tokenKey] = token } /// Sets `clientData` value in callback response /// - Parameter clientData: Base64 String value of clientData public func setClientData(_ clientData: String) { - self.inputValues[self.clientData] = clientData + self.inputValues[self.clientDataKey] = clientData } /// Attest the device for iOS14 and above devices /// - Throws: `FRDeviceCheckAPIFailure` /// - Parameter attestation: Optional Protocol for providing a ``FRAppAttestation`` to implement own attestation @available(iOS 14.0, *) - public func attest() async throws { + public func requestIntegrityToken() async throws { do { let result = try await FRAppAttestDomainModal.shared.attest(challenge: challenge) - self.setAttestation(result.attestKey) - self.setVerification(result.assertKey) + self.setAttestation(result.appAttestKey) + self.setAssertion(result.assertKey ?? "") self.setkeyId(result.keyIdentifier) self.setClientData(result.clientDataHash) + self.appIntegritykeys = result } catch { FRLog.e("Error: \(error.localizedDescription)") @@ -154,12 +171,12 @@ public class FRAppIntegrityCallback: MultipleValuesCallback { /// Attest the device /// - Parameter completionHandler: Returns FRAppIntegrityFailure for Error and nil if there are no errors - public func attest(completionHandler: @escaping (Error?) -> (Void)) { + public func requestIntegrityToken(completionHandler: @escaping (Error?) -> (Void)) { do { if #available(iOS 14.0, *) { Task { do { - try await attest() + try await requestIntegrityToken() completionHandler(nil) } catch { @@ -174,4 +191,8 @@ public class FRAppIntegrityCallback: MultipleValuesCallback { } } } + + public func isAttestationCompleted() -> Bool { + return appIntegritykeys.isAttestationCompleted() + } } diff --git a/FRAuth/FRAuth/Constants/CallbackConstants.swift b/FRAuth/FRAuth/Constants/CallbackConstants.swift index d4eb2204..e5cc2537 100644 --- a/FRAuth/FRAuth/Constants/CallbackConstants.swift +++ b/FRAuth/FRAuth/Constants/CallbackConstants.swift @@ -248,7 +248,7 @@ extension CBConstants { // MARK: - AppIntegrity extension CBConstants { - static let assert = "assert" + static let attest = "attestToken" static let keyId = "keyId" static let clientData = "clientData" } diff --git a/FRAuth/FRAuthTests/FRAuthSwiftTests/E2ETests/Callback-Live/AA_07_AppIntegrityTest.swift b/FRAuth/FRAuthTests/FRAuthSwiftTests/E2ETests/Callback-Live/AA_07_AppIntegrityTest.swift index 919f8c69..8ecd7e9c 100644 --- a/FRAuth/FRAuthTests/FRAuthSwiftTests/E2ETests/Callback-Live/AA_07_AppIntegrityTest.swift +++ b/FRAuth/FRAuthTests/FRAuthSwiftTests/E2ETests/Callback-Live/AA_07_AppIntegrityTest.swift @@ -70,7 +70,7 @@ final class AA_07_AppIntegrityTest: CallbackBaseTest { var bindingResult = "" let ex = self.expectation(description: "App Integrity") - integrityCallback.attest { error in + integrityCallback.requestIntegrityToken { error in bindingResult = (error == nil) ? "Success" : "failure" ex.fulfill() @@ -123,7 +123,7 @@ final class AA_07_AppIntegrityTest: CallbackBaseTest { var bindingResult = "" let ex = self.expectation(description: "App Integrity") - integrityCallback.attest { error in + integrityCallback.requestIntegrityToken { error in bindingResult = (error == nil) ? "Success" : error?.localizedDescription ?? "error" ex.fulfill() diff --git a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/AppIntegrity/FRAppAttestDomainModalTests.swift b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/AppIntegrity/FRAppAttestDomainModalTests.swift index 3da647fc..65410c33 100644 --- a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/AppIntegrity/FRAppAttestDomainModalTests.swift +++ b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/AppIntegrity/FRAppAttestDomainModalTests.swift @@ -11,26 +11,85 @@ import XCTest @testable import FRAuth +@testable import FRCore import DeviceCheck @available(iOS 14.0, *) -final class FRAppAttestModalTests: XCTestCase { +final class FRAppAttestModalTests: FRAuthBaseTest { + + private let keychain: KeychainManager? = FRAuth.shared?.keychainManager + private let keychainKey = "FRAppAttestKey" + + override func setUp() { + self.configFileName = "Config" + super.setUp() + self.startSDK() + } func testSuccessPath() async { let service = MockAppAttestService() let testObject = FRAppAttestDomainModal(service: service) + do { + let result = try await testObject.attest(challenge: "1234") + XCTAssertNil(result.assertKey) + XCTAssertNotNil(result.appAttestKey) + XCTAssertNotNil(result.keyIdentifier) + XCTAssertNotNil(result.clientDataHash) + XCTAssertFalse(service.assertKeyCalled) + XCTAssertTrue(service.attestKeyCalled) + XCTAssertTrue(service.generateKeyCalled) + XCTAssertEqual(service.assertKeyCalledTimes, 0) + XCTAssertEqual(service.attestKeyCalledTimes, 1) + XCTAssertEqual(service.geenrateKeyCalledTimes,1) + } catch { + XCTFail("AppAttestation failed") + } + } + + func testSuccessPathAssertion() async { + let service = MockAppAttestService() + let testObject = FRAppAttestDomainModal(service: service) + FRAppIntegrityKeys().updateKey(value: "keyid::attest") do { let result = try await testObject.attest(challenge: "1234") XCTAssertNotNil(result.assertKey) - XCTAssertNotNil(result.attestKey) + XCTAssertNotNil(result.appAttestKey) XCTAssertNotNil(result.keyIdentifier) XCTAssertNotNil(result.clientDataHash) XCTAssertTrue(service.assertKeyCalled) + XCTAssertFalse(service.attestKeyCalled) + XCTAssertFalse(service.generateKeyCalled) + XCTAssertEqual(service.assertKeyCalledTimes, 1) + XCTAssertEqual(service.attestKeyCalledTimes, 0) + XCTAssertEqual(service.geenrateKeyCalledTimes,0) + } catch { + XCTFail("AppAttestation failed") + } + } + + + func testInvalidAssertionRetryTwiceOnError() async { + FRAppIntegrityKeys().updateKey(value: "keyid::attest") + + let service = MockAppAttestService(supported: true, error: .invalidKey, assertKeyError: true) + let testObject = FRAppAttestDomainModal(service: service) + do { + let result = try await testObject.attest(challenge: "1234") + XCTAssertNil(result.assertKey) + XCTAssertNotNil(result.appAttestKey) + XCTAssertNotNil(result.keyIdentifier) + XCTAssertNotNil(result.clientDataHash) + XCTAssertTrue(service.assertKeyCalled) + XCTAssertEqual(service.assertKeyCalledTimes, 2) + XCTAssertEqual(service.attestKeyCalledTimes, 1) + XCTAssertEqual(service.geenrateKeyCalledTimes, 1) XCTAssertTrue(service.attestKeyCalled) XCTAssertTrue(service.generateKeyCalled) + } catch { XCTFail("AppAttestation failed") } + } func testInvalidChallenge() async { @@ -81,7 +140,7 @@ final class FRAppAttestModalTests: XCTestCase { } func testDCErrorInvalidInput() async { - let service = MockAppAttestService(supported: true, error: .invalidInput) + let service = MockAppAttestService(supported: true, error: .invalidInput, generateKeyError: true) let testObject = FRAppAttestDomainModal(service: service) do { _ = try await testObject.attest(challenge: "1234") @@ -92,7 +151,7 @@ final class FRAppAttestModalTests: XCTestCase { } func testDCErrorInvalidKey() async { - let service = MockAppAttestService(supported: true, error: .invalidKey) + let service = MockAppAttestService(supported: true, error: .invalidKey, generateKeyError: true) let testObject = FRAppAttestDomainModal(service: service) do { _ = try await testObject.attest(challenge: "1234") @@ -103,7 +162,7 @@ final class FRAppAttestModalTests: XCTestCase { } func testDCErrorServerUnavailable() async { - let service = MockAppAttestService(supported: true, error: .serverUnavailable) + let service = MockAppAttestService(supported: true, error: .serverUnavailable, generateKeyError: true) let testObject = FRAppAttestDomainModal(service: service) do { _ = try await testObject.attest(challenge: "1234") @@ -113,8 +172,20 @@ final class FRAppAttestModalTests: XCTestCase { } } + func testDCErrorServerUnavailableAttestation() async { + let service = MockAppAttestService(supported: true, error: .serverUnavailable, attestKeyError: true) + let testObject = FRAppAttestDomainModal(service: service) + do { + _ = try await testObject.attest(challenge: "1234") + XCTFail("AppAttestation failed") + } catch { + XCTAssertTrue(error.localizedDescription == FRDeviceCheckAPIFailure.serverUnavailable.localizedDescription) + } + } + + func testDCErrorUnknownSystemFailure() async { - let service = MockAppAttestService(supported: true, error: .unknownSystemFailure) + let service = MockAppAttestService(supported: true, error: .unknownSystemFailure, generateKeyError: true) let testObject = FRAppAttestDomainModal(service: service) do { _ = try await testObject.attest(challenge: "1234") @@ -125,7 +196,7 @@ final class FRAppAttestModalTests: XCTestCase { } func testUnknownError() async { - let service: FRAppAttestService = MockAppAttestService(supported: true, unknownError: NSError(domain: "unknown", code: 100)) + let service: FRAppAttestService = MockAppAttestService(supported: true, unknownError: NSError(domain: "unknown", code: 100), generateKeyError: true) let testObject = FRAppAttestDomainModal(service: service) do { _ = try await testObject.attest(challenge: "1234") @@ -169,33 +240,67 @@ class MockAppAttestService: FRAppAttestService { var supported = true var dcError: DCError.Code? = nil var unknownError: Error? = nil + var assertKeyCalledTimes = 0 + var geenrateKeyCalledTimes = 0 + var attestKeyCalledTimes = 0 + + var generateKeyError = false + var attestKeyError = false + var assertKeyError = false init(supported: Bool = true, error: DCError.Code? = nil, - unknownError: Error? = nil) { + unknownError: Error? = nil, + generateKeyError: Bool = false, + attestKeyError: Bool = false, + assertKeyError: Bool = false) { self.supported = supported self.dcError = error self.unknownError = unknownError + self.generateKeyError = generateKeyError + self.attestKeyError = attestKeyError + self.assertKeyError = assertKeyError } func generateKey() async throws -> String { generateKeyCalled = true - if let error: DCError.Code = self.dcError { - throw DCError.init(error) - } - if let error = self.unknownError { - throw error + geenrateKeyCalledTimes += 1 + if self.generateKeyError { + if let error: DCError.Code = self.dcError { + throw DCError.init(error) + } + if let error = self.unknownError { + throw error + } } return "key" } func attest(keyIdentifier: String, clientDataHash: Data) async throws -> Data { attestKeyCalled = true + attestKeyCalledTimes += 1 + if self.attestKeyError { + if let error: DCError.Code = self.dcError { + throw DCError.init(error) + } + if let error = self.unknownError { + throw error + } + } return "attestKey".data(using: .utf8)! } func generateAssertion(keyIdentifier: String, clientDataHash: Data) async throws -> Data { + assertKeyCalledTimes += 1 assertKeyCalled = true + if self.assertKeyError { + if let error: DCError.Code = self.dcError { + throw DCError.init(error) + } + if let error = self.unknownError { + throw error + } + } return "assertkey".data(using: .utf8)! } diff --git a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/AppIntegrity/FRAppIntegrityKeysTests.swift b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/AppIntegrity/FRAppIntegrityKeysTests.swift new file mode 100644 index 00000000..a9aadf50 --- /dev/null +++ b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/AppIntegrity/FRAppIntegrityKeysTests.swift @@ -0,0 +1,47 @@ +// +// FRAppIntegrityKeys.swift +// FRAuthTests +// +// 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 XCTest +@testable import FRAuth +@testable import FRCore + +final class FRAppIntegrityKeysTests: FRAuthBaseTest { + + private let keychain: KeychainManager? = FRAuth.shared?.keychainManager + private let keychainKey = "FRAppAttestKey" + + override func setUp() { + self.configFileName = "Config" + super.setUp() + self.startSDK() + } + + + func testKeys() { + let testObject = FRAppIntegrityKeys(attestKey: "attest", assertKey: "assert", keyIdentifier: "keyid", clientDataHash: "hash") + XCTAssertEqual(testObject.appAttestKey, "attest") + XCTAssertEqual(testObject.assertKey, "assert") + XCTAssertEqual(testObject.keyIdentifier, "keyid") + XCTAssertEqual(testObject.clientDataHash, "hash") + } + + func testKeyChainKeys() { + let testObject = FRAppIntegrityKeys(attestKey: "attest", assertKey: "assert", keyIdentifier: "keyid", clientDataHash: "hash") + + testObject.updateKey(value: "keyid::attest") + XCTAssertEqual(testObject.getKey(), "keyid::attest") + XCTAssertEqual(testObject.isAttestationCompleted(), true) + testObject.deleteKey() + XCTAssertEqual(testObject.getKey(), nil) + XCTAssertEqual(testObject.isAttestationCompleted(), false) + } + +} diff --git a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/Callback/FRAppIntegrityCallbackTests.swift b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/Callback/FRAppIntegrityCallbackTests.swift index a4ab28de..d2c30a10 100644 --- a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/Callback/FRAppIntegrityCallbackTests.swift +++ b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/Callback/FRAppIntegrityCallbackTests.swift @@ -14,9 +14,34 @@ import XCTest final class FRAppIntegrityCallbackTests: FRAuthBaseTest { + override func setUp() { + self.configFileName = "Config" + super.setUp() + self.startSDK() + } + func test_01_CallbackConstruction_Successful() throws { let jsonStr = """ - {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1assert","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} + {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="}, {"name":"attestToken","value":""}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1attestToken","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} + """ + + let callbackResponse = self.parseStringToDictionary(jsonStr) + + // Try + do { + let callback = try FRAppIntegrityCallback(json: callbackResponse) + + // Then + XCTAssertEqual(callback.challenge, "x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM=") + XCTAssertEqual(callback.type, "AppIntegrityCallback") + } catch let error { + XCTFail("Failed while expecting success: \(error)") + } + } + + func test_01_CallbackConstruction_Successful_Persist() throws { + let jsonStr = """ + {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="}, {"name":"attestToken","value":"keyid::appattest"}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1attestToken","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} """ let callbackResponse = self.parseStringToDictionary(jsonStr) @@ -24,10 +49,12 @@ final class FRAppIntegrityCallbackTests: FRAuthBaseTest { // Try do { let callback = try FRAppIntegrityCallback(json: callbackResponse) + let appIntegrityKeys = FRAppIntegrityKeys() // Then XCTAssertEqual(callback.challenge, "x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM=") XCTAssertEqual(callback.type, "AppIntegrityCallback") + XCTAssertEqual(appIntegrityKeys.getKey(), "keyid::appattest") } catch let error { XCTFail("Failed while expecting success: \(error)") } @@ -36,7 +63,7 @@ final class FRAppIntegrityCallbackTests: FRAuthBaseTest { @available(iOS 14.0, *) func test_02_InputConstruction_Successful() async throws { let jsonStr = """ - {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1assert","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} + {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="},{"name":"attestToken","value":""}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1attestToken","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} """ let callbackResponse = self.parseStringToDictionary(jsonStr) @@ -53,7 +80,7 @@ final class FRAppIntegrityCallbackTests: FRAuthBaseTest { XCTAssertEqual(callback.type, "AppIntegrityCallback") - try await callback.attest() + try await callback.requestIntegrityToken() let buildResponse = callback.buildResponse().description @@ -71,7 +98,7 @@ final class FRAppIntegrityCallbackTests: FRAuthBaseTest { @available(iOS 14.0, *) func test_03_InvalidClientData_Failure() async throws { let jsonStr = """ - {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1assert","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} + {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="}, {"name":"attestToken","value":""}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1attestToken","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} """ let callbackResponse = self.parseStringToDictionary(jsonStr) @@ -86,7 +113,7 @@ final class FRAppIntegrityCallbackTests: FRAuthBaseTest { XCTAssertEqual(callback?.challenge, "x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM=") XCTAssertEqual(callback?.type, "AppIntegrityCallback") - try await callback?.attest() + try await callback?.requestIntegrityToken() XCTFail("Failed while expecting success") @@ -103,7 +130,7 @@ final class FRAppIntegrityCallbackTests: FRAuthBaseTest { @available(iOS 14.0, *) func test_03_Unknown_Error() async throws { let jsonStr = """ - {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1assert","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} + {"type":"AppIntegrityCallback","output":[{"name":"challenge","value":"x2AMmYOIP7CFCkp0tbXkr69NDBaP1dUypxioQTbdnfM="}, {"name":"attestToken","value":""}],"input":[{"name":"IDToken1clientError","value":""},{"name":"IDToken1attestToken","value":""},{"name":"IDToken1token","value":""},{"name":"IDToken1clientData","value":""},{"name":"IDToken1keyId","value":""}]} """ let callbackResponse = self.parseStringToDictionary(jsonStr) @@ -118,7 +145,7 @@ final class FRAppIntegrityCallbackTests: FRAuthBaseTest { XCTAssertEqual(callback?.type, "AppIntegrityCallback") - try await callback?.attest() + try await callback?.requestIntegrityToken() XCTFail("Failed while expecting success") diff --git a/FRUI/FRUI/ViewControllers/AuthStepViewController.swift b/FRUI/FRUI/ViewControllers/AuthStepViewController.swift index f78ed561..6c3d1c8e 100644 --- a/FRUI/FRUI/ViewControllers/AuthStepViewController.swift +++ b/FRUI/FRUI/ViewControllers/AuthStepViewController.swift @@ -203,7 +203,7 @@ class AuthStepViewController: UIViewController { if #available(iOS 14.0, *) { Task { do { - try await appIntegrity.attest() + try await appIntegrity.requestIntegrityToken() alert.message = "Success" } catch let error { @@ -211,7 +211,11 @@ class AuthStepViewController: UIViewController { } self.stopLoading() await MainActor.run { - self.present(alert, animated: true) + if appIntegrity.isAttestationCompleted() { + self.present(alert, animated: true) + } else { + self.submitCurrentNode() + } } } } else {