@@ -2,6 +2,7 @@ import CryptoKit
22import DeviceCheck
33import Foundation
44import React
5+ import Security
56
67/// Native bridge for iOS App Attest (app-level attestation).
78///
@@ -16,6 +17,48 @@ class EdgeAttestation: NSObject {
1617 return false
1718 }
1819
20+ // Keychain persistence for the attested App Attest key id. App Attest private
21+ // keys live in the Secure Enclave keyed by this id; Apple recommends storing
22+ // the id in the Keychain so it survives across launches and is reused for
23+ // assertions (no re-attestation).
24+ private static let keychainService = " co.edgesecure.app.appattest "
25+ private static let keychainAccount = " keyId "
26+
27+ private func storeKeyId( _ keyId: String ) {
28+ clearKeyId ( )
29+ guard let data = keyId. data ( using: . utf8) else { return }
30+ let query : [ String : Any ] = [
31+ kSecClass as String : kSecClassGenericPassword,
32+ kSecAttrService as String : EdgeAttestation . keychainService,
33+ kSecAttrAccount as String : EdgeAttestation . keychainAccount,
34+ kSecValueData as String : data
35+ ]
36+ SecItemAdd ( query as CFDictionary , nil )
37+ }
38+
39+ private func loadKeyId( ) -> String ? {
40+ let query : [ String : Any ] = [
41+ kSecClass as String : kSecClassGenericPassword,
42+ kSecAttrService as String : EdgeAttestation . keychainService,
43+ kSecAttrAccount as String : EdgeAttestation . keychainAccount,
44+ kSecReturnData as String : true ,
45+ kSecMatchLimit as String : kSecMatchLimitOne
46+ ]
47+ var item : CFTypeRef ?
48+ let status = SecItemCopyMatching ( query as CFDictionary , & item)
49+ guard status == errSecSuccess, let data = item as? Data else { return nil }
50+ return String ( data: data, encoding: . utf8)
51+ }
52+
53+ private func clearKeyId( ) {
54+ let query : [ String : Any ] = [
55+ kSecClass as String : kSecClassGenericPassword,
56+ kSecAttrService as String : EdgeAttestation . keychainService,
57+ kSecAttrAccount as String : EdgeAttestation . keychainAccount
58+ ]
59+ SecItemDelete ( query as CFDictionary )
60+ }
61+
1962 @objc ( isSupported: rejecter: )
2063 func isSupported(
2164 _ resolve: @escaping RCTPromiseResolveBlock ,
@@ -69,6 +112,9 @@ class EdgeAttestation: NSObject {
69112 reject ( " attestKey " , " Failed to produce an attestation object " , nil )
70113 return
71114 }
115+ // Persist the key id so subsequent handshakes refresh via assertions
116+ // instead of a full (rate-limited) attestation.
117+ self . storeKeyId ( keyId)
72118 resolve ( [
73119 " keyId " : keyId,
74120 " attestation " : attestation. base64EncodedString ( ) ,
@@ -77,4 +123,51 @@ class EdgeAttestation: NSObject {
77123 }
78124 }
79125 }
126+
127+ @objc ( generateAssertion: resolver: rejecter: )
128+ func generateAssertion(
129+ _ challenge: String ,
130+ resolver resolve: @escaping RCTPromiseResolveBlock ,
131+ rejecter reject: @escaping RCTPromiseRejectBlock
132+ ) {
133+ guard #available( iOS 14 . 0 , * ) , DCAppAttestService . shared. isSupported else {
134+ reject ( " unsupported " , " App Attest is not supported on this device " , nil )
135+ return
136+ }
137+ guard let keyId = loadKeyId ( ) else {
138+ reject ( " noKey " , " No attested App Attest key is stored " , nil )
139+ return
140+ }
141+ let clientDataHash = Data ( SHA256 . hash ( data: Data ( challenge. utf8) ) )
142+ DCAppAttestService . shared. generateAssertion ( keyId, clientDataHash: clientDataHash) { assertion, error in
143+ if let error = error as? DCError , error. code == . invalidKey {
144+ // The key no longer exists (reinstall/restore); force re-attestation.
145+ self . clearKeyId ( )
146+ reject ( " invalidKey " , " Stored App Attest key is invalid " , error)
147+ return
148+ }
149+ if let error = error {
150+ reject ( " generateAssertion " , error. localizedDescription, error)
151+ return
152+ }
153+ guard let assertion = assertion else {
154+ reject ( " generateAssertion " , " Failed to produce an assertion " , nil )
155+ return
156+ }
157+ resolve ( [
158+ " keyId " : keyId,
159+ " assertion " : assertion. base64EncodedString ( ) ,
160+ " bundleId " : Bundle . main. bundleIdentifier ?? " "
161+ ] )
162+ }
163+ }
164+
165+ @objc ( clearKey: rejecter: )
166+ func clearKey(
167+ _ resolve: @escaping RCTPromiseResolveBlock ,
168+ rejecter reject: @escaping RCTPromiseRejectBlock
169+ ) {
170+ clearKeyId ( )
171+ resolve ( nil )
172+ }
80173}
0 commit comments