Skip to content

Commit fe7b709

Browse files
Updated client to properly call into registration attestation before finalizing the results and coordinating in-flight tasks
1 parent 6085c6c commit fe7b709

File tree

3 files changed

+58
-35
lines changed

3 files changed

+58
-35
lines changed

Sources/WebAuthn/Ceremonies/Registration/AttestationFormat.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
public enum AttestationFormat: String, RawRepresentable, Equatable {
15+
public enum AttestationFormat: String, RawRepresentable, Equatable, Sendable {
1616
case packed
1717
case tpm
1818
case androidKey = "android-key"

Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414

1515
import Foundation
1616
import Crypto
17-
import SwiftCBOR
17+
@preconcurrency import SwiftCBOR
1818

1919
/// Contains the cryptographic attestation that a new key pair was created by that authenticator.
20-
public struct AttestationObject {
20+
public struct AttestationObject: Sendable {
2121
let authenticatorData: AuthenticatorData
2222
let rawAuthenticatorData: [UInt8]
2323
let format: AttestationFormat

Sources/WebAuthn/WebAuthnClient.swift

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -147,40 +147,57 @@ public struct WebAuthnClient {
147147
// Skip.
148148

149149
/// Step 24. Start lifetimeTimer.
150-
// Skip.
150+
let timeoutTask = Task { try? await Task.sleep(for: timeout) }
151151

152152
/// Step 25. While lifetimeTimer has not expired, perform the following actions depending upon lifetimeTimer, and the state and response for each authenticator in authenticators:
153153
do {
154-
/// → If lifetimeTimer expires,
155-
/// For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests.
156-
/// → If the user exercises a user agent user-interface option to cancel the process,
157-
/// For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests. Throw a "NotAllowedError" DOMException.
158-
/// → If options.signal is present and aborted,
159-
/// For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests. Then throw the options.signal’s abort reason.
160-
/// → If an authenticator becomes available on this client device,
161-
/// See ``KeyPairAuthenticator/makeCredentials(with:)`` for full implementation
162-
/// → If an authenticator ceases to be available on this client device,
163-
/// Remove authenticator from issuedRequests.
164-
/// → If any authenticator returns a status indicating that the user cancelled the operation,
165-
/// 1. Remove authenticator from issuedRequests.
166-
/// 2. For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
167-
/// NOTE: Authenticators may return an indication of "the user cancelled the entire operation". How a user agent manifests this state to users is unspecified.
168-
/// → If any authenticator returns an error status equivalent to "InvalidStateError",
169-
/// 1. Remove authenticator from issuedRequests.
170-
/// 2. For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
171-
/// 3. Throw an "InvalidStateError" DOMException.
172-
/// NOTE: This error status is handled separately because the authenticator returns it only if excludeCredentialDescriptorList identifies a credential bound to the authenticator and the user has consented to the operation. Given this explicit consent, it is acceptable for this case to be distinguishable to the Relying Party.
173-
/// → If any authenticator returns an error status not equivalent to "InvalidStateError",
174-
/// Remove authenticator from issuedRequests.
175-
/// NOTE: This case does not imply user consent for the operation, so details about the error are hidden from the Relying Party in order to prevent leak of potentially identifying information. See § 14.5.1 Registration Ceremony Privacy for details.
176-
177-
try await attestRegistration(AttestationRegistrationRequest(
178-
options: options,
179-
publicKeyCredentialParameters: publicKeyCredentialParameters,
180-
clientDataHash: clientDataHash
181-
) { attestationObject in
182-
throw WebAuthnError.unsupported
183-
})
154+
/// Let the caller do what it needs to do to coordinate with authenticators, so long as at least one of them calls the attestation callback.
155+
let result: AttestationObject = try await withCancellableFirstSuccessfulContinuation { [attestRegistration, publicKeyCredentialParameters] continuation in
156+
/// → If lifetimeTimer expires,
157+
/// For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests.
158+
Task {
159+
/// Let the timer run in the background to cancel the continuation if it runs over.
160+
await timeoutTask.value
161+
continuation.cancel() // TODO: Should be a timeout error
162+
}
163+
164+
/// → If the user exercises a user agent user-interface option to cancel the process,
165+
/// For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests. Throw a "NotAllowedError" DOMException.
166+
// Implemented in catch statement below.
167+
168+
/// → If options.signal is present and aborted,
169+
/// For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests. Then throw the options.signal’s abort reason.
170+
// Skip.
171+
172+
/// → If an authenticator becomes available on this client device,
173+
/// See ``KeyPairAuthenticator/makeCredentials(with:)`` for full implementation
174+
/// → If an authenticator ceases to be available on this client device,
175+
/// Remove authenticator from issuedRequests.
176+
/// → If any authenticator returns a status indicating that the user cancelled the operation,
177+
/// 1. Remove authenticator from issuedRequests.
178+
/// 2. For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
179+
/// NOTE: Authenticators may return an indication of "the user cancelled the entire operation". How a user agent manifests this state to users is unspecified.
180+
// User can cancel the main task instead.
181+
182+
/// → If any authenticator returns an error status equivalent to "InvalidStateError",
183+
/// 1. Remove authenticator from issuedRequests.
184+
/// 2. For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
185+
/// 3. Throw an "InvalidStateError" DOMException.
186+
/// NOTE: This error status is handled separately because the authenticator returns it only if excludeCredentialDescriptorList identifies a credential bound to the authenticator and the user has consented to the operation. Given this explicit consent, it is acceptable for this case to be distinguishable to the Relying Party.
187+
// TODO: Need to catch this specific type of error
188+
/// → If any authenticator returns an error status not equivalent to "InvalidStateError",
189+
/// Remove authenticator from issuedRequests.
190+
/// NOTE: This case does not imply user consent for the operation, so details about the error are hidden from the Relying Party in order to prevent leak of potentially identifying information. See § 14.5.1 Registration Ceremony Privacy for details.
191+
192+
/// Kick off the attestation process, waiting for one to succeed before the timeout.
193+
try await attestRegistration(AttestationRegistrationRequest(
194+
options: options,
195+
publicKeyCredentialParameters: publicKeyCredentialParameters,
196+
clientDataHash: clientDataHash
197+
) { attestationObject in
198+
continuation.resume(returning: attestationObject)
199+
})
200+
}
184201

185202
/// → If any authenticator indicates success,
186203
/// 1. Remove authenticator from issuedRequests. This authenticator is now the selected authenticator.
@@ -234,7 +251,13 @@ public struct WebAuthnClient {
234251
} catch {
235252
/// Step 35. Throw a "NotAllowedError" DOMException. In order to prevent information leak that could identify the user without consent, this step MUST NOT be executed before lifetimeTimer has expired. See § 14.5.1 Registration Ceremony Privacy for details.
236253
/// During the above process, the user agent SHOULD show some UI to the user to guide them in the process of selecting and authorizing an authenticator.
237-
254+
await withTaskCancellationHandler {
255+
/// Make sure to wait until the timeout finishes if an error did occur.
256+
await timeoutTask.value
257+
} onCancel: {
258+
/// However, if the user cancelled the process, stop the timer early.
259+
timeoutTask.cancel()
260+
}
238261
/// Propagate the error originally thrown.
239262
throw error
240263
}

0 commit comments

Comments
 (0)