@@ -17,6 +17,17 @@ class EdgeAttestation: NSObject {
1717 return false
1818 }
1919
20+ // Serializes all key operations. The JS engine normally single-flights
21+ // handshakes, but its 90s watchdog can release the lock and start a second
22+ // handshake while an older native call is still running. Without this queue,
23+ // overlapping getAttestation / generateAssertion / clearKey calls could race
24+ // on the stored key id and leave assertions out of sync with the cached JWT.
25+ // Each async App Attest operation holds the queue (via a semaphore) until it
26+ // completes, so the operations never interleave.
27+ private static let serialQueue = DispatchQueue (
28+ label: " co.edgesecure.app.appattest.serial "
29+ )
30+
2031 // Keychain persistence for the attested App Attest key id. App Attest private
2132 // keys live in the Secure Enclave keyed by this id; Apple recommends storing
2233 // the id in the Keychain so it survives across launches and is reused for
@@ -87,40 +98,51 @@ class EdgeAttestation: NSObject {
8798 return
8899 }
89100
90- // A fresh key is generated per handshake: an App Attest key can only be
91- // attested once, so reuse would require the assertion flow instead.
92- service. generateKey { keyId, error in
93- if let error = error {
94- reject ( " generateKey " , error. localizedDescription, error)
95- return
96- }
97- guard let keyId = keyId else {
98- reject ( " generateKey " , " Failed to generate an App Attest key " , nil )
99- return
100- }
101+ // Serialize against other key operations; hold the queue until the async
102+ // attest completes.
103+ EdgeAttestation . serialQueue. async {
104+ let done = DispatchSemaphore ( value: 0 )
101105
102- // The client data is the challenge's UTF-8 bytes; the server recomputes
103- // SHA256(challenge) to validate the attestation nonce.
104- let clientDataHash = Data ( SHA256 . hash ( data: Data ( challenge. utf8) ) )
105-
106- service. attestKey ( keyId, clientDataHash: clientDataHash) { attestation, error in
106+ // A fresh key is generated per handshake: an App Attest key can only be
107+ // attested once, so reuse would require the assertion flow instead.
108+ service. generateKey { keyId, error in
107109 if let error = error {
108- reject ( " attestKey " , error. localizedDescription, error)
110+ reject ( " generateKey " , error. localizedDescription, error)
111+ done. signal ( )
109112 return
110113 }
111- guard let attestation = attestation else {
112- reject ( " attestKey " , " Failed to produce an attestation object " , nil )
114+ guard let keyId = keyId else {
115+ reject ( " generateKey " , " Failed to generate an App Attest key " , nil )
116+ done. signal ( )
113117 return
114118 }
115- // Persist the key id so subsequent handshakes refresh via assertions
116- // instead of a full (rate-limited) attestation.
117- self . storeKeyId ( keyId)
118- resolve ( [
119- " keyId " : keyId,
120- " attestation " : attestation. base64EncodedString ( ) ,
121- " bundleId " : Bundle . main. bundleIdentifier ?? " "
122- ] )
119+
120+ // The client data is the challenge's UTF-8 bytes; the server recomputes
121+ // SHA256(challenge) to validate the attestation nonce.
122+ let clientDataHash = Data ( SHA256 . hash ( data: Data ( challenge. utf8) ) )
123+
124+ service. attestKey ( keyId, clientDataHash: clientDataHash) { attestation, error in
125+ defer { done. signal ( ) }
126+ if let error = error {
127+ reject ( " attestKey " , error. localizedDescription, error)
128+ return
129+ }
130+ guard let attestation = attestation else {
131+ reject ( " attestKey " , " Failed to produce an attestation object " , nil )
132+ return
133+ }
134+ // Persist the key id so subsequent handshakes refresh via assertions
135+ // instead of a full (rate-limited) attestation.
136+ self . storeKeyId ( keyId)
137+ resolve ( [
138+ " keyId " : keyId,
139+ " attestation " : attestation. base64EncodedString ( ) ,
140+ " bundleId " : Bundle . main. bundleIdentifier ?? " "
141+ ] )
142+ }
123143 }
144+
145+ done. wait ( )
124146 }
125147 }
126148
@@ -134,31 +156,37 @@ class EdgeAttestation: NSObject {
134156 reject ( " unsupported " , " App Attest is not supported on this device " , nil )
135157 return
136158 }
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)
159+
160+ EdgeAttestation . serialQueue. async {
161+ guard let keyId = self . loadKeyId ( ) else {
162+ reject ( " noKey " , " No attested App Attest key is stored " , nil )
151163 return
152164 }
153- guard let assertion = assertion else {
154- reject ( " generateAssertion " , " Failed to produce an assertion " , nil )
155- return
165+ let clientDataHash = Data ( SHA256 . hash ( data: Data ( challenge. utf8) ) )
166+ let done = DispatchSemaphore ( value: 0 )
167+ DCAppAttestService . shared. generateAssertion ( keyId, clientDataHash: clientDataHash) { assertion, error in
168+ defer { done. signal ( ) }
169+ if let error = error as? DCError , error. code == . invalidKey {
170+ // The key no longer exists (reinstall/restore); force re-attestation.
171+ self . clearKeyId ( )
172+ reject ( " invalidKey " , " Stored App Attest key is invalid " , error)
173+ return
174+ }
175+ if let error = error {
176+ reject ( " generateAssertion " , error. localizedDescription, error)
177+ return
178+ }
179+ guard let assertion = assertion else {
180+ reject ( " generateAssertion " , " Failed to produce an assertion " , nil )
181+ return
182+ }
183+ resolve ( [
184+ " keyId " : keyId,
185+ " assertion " : assertion. base64EncodedString ( ) ,
186+ " bundleId " : Bundle . main. bundleIdentifier ?? " "
187+ ] )
156188 }
157- resolve ( [
158- " keyId " : keyId,
159- " assertion " : assertion. base64EncodedString ( ) ,
160- " bundleId " : Bundle . main. bundleIdentifier ?? " "
161- ] )
189+ done. wait ( )
162190 }
163191 }
164192
@@ -167,7 +195,9 @@ class EdgeAttestation: NSObject {
167195 _ resolve: @escaping RCTPromiseResolveBlock ,
168196 rejecter reject: @escaping RCTPromiseRejectBlock
169197 ) {
170- clearKeyId ( )
171- resolve ( nil )
198+ EdgeAttestation . serialQueue. async {
199+ self . clearKeyId ( )
200+ resolve ( nil )
201+ }
172202 }
173203}
0 commit comments