Skip to content

Commit

Permalink
appintegrity keymanagement
Browse files Browse the repository at this point in the history
  • Loading branch information
jeyanthanperiyasamy committed Oct 23, 2023
1 parent 17f4e99 commit f3709f1
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 74 deletions.
8 changes: 8 additions & 0 deletions FRAuth/FRAuth.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -340,7 +342,9 @@
3A4B46DD2AB38BA6009E7171 /* FRAppIntegrityCallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRAppIntegrityCallbackTests.swift; sourceTree = "<group>"; };
3A4B46E32AB95B3D009E7171 /* AA_07_AppIntegrityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AA_07_AppIntegrityTest.swift; sourceTree = "<group>"; };
3A53E4D92A153AF200E17DDF /* PolicyAdviceCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyAdviceCreatorTests.swift; sourceTree = "<group>"; };
3A67B8822AD83946003331C5 /* FRAppIntegrityKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRAppIntegrityKeys.swift; sourceTree = "<group>"; };
3A6D26662A1345400099D877 /* PolicyAdviceCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyAdviceCreator.swift; sourceTree = "<group>"; };
3AB062F92AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRAppIntegrityKeysTests.swift; sourceTree = "<group>"; };
959D7D97290B4B9200A1F22F /* AA-05-DeviceBindingCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AA-05-DeviceBindingCallbackTest.swift"; sourceTree = "<group>"; };
95E180B32992A6F20087457D /* AA-06-DeviceSigningVerifierCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AA-06-DeviceSigningVerifierCallbackTest.swift"; sourceTree = "<group>"; };
A5950A2927EA205B00EDEFE4 /* SSLPinningTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLPinningTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -638,6 +642,7 @@
children = (
3A3E78622AAF764F007962B7 /* FRAppAttestDomainModal.swift */,
3A3E78642AAF9919007962B7 /* FRAppAttestServiceImpl.swift */,
3A67B8822AD83946003331C5 /* FRAppIntegrityKeys.swift */,
);
path = AppIntegrity;
sourceTree = "<group>";
Expand All @@ -647,6 +652,7 @@
children = (
3A3E78672AB0D642007962B7 /* FRAppAttestDomainModalTests.swift */,
3A4B46DB2AB2D149009E7171 /* FRAppAttestServiceImplTests.swift */,
3AB062F92AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift */,
);
path = AppIntegrity;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
139 changes: 112 additions & 27 deletions FRAuth/FRAuth/AppIntegrity/FRAppAttestDomainModal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -46,52 +49,143 @@ 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
/// - Parameter challenge: Challenge Received from server
/// - 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]

guard let jsonData = try? encoder.encode(userClientData) else {
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<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
Expand All @@ -106,6 +200,7 @@ public enum FRDeviceCheckAPIFailure: String, Error {
case invalidBundleIdentifier
case invalidClientData
case unknownError
case keyChainError

var clientError: String {
switch self {
Expand Down Expand Up @@ -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
}

49 changes: 49 additions & 0 deletions FRAuth/FRAuth/AppIntegrity/FRAppIntegrityKeys.swift
Original file line number Diff line number Diff line change
@@ -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)
}

}
Loading

0 comments on commit f3709f1

Please sign in to comment.