Skip to content

Commit 9335775

Browse files
Added assertion signing to authenticators
1 parent 9632a1d commit 9335775

File tree

3 files changed

+147
-2
lines changed

3 files changed

+147
-2
lines changed

Sources/WebAuthn/Authenticators/KeyPairAuthenticator.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public struct KeyPairAuthenticator: AuthenticatorProtocol, Sendable {
1919
public let attestationGloballyUniqueID: AAGUID
2020
public let attachmentModality: AuthenticatorAttachment
2121
public let supportedPublicKeyCredentialParameters: Set<PublicKeyCredentialParameters>
22+
23+
/// As the credentials are directly supplied by the caller, ``KeyPairAuthenticator``s are always capable of performing user verification, though they can be initialized to indicate silent authorization was performed if relevant.
2224
public let canPerformUserVerification: Bool = true
2325
public let canStoreCredentialSourceClientSide: Bool = true
2426

Sources/WebAuthn/Authenticators/Protocol/AuthenticatorProtocol.swift

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,145 @@ extension AuthenticatorProtocol {
345345
authenticationRequest: AssertionAuthenticationRequest,
346346
credentials: CredentialStore<Self>
347347
) async throws -> CredentialSource {
348-
throw WebAuthnError.unsupported
348+
/// [WebAuthn Level 3 Editor's Draft §5.1.4.2. Issuing a Credential Request to an Authenticator](https://w3c.github.io/webauthn/#sctn-issuing-cred-request-to-authenticator)
349+
/// Step 1. If pkOptions.userVerification is set to required and the authenticator is not capable of performing user verification, return false.
350+
if authenticationRequest.options.userVerification == .required && !canPerformUserVerification {
351+
throw WebAuthnError.requiredUserVerificationNotSupported
352+
}
353+
354+
/// Step 2. Let userVerification be the effective user verification requirement for assertion, a Boolean value, as follows. If pkOptions.userVerification
355+
let requestsUserVerification = switch authenticationRequest.options.userVerification {
356+
/// → is set to required
357+
/// Let userVerification be true.
358+
case .required: true
359+
/// → is set to preferred
360+
/// If the authenticator
361+
/// → is capable of user verification
362+
/// Let userVerification be true.
363+
/// → is not capable of user verification
364+
/// Let userVerification be false.
365+
case .preferred: canPerformUserVerification
366+
/// → is set to discouraged
367+
/// Let userVerification be false.
368+
case .discouraged: false
369+
/// Default to preferred case: [WebAuthn Level 3 Editor's Draft §5.5. Options for Assertion Generation (dictionary PublicKeyCredentialRequestOptions)](https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-userverification)
370+
default: canPerformUserVerification
371+
}
372+
373+
/// Step 8. If pkOptions.allowCredentials
374+
let allowedCredentialDescriptorList: [PublicKeyCredentialDescriptor] = if let allowCredentials = authenticationRequest.options.allowCredentials, !allowCredentials.isEmpty {
375+
/// → is not empty
376+
/// 1. Let allowCredentialDescriptorList be a new list.
377+
/// 2. Execute a client platform-specific procedure to determine which, if any, public key credentials described by pkOptions.allowCredentials are bound to this authenticator, by matching with rpId, pkOptions.allowCredentials.id, and pkOptions.allowCredentials.type. Set allowCredentialDescriptorList to this filtered list.
378+
/// 3. If allowCredentialDescriptorList is empty, return false.
379+
/// 4. Let distinctTransports be a new ordered set.
380+
/// 5. If allowCredentialDescriptorList has exactly one value, set savedCredentialIds[authenticator] to allowCredentialDescriptorList[0].id’s value (see here in § 6.3.3 The authenticatorGetAssertion Operation for more information).
381+
/// 6. For each credential descriptor C in allowCredentialDescriptorList, append each value, if any, of C.transports to distinctTransports.
382+
/// NOTE: This will aggregate only distinct values of transports (for this authenticator) in distinctTransports due to the properties of ordered sets.
383+
/// 7. If distinctTransports
384+
/// → is not empty
385+
/// The client selects one transport value from distinctTransports, possibly incorporating local configuration knowledge of the appropriate transport to use with authenticator in making its selection.
386+
/// Then, using transport, invoke the authenticatorGetAssertion operation on authenticator, with rpId, clientDataHash, allowCredentialDescriptorList, userVerification, and authenticatorExtensions as parameters.
387+
/// → is empty
388+
/// Using local configuration knowledge of the appropriate transport to use with authenticator, invoke the authenticatorGetAssertion operation on authenticator with rpId, clientDataHash, allowCredentialDescriptorList, userVerification, and authenticatorExtensions as parameters.
389+
filteredCredentialDescriptors(
390+
credentialDescriptors: allowCredentials,
391+
relyingPartyID: authenticationRequest.options.relyingPartyID
392+
)
393+
} else {
394+
/// → is empty
395+
/// Using local configuration knowledge of the appropriate transport to use with authenticator, invoke the authenticatorGetAssertion operation on authenticator with rpId, clientDataHash, userVerification, and authenticatorExtensions as parameters.
396+
/// NOTE: In this case, the Relying Party did not supply a list of acceptable credential descriptors. Thus, the authenticator is being asked to exercise any credential it may possess that is scoped to the Relying Party, as identified by rpId.
397+
[]
398+
}
399+
400+
/// Step 11. Return true.
401+
// Skip.
402+
403+
/// [WebAuthn Level 3 Editor's Draft §6.3.3. The authenticatorGetAssertion Operation](https://w3c.github.io/webauthn/#authenticatorgetassertion)
404+
/// Step 1. Check if all the supplied parameters are syntactically well-formed and of the correct length. If not, return an error code equivalent to "UnknownError" and terminate the operation.
405+
// Skip.
406+
407+
/// Step 2. Let credentialOptions be a new empty set of public key credential sources.
408+
/// Step 3. If allowCredentialDescriptorList was supplied, then for each descriptor of allowCredentialDescriptorList:
409+
/// 1. Let credSource be the result of looking up descriptor.id in this authenticator.
410+
/// 2. If credSource is not null, append it to credentialOptions.
411+
/// Step 4. Otherwise (allowCredentialDescriptorList was not supplied), for each key → credSource of this authenticator’s credentials map, append credSource to credentialOptions.
412+
var credentialOptions = if !allowedCredentialDescriptorList.isEmpty {
413+
allowedCredentialDescriptorList.compactMap { credentialDescriptor -> CredentialSource? in
414+
guard
415+
credentialDescriptor.type == .publicKey,
416+
let id = CredentialSource.ID(bytes: credentialDescriptor.id)
417+
else { return nil }
418+
419+
return credentials[id]
420+
}
421+
} else {
422+
Array(credentials.values)
423+
}
424+
425+
/// Step 5. Remove any items from credentialOptions whose rpId is not equal to rpId.
426+
credentialOptions.removeAll { $0.relyingPartyID != authenticationRequest.options.relyingPartyID }
427+
428+
/// Step 6. If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation.
429+
guard !credentialOptions.isEmpty
430+
else { throw WebAuthnError.noCredentialsAvailable }
431+
432+
/// Step 7. Prompt the user to select a public key credential source selectedCredential from credentialOptions. Collect an authorization gesture confirming user consent for using selectedCredential. The prompt for the authorization gesture may be shown by the authenticator if it has its own output capability, or by the user agent otherwise.
433+
/// If requireUserVerification is true, the authorization gesture MUST include user verification.
434+
/// If requireUserPresence is true, the authorization gesture MUST include a test of user presence.
435+
/// If the user does not consent, return an error code equivalent to "NotAllowedError" and terminate the operation.
436+
let selectedCredential = try await collectAuthorizationGesture(
437+
requiresUserVerification: requestsUserVerification,
438+
requiresUserPresence: true, // TODO: Make option
439+
credentialOptions: credentialOptions
440+
)
441+
442+
/// Step 8. Let processedExtensions be the result of authenticator extension processing for each supported extension identifier → authenticator extension input in extensions.
443+
// Skip.
444+
445+
/// Step 9. Increment the credential associated signature counter or the global signature counter value, depending on which approach is implemented by the authenticator, by some positive value. If the authenticator does not implement a signature counter, let the signature counter value remain constant at zero.
446+
// Done already in Step 7.
447+
448+
/// Step 10. Let authenticatorData be the byte array specified in § 6.1 Authenticator Data including processedExtensions, if any, as the extensions and excluding attestedCredentialData.
449+
let authenticatorData = AuthenticatorData(
450+
relyingPartyIDHash: SHA256.hash(data: Array(authenticationRequest.options.relyingPartyID.utf8)),
451+
flags: AuthenticatorFlags(
452+
userPresent: true,
453+
userVerified: true,
454+
isBackupEligible: true,
455+
isCurrentlyBackedUp: true,
456+
attestedCredentialData: false,
457+
extensionDataIncluded: false
458+
), // TODO: Add first four flags to credential source/collection gesture
459+
counter: 0 // TODO: Add to credential source requirement
460+
).bytes
461+
462+
/// Step 11. Let signature be the assertion signature of the concatenation authenticatorData || hash using the privateKey of selectedCredential as shown in Figure , below. A simple, undelimited concatenation is safe to use here because the authenticator data describes its own length. The hash of the serialized client data (which potentially has a variable length) is always the last element.
463+
/// Step 12. If any error occurred while generating the assertion signature, return an error code equivalent to "UnknownError" and terminate the operation.
464+
let signature = try await selectedCredential.signAssertion(
465+
authenticatorData: authenticatorData,
466+
clientDataHash: authenticationRequest.clientDataHash
467+
)
468+
469+
/// Step 13. Return to the user agent:
470+
/// selectedCredential.id, if either a list of credentials (i.e., allowCredentialDescriptorList) of length 2 or greater was supplied by the client, or no such list was supplied.
471+
/// NOTE: If, within allowCredentialDescriptorList, the client supplied exactly one credential and it was successfully employed, then its credential ID is not returned since the client already knows it. This saves transmitting these bytes over what may be a constrained connection in what is likely a common case.
472+
/// authenticatorData
473+
/// signature
474+
/// selectedCredential.userHandle
475+
/// NOTE: In cases where allowCredentialDescriptorList was supplied the returned userHandle value may be null, see: userHandleResult.
476+
try await authenticationRequest.attemptAuthentication.submitAssertionResults(
477+
credentialID: selectedCredential.id.bytes,
478+
authenticatorData: authenticatorData,
479+
signature: signature,
480+
userHandle: selectedCredential.userHandle,
481+
authenticatorAttachment: .platform // TODO: Make option
482+
)
483+
484+
/// If the authenticator cannot find any credential corresponding to the specified Relying Party that matches the specified criteria, it terminates the operation and returns an error.
485+
// Already done.
486+
487+
return selectedCredential
349488
}
350489
}

Sources/WebAuthn/WebAuthnError.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,15 @@ public struct WebAuthnError: Error, Hashable, Sendable {
6767
case invalidExponent
6868
case unsupportedCOSEAlgorithmForRSAPublicKey
6969
case unsupported
70-
70+
7171
// MARK: WebAuthnClient
7272
case noSupportedCredentialParameters
7373
case missingCredentialSourceDespiteSuccess
7474

7575
// MARK: Authenticator
7676
case unsupportedCredentialPublicKeyType
77+
case requiredUserVerificationNotSupported
78+
case noCredentialsAvailable
7779
case authorizationGestureNotAllowed
7880
}
7981

@@ -142,5 +144,7 @@ public struct WebAuthnError: Error, Hashable, Sendable {
142144

143145
// MARK: Authenticator
144146
public static let unsupportedCredentialPublicKeyType = Self(reason: .unsupportedCredentialPublicKeyType)
147+
public static let requiredUserVerificationNotSupported = Self(reason: .requiredUserVerificationNotSupported)
148+
public static let noCredentialsAvailable = Self(reason: .noCredentialsAvailable)
145149
public static let authorizationGestureNotAllowed = Self(reason: .authorizationGestureNotAllowed)
146150
}

0 commit comments

Comments
 (0)