From 3d084c20a0a8753b8a6184490bff03810fc8a72d Mon Sep 17 00:00:00 2001 From: Joride Date: Mon, 19 Aug 2024 12:33:55 +0200 Subject: [PATCH 1/5] - expanded `PublicKeyCredentialCreationOptions` with more options from the spec - updated implementation of `beginRegistration(..)` supply milliseconds to a new initializer N.b. `beginRegistration(..)` now returns different fields (and so different JSON). --- .../PublicKeyCredentialCreationOptions.swift | 104 +++++++++++++++++- Sources/WebAuthn/WebAuthnManager.swift | 2 +- 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index f08556e6..91305f40 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -21,6 +21,30 @@ import Foundation /// /// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { + + init(challenge: [UInt8], + user: PublicKeyCredentialUserEntity, + relyingParty: PublicKeyCredentialRelyingPartyEntity, + publicKeyCredentialParameters: [PublicKeyCredentialParameters], + timeout: Int64?, + attestation: AttestationConveyancePreference, + hints: [Hint] = [], + extensions: Extensions = .init(credProps: true), + excludeCredentials: [Credentials] = [], + authenticatorSelection: AuthenticatorSelection = .init(residentKey: .preferred, + requireResidentKey: false, + userVerification: .preferred)){ + self.challenge = challenge + self.user = user + self.relyingParty = relyingParty + self.publicKeyCredentialParameters = publicKeyCredentialParameters + self.timeoutInMilliseconds = timeout + self.attestation = attestation + self.hints = hints + self.extensions = extensions + self.excludeCredentials = excludeCredentials + self.authenticatorSelection = authenticatorSelection + } /// A byte array randomly generated by the Relying Party. Should be at least 16 bytes long to ensure sufficient /// entropy. /// @@ -38,11 +62,9 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { /// preferred. public let publicKeyCredentialParameters: [PublicKeyCredentialParameters] - /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a + /// A time, in milliseconds, that the caller is willing to wait for the call to complete. This is treated as a /// hint, and may be overridden by the client. - /// - /// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``. - public let timeout: Duration? + public let timeoutInMilliseconds: Int64? /// Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is /// supported. @@ -55,8 +77,74 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { try container.encode(user, forKey: .user) try container.encode(relyingParty, forKey: .relyingParty) try container.encode(publicKeyCredentialParameters, forKey: .publicKeyCredentialParameters) - try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) + try container.encodeIfPresent(timeoutInMilliseconds, forKey: .timeoutInMilliseconds) try container.encode(attestation, forKey: .attestation) + try container.encode(authenticatorSelection, forKey: .authenticatorSelection) + try container.encode(hints, forKey: .hints) + try container.encode(extensions, forKey: .extensions) + try container.encode(excludeCredentials, forKey: .excludeCredentials) + } + + let hints: [Hint] + enum Hint: String, Encodable + { + /// Hint to get the user to register their platform authenticator + case clientDevice = "client-device" + + /// Hint to the user should be guided to register a security key. Iconography and text should emphasize the use of security keys + case securityKey = "security-key" + + /// Hint to the user to to register a passkey using their mobile device by scanning a QR code that’s displayed on a computer + case hybrid = "hybrid" + } + + let extensions: Extensions + + struct Extensions: Encodable + { + let credProps: Bool + } + + let excludeCredentials: [Credentials] + struct Credentials: Encodable + { + let id: String + let type: [UInt8] + + private enum CodingKeys: String, CodingKey + { + case id + case type + } + + public func encode(to encoder: Encoder) throws + { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type.base64URLEncodedString(), forKey: .id) + try container.encode(id, forKey: .id) + } + } + + let authenticatorSelection: AuthenticatorSelection + struct AuthenticatorSelection: Encodable + { + enum ResidentKey: String, Encodable + { + case required = "required" + case preferred = "preferred" + case discouraged = "discouraged" + } + + enum UserVerification: String, Encodable + { + case required = "required" + case preferred = "preferred" + case discouraged = "discouraged" + } + let residentKey: ResidentKey + let requireResidentKey: Bool + let userVerification: UserVerification } private enum CodingKeys: String, CodingKey { @@ -64,8 +152,12 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { case user case relyingParty = "rp" case publicKeyCredentialParameters = "pubKeyCredParams" - case timeout + case timeoutInMilliseconds = "timeout" case attestation + case authenticatorSelection + case hints + case extensions + case excludeCredentials } } diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index bb350895..ed73878b 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -69,7 +69,7 @@ public struct WebAuthnManager: Sendable { user: user, relyingParty: .init(id: configuration.relyingPartyID, name: configuration.relyingPartyName), publicKeyCredentialParameters: publicKeyCredentialParameters, - timeout: timeout, + timeout: timeout?.milliseconds, attestation: attestation ) } From b103adb3e2b81f7404ae4353f6c545d51197de6d Mon Sep 17 00:00:00 2001 From: Joride Date: Mon, 19 Aug 2024 16:08:59 +0200 Subject: [PATCH 2/5] changed parameter from representing ms to representing seconds, in line with a suggestion from dimitribouniol: https://github.com/swift-server/webauthn-swift/pull/76/files/3d084c20a0a8753b8a6184490bff03810fc8a72d#r1721784750 --- .../Registration/PublicKeyCredentialCreationOptions.swift | 8 ++++---- Sources/WebAuthn/WebAuthnManager.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index 91305f40..12047044 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -38,7 +38,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { self.user = user self.relyingParty = relyingParty self.publicKeyCredentialParameters = publicKeyCredentialParameters - self.timeoutInMilliseconds = timeout + self.timeout = timeout self.attestation = attestation self.hints = hints self.extensions = extensions @@ -62,9 +62,9 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { /// preferred. public let publicKeyCredentialParameters: [PublicKeyCredentialParameters] - /// A time, in milliseconds, that the caller is willing to wait for the call to complete. This is treated as a + /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a /// hint, and may be overridden by the client. - public let timeoutInMilliseconds: Int64? + public let timeout: Int64? /// Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is /// supported. @@ -77,7 +77,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { try container.encode(user, forKey: .user) try container.encode(relyingParty, forKey: .relyingParty) try container.encode(publicKeyCredentialParameters, forKey: .publicKeyCredentialParameters) - try container.encodeIfPresent(timeoutInMilliseconds, forKey: .timeoutInMilliseconds) + try container.encodeIfPresent(timeout, forKey: .timeoutInMilliseconds) try container.encode(attestation, forKey: .attestation) try container.encode(authenticatorSelection, forKey: .authenticatorSelection) try container.encode(hints, forKey: .hints) diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index ed73878b..7251150d 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -69,7 +69,7 @@ public struct WebAuthnManager: Sendable { user: user, relyingParty: .init(id: configuration.relyingPartyID, name: configuration.relyingPartyName), publicKeyCredentialParameters: publicKeyCredentialParameters, - timeout: timeout?.milliseconds, + timeout: (timeout?.milliseconds ?? 0) / 1000, attestation: attestation ) } From ee000536baa6801d50b9c4a1242f43a9be1d71f1 Mon Sep 17 00:00:00 2001 From: Joride Date: Thu, 22 Aug 2024 15:43:43 +0200 Subject: [PATCH 3/5] reverting to using seconds on `PublicKeyCredentialCreationOptions`' for timeout, of type `Duration` Follow up on this discussion: https://github.com/swift-server/webauthn-swift/pull/76#discussion_r1721940603 --- .../Registration/PublicKeyCredentialCreationOptions.swift | 8 ++++---- Sources/WebAuthn/WebAuthnManager.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index 12047044..a4ce8d7a 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -26,7 +26,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { user: PublicKeyCredentialUserEntity, relyingParty: PublicKeyCredentialRelyingPartyEntity, publicKeyCredentialParameters: [PublicKeyCredentialParameters], - timeout: Int64?, + timeout: Duration?, attestation: AttestationConveyancePreference, hints: [Hint] = [], extensions: Extensions = .init(credProps: true), @@ -64,7 +64,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a /// hint, and may be overridden by the client. - public let timeout: Int64? + public let timeout: Duration? /// Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is /// supported. @@ -77,7 +77,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { try container.encode(user, forKey: .user) try container.encode(relyingParty, forKey: .relyingParty) try container.encode(publicKeyCredentialParameters, forKey: .publicKeyCredentialParameters) - try container.encodeIfPresent(timeout, forKey: .timeoutInMilliseconds) + try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) try container.encode(attestation, forKey: .attestation) try container.encode(authenticatorSelection, forKey: .authenticatorSelection) try container.encode(hints, forKey: .hints) @@ -152,7 +152,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { case user case relyingParty = "rp" case publicKeyCredentialParameters = "pubKeyCredParams" - case timeoutInMilliseconds = "timeout" + case timeout case attestation case authenticatorSelection case hints diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index 7251150d..bb350895 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -69,7 +69,7 @@ public struct WebAuthnManager: Sendable { user: user, relyingParty: .init(id: configuration.relyingPartyID, name: configuration.relyingPartyName), publicKeyCredentialParameters: publicKeyCredentialParameters, - timeout: (timeout?.milliseconds ?? 0) / 1000, + timeout: timeout, attestation: attestation ) } From 3a2815e1dd3bdd07bbbb57c311eac272da05bb75 Mon Sep 17 00:00:00 2001 From: Joride Date: Thu, 22 Aug 2024 16:08:49 +0200 Subject: [PATCH 4/5] removed custom initializer from `PublicKeyCredentialCreationOptions`, moved defaults to `beginRegistration(..)` on `WebAuthnManager` see per suggestion: https://github.com/swift-server/webauthn-swift/pull/76#discussion_r1721803173 --- .../PublicKeyCredentialCreationOptions.swift | 23 ------------------- Sources/WebAuthn/WebAuthnManager.swift | 8 ++++++- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index a4ce8d7a..36842abb 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -22,29 +22,6 @@ import Foundation /// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { - init(challenge: [UInt8], - user: PublicKeyCredentialUserEntity, - relyingParty: PublicKeyCredentialRelyingPartyEntity, - publicKeyCredentialParameters: [PublicKeyCredentialParameters], - timeout: Duration?, - attestation: AttestationConveyancePreference, - hints: [Hint] = [], - extensions: Extensions = .init(credProps: true), - excludeCredentials: [Credentials] = [], - authenticatorSelection: AuthenticatorSelection = .init(residentKey: .preferred, - requireResidentKey: false, - userVerification: .preferred)){ - self.challenge = challenge - self.user = user - self.relyingParty = relyingParty - self.publicKeyCredentialParameters = publicKeyCredentialParameters - self.timeout = timeout - self.attestation = attestation - self.hints = hints - self.extensions = extensions - self.excludeCredentials = excludeCredentials - self.authenticatorSelection = authenticatorSelection - } /// A byte array randomly generated by the Relying Party. Should be at least 16 bytes long to ensure sufficient /// entropy. /// diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index bb350895..c93f1970 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -70,7 +70,13 @@ public struct WebAuthnManager: Sendable { relyingParty: .init(id: configuration.relyingPartyID, name: configuration.relyingPartyName), publicKeyCredentialParameters: publicKeyCredentialParameters, timeout: timeout, - attestation: attestation + attestation: attestation, + hints: [], + extensions: .init(credProps: true), + excludeCredentials: [], + authenticatorSelection: .init(residentKey: .preferred, + requireResidentKey: false, + userVerification: .preferred) ) } From 5f8974df64c68188297a6841e77df797bb11ed70 Mon Sep 17 00:00:00 2001 From: Joride Date: Sat, 24 Aug 2024 11:35:24 +0200 Subject: [PATCH 5/5] - dedicated public types for new types - removed some types that turned out to be redundant with existing ones - switched from using enums to using UnreferencedStringEnumeration addresses these discussion points https://github.com/swift-server/webauthn-swift/pull/76#discussion_r1721799400 https://github.com/swift-server/webauthn-swift/pull/76#discussion_r1721792482 https://github.com/swift-server/webauthn-swift/pull/76#discussion_r1721794102 --- .../PublicKeyCredentialCreationOptions.swift | 135 ++++++++++-------- 1 file changed, 73 insertions(+), 62 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index 36842abb..de3e2fa6 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -46,6 +46,22 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { /// Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is /// supported. public let attestation: AttestationConveyancePreference + + /// Use this enumeration to communicate hints to the user-agent about how a request may be best completed. These hints are not requirements, and do not bind the user-agent, but may guide it in providing the best experience by using contextual information that the Relying Party has about the request. + /// https://www.w3.org/TR/webauthn-3/#enum-hints + let hints: [Hint] + + /// This client registration extension facilitates reporting certain credential properties known by the client to the requesting WebAuthn Relying Party upon creation of a public key credential source as a result of a registration ceremony. + /// https://www.w3.org/TR/webauthn-2/#sctn-extensions-inputs-outputs + let extensions: Extensions + + /// This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator + /// https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialcreationoptions-excludecredentials + let excludeCredentials: [PublicKeyCredentialDescriptor] + + /// This member is intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the create() operation. + /// https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection + let authenticatorSelection: AuthenticatorSelectionCriteria public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -61,68 +77,6 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { try container.encode(extensions, forKey: .extensions) try container.encode(excludeCredentials, forKey: .excludeCredentials) } - - let hints: [Hint] - enum Hint: String, Encodable - { - /// Hint to get the user to register their platform authenticator - case clientDevice = "client-device" - - /// Hint to the user should be guided to register a security key. Iconography and text should emphasize the use of security keys - case securityKey = "security-key" - - /// Hint to the user to to register a passkey using their mobile device by scanning a QR code that’s displayed on a computer - case hybrid = "hybrid" - } - - let extensions: Extensions - - struct Extensions: Encodable - { - let credProps: Bool - } - - let excludeCredentials: [Credentials] - struct Credentials: Encodable - { - let id: String - let type: [UInt8] - - private enum CodingKeys: String, CodingKey - { - case id - case type - } - - public func encode(to encoder: Encoder) throws - { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(type.base64URLEncodedString(), forKey: .id) - try container.encode(id, forKey: .id) - } - } - - let authenticatorSelection: AuthenticatorSelection - struct AuthenticatorSelection: Encodable - { - enum ResidentKey: String, Encodable - { - case required = "required" - case preferred = "preferred" - case discouraged = "discouraged" - } - - enum UserVerification: String, Encodable - { - case required = "required" - case preferred = "preferred" - case discouraged = "discouraged" - } - let residentKey: ResidentKey - let requireResidentKey: Bool - let userVerification: UserVerification - } private enum CodingKeys: String, CodingKey { case challenge @@ -226,3 +180,60 @@ public struct PublicKeyCredentialUserEntity: Encodable, Sendable { case displayName } } + +public struct Hint: UnreferencedStringEnumeration, Sendable { + public var rawValue: String + public init(_ rawValue: String) { + self.rawValue = rawValue + } + + /// Indicates that the Relying Party believes that users will satisfy this request with a platform authenticator attached to the client device. + public static let clientDevice: Self = "client-device" + + /// Indicates that the Relying Party believes that users will satisfy this request with a physical security key + public static let securityKey: Self = "security-key" + + /// Indicates that the Relying Party believes that users will satisfy this request with general-purpose authenticators such as smartphones. + public static let hybrid: Self = "hybrid" +} + + +struct Extensions: Encodable { + /// Indicate that this extension is requested by the Relying Party. + /// https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension + let credProps: Bool +} + +struct AuthenticatorSelectionCriteria: Encodable { + /// Specifies the extent to which the Relying Party desires to create a client-side discoverable credential. + /// https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey + let residentKey: ResidentKeyRequirement + + /// Relying Parties SHOULD set this to true if, and only if, `residentKey` is set to `required`. + /// https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-requireresidentkey + let requireResidentKey: Bool + + /// This member specifies the Relying Party's requirements regarding user verification for the create() operation. + /// https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification + let userVerification: UserVerificationRequirement +} + + +public struct ResidentKeyRequirement: UnreferencedStringEnumeration, Sendable { + public var rawValue: String + public init(_ rawValue: String) { + self.rawValue = rawValue + } + + /// The Relying Party requires a client-side discoverable credential. + /// https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-required + public static let required: Self = "required" + + /// The Relying Party strongly prefers creating a client-side discoverable credential, but will accept a server-side credential. + /// https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred + public static let preferred: Self = "preferred" + + /// The Relying Party prefers creating a server-side credential, but will accept a client-side discoverable credential. + /// https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-discouraged + public static let discouraged: Self = "discouraged" +}