Skip to content

Commit 179e0e3

Browse files
Added basic key management and signing procedures
1 parent 9c97bea commit 179e0e3

File tree

3 files changed

+281
-9
lines changed

3 files changed

+281
-9
lines changed

Sources/WebAuthn/Authenticators/KeyPairAuthenticator.swift

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16-
import Crypto
16+
@preconcurrency import Crypto
1717

1818
public struct KeyPairAuthenticator: AuthenticatorProtocol, Sendable {
1919
public let attestationGloballyUniqueID: AAGUID
@@ -25,8 +25,21 @@ public struct KeyPairAuthenticator: AuthenticatorProtocol, Sendable {
2525
/// The specific subset the client fully supports, in case more are added over time.
2626
static let implementedPublicKeyCredentialParameterSubset: Set<PublicKeyCredentialParameters> = [
2727
PublicKeyCredentialParameters(alg: .algES256),
28+
PublicKeyCredentialParameters(alg: .algES384),
29+
PublicKeyCredentialParameters(alg: .algES512),
2830
]
2931

32+
/// Generate credentials for the full subset the implementation supports.
33+
///
34+
/// This list must match those supported in ``KeyPairAuthenticator/implementedPublicKeyCredentialParameterSubset``.
35+
static func generateCredentialSourceKey(for chosenCredentialParameters: PublicKeyCredentialParameters) -> CredentialSource.Key {
36+
switch chosenCredentialParameters.alg {
37+
case .algES256: .es256(P256.Signing.PrivateKey(compactRepresentable: false))
38+
case .algES384: .es384(P384.Signing.PrivateKey(compactRepresentable: false))
39+
case .algES512: .es521(P521.Signing.PrivateKey(compactRepresentable: false))
40+
}
41+
}
42+
3043
/// Initialize a key-pair based authenticator with a globally unique ID representing your application.
3144
/// - Note: To generate an AAGUID, run `% uuidgen` in your terminal. This value should generally not change across installations or versions of your app, and should be the same for every user.
3245
/// - Parameter attestationGloballyUniqueID: The AAGUID associated with the authenticator.
@@ -48,7 +61,13 @@ public struct KeyPairAuthenticator: AuthenticatorProtocol, Sendable {
4861
relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID,
4962
userHandle: PublicKeyCredentialUserEntity.ID
5063
) async throws -> CredentialSource {
51-
throw WebAuthnError.unsupported
64+
CredentialSource(
65+
id: UUID(),
66+
key: Self.generateCredentialSourceKey(for: credentialParameters),
67+
relyingPartyID: relyingPartyID,
68+
userHandle: userHandle,
69+
counter: 0
70+
)
5271
}
5372

5473
public func filteredCredentialDescriptors(
@@ -72,17 +91,46 @@ public struct KeyPairAuthenticator: AuthenticatorProtocol, Sendable {
7291

7392
extension KeyPairAuthenticator {
7493
public struct CredentialSource: AuthenticatorCredentialSourceProtocol, Sendable {
94+
public enum Key: Sendable {
95+
case es256(P256.Signing.PrivateKey)
96+
case es384(P384.Signing.PrivateKey)
97+
case es521(P521.Signing.PrivateKey)
98+
}
99+
75100
public var id: UUID
101+
public var key: Key
76102
public var relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID
77103
public var userHandle: PublicKeyCredentialUserEntity.ID
78104
public var counter: UInt32
79105

80106
public var credentialParameters: PublicKeyCredentialParameters {
81-
PublicKeyCredentialParameters(alg: .algES256)
107+
switch key {
108+
case .es256: PublicKeyCredentialParameters(alg: .algES256)
109+
case .es384: PublicKeyCredentialParameters(alg: .algES384)
110+
case .es521: PublicKeyCredentialParameters(alg: .algES512)
111+
}
82112
}
83113

84114
public var rawKeyData: Data {
85-
Data()
115+
switch key {
116+
case .es256(let privateKey): privateKey.rawRepresentation
117+
case .es384(let privateKey): privateKey.rawRepresentation
118+
case .es521(let privateKey): privateKey.rawRepresentation
119+
}
120+
}
121+
122+
public init(
123+
id: ID,
124+
key: Key,
125+
relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID,
126+
userHandle: PublicKeyCredentialUserEntity.ID,
127+
counter: UInt32
128+
) {
129+
self.id = id
130+
self.key = key
131+
self.relyingPartyID = relyingPartyID
132+
self.userHandle = userHandle
133+
self.counter = 0
86134
}
87135

88136
public init(
@@ -97,6 +145,11 @@ extension KeyPairAuthenticator {
97145
else { throw WebAuthnError.unsupportedCredentialPublicKeyType }
98146

99147
self.id = id
148+
switch credentialParameters.alg {
149+
case .algES256: key = .es256(try P256.Signing.PrivateKey(rawRepresentation: rawKeyData))
150+
case .algES384: key = .es384(try P384.Signing.PrivateKey(rawRepresentation: rawKeyData))
151+
case .algES512: key = .es521(try P521.Signing.PrivateKey(rawRepresentation: rawKeyData))
152+
}
100153
self.relyingPartyID = relyingPartyID
101154
self.userHandle = userHandle
102155
self.counter = counter
@@ -106,7 +159,12 @@ extension KeyPairAuthenticator {
106159
authenticatorData: [UInt8],
107160
clientDataHash: SHA256Digest
108161
) throws -> [UInt8] {
109-
throw WebAuthnError.unsupported
162+
let digest = authenticatorData + clientDataHash
163+
return switch key {
164+
case .es256(let privateKey): Array(try privateKey.signature(for: digest).derRepresentation)
165+
case .es384(let privateKey): Array(try privateKey.signature(for: digest).derRepresentation)
166+
case .es521(let privateKey): Array(try privateKey.signature(for: digest).derRepresentation)
167+
}
110168
}
111169
}
112170
}

Sources/WebAuthn/Authenticators/Protocol/AuthenticatorProtocol.swift

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public protocol AuthenticatorProtocol<CredentialSource> {
6161
/// Make credentials for the specified registration request, returning the credential source that the caller should store for subsequent authentication.
6262
///
6363
/// - Important: Depending on the authenticator being used, the credential source may contain private keys, and must be stored sequirely, such as in the user's Keychain, or in a Hardware Security Module appropriate with the level of security you wish to secure your user's account with.
64+
/// - SeeAlso: [WebAuthn Level 3 Editor's Draft §5.1.3. Create a New Credential - PublicKeyCredential’s Create(origin, options, sameOriginWithAncestors) Method, Step 25.]( https://w3c.github.io/webauthn/#CreateCred-async-loop)
65+
/// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.3.2. The authenticatorMakeCredential Operation](https://w3c.github.io/webauthn/#sctn-op-make-cred)
6466
func makeCredentials(with registration: AttestationRegistrationRequest) async throws -> CredentialSource
6567

6668
/// Filter the provided credential descriptors to determine which, if any, should be handled by this authenticator.
@@ -124,9 +126,136 @@ extension AuthenticatorProtocol {
124126
public func makeCredentials(
125127
with registration: AttestationRegistrationRequest
126128
) async throws -> CredentialSource {
129+
/// See [WebAuthn Level 3 Editor's Draft §5.1.3. Create a New Credential - PublicKeyCredential’s Create(origin, options, sameOriginWithAncestors) Method, Step 25.]( https://w3c.github.io/webauthn/#CreateCred-async-loop)
130+
/// Step 1. This authenticator is now the candidate authenticator.
131+
/// Step 2. If pkOptions.authenticatorSelection is present:
132+
/// 1. If pkOptions.authenticatorSelection.authenticatorAttachment is present and its value is not equal to authenticator’s authenticator attachment modality, continue.
133+
/// 2. If pkOptions.authenticatorSelection.residentKey
134+
/// → is present and set to required
135+
/// If the authenticator is not capable of storing a client-side discoverable public key credential source, continue.
136+
/// → is present and set to preferred or discouraged
137+
/// No effect.
138+
/// → is not present
139+
/// if pkOptions.authenticatorSelection.requireResidentKey is set to true and the authenticator is not capable of storing a client-side discoverable public key credential source, continue.
140+
/// 6. If pkOptions.authenticatorSelection.userVerification is set to required and the authenticator is not capable of performing user verification, continue.
141+
// Skip.
142+
143+
/// Step 3. Let requireResidentKey be the effective resident key requirement for credential creation, a Boolean value, as follows:
144+
/// If pkOptions.authenticatorSelection.residentKey
145+
/// → is present and set to required
146+
/// Let requireResidentKey be true.
147+
/// → is present and set to preferred
148+
/// If the authenticator
149+
/// → is capable of client-side credential storage modality
150+
/// Let requireResidentKey be true.
151+
/// → is not capable of client-side credential storage modality, or if the client cannot determine authenticator capability,
152+
/// Let requireResidentKey be false.
153+
/// → is present and set to discouraged
154+
/// Let requireResidentKey be false.
155+
/// → is not present
156+
/// Let requireResidentKey be the value of pkOptions.authenticatorSelection.requireResidentKey.
157+
let requiresClientSideKeyStorage = false
158+
159+
/// Step 10. Let userVerification be the effective user verification requirement for credential creation, a Boolean value, as follows. If pkOptions.authenticatorSelection.userVerification
160+
/// → is set to required
161+
/// Let userVerification be true.
162+
/// → is set to preferred
163+
/// If the authenticator
164+
/// → is capable of user verification
165+
/// Let userVerification be true.
166+
/// → is not capable of user verification
167+
/// Let userVerification be false.
168+
/// → is set to discouraged
169+
/// Let userVerification be false.
170+
let shouldPerformUserVerification = false
171+
172+
/// Step 16. Let enterpriseAttestationPossible be a Boolean value, as follows. If pkOptions.attestation
173+
/// → is set to enterprise
174+
/// Let enterpriseAttestationPossible be true if the user agent wishes to support enterprise attestation for pkOptions.rp.id (see Step 8, above). Otherwise false.
175+
/// → otherwise
176+
/// Let enterpriseAttestationPossible be false.
177+
let isEnterpriseAttestationPossible = false
178+
179+
/// Step 19. Let attestationFormats be a list of strings, initialized to the value of pkOptions.attestationFormats.
180+
/// Step 20. If pkOptions.attestation
181+
/// → is set to none
182+
/// Set attestationFormats be the single-element list containing the string “none”
183+
guard case .none = registration.options.attestation else { throw WebAuthnError.attestationFormatNotSupported }
184+
185+
/// Step 22. Let excludeCredentialDescriptorList be a new list.
186+
/// Step 23. For each credential descriptor C in pkOptions.excludeCredentials:
187+
/// 1. If C.transports is not empty, and authenticator is connected over a transport not mentioned in C.transports, the client MAY continue.
188+
/// 2. Otherwise, Append C to excludeCredentialDescriptorList.
189+
/// 3. Invoke the authenticatorMakeCredential operation on authenticator with clientDataHash, pkOptions.rp, pkOptions.user, requireResidentKey, userVerification, credTypesAndPubKeyAlgs, excludeCredentialDescriptorList, enterpriseAttestationPossible, attestationFormats, and authenticatorExtensions as parameters.
190+
/// Step 24. Append authenticator to issuedRequests.
191+
192+
/// See [WebAuthn Level 3 Editor's Draft §6.3.2. The authenticatorMakeCredential Operation](https://w3c.github.io/webauthn/#sctn-op-make-cred)
193+
/// 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.
194+
/// Step 2. Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation.
127195
guard let chosenCredentialParameters = registration.publicKeyCredentialParameters.first(where: supportedPublicKeyCredentialParameters.contains(_:))
128196
else { throw WebAuthnError.noSupportedCredentialParameters }
129197

130-
throw WebAuthnError.unsupported
198+
/// Step 3. For each descriptor of excludeCredentialDescriptorList:
199+
/// 1. If looking up descriptor.id in this authenticator returns non-null, and the returned item's RP ID and type match rpEntity.id and excludeCredentialDescriptorList.type respectively, then collect an authorization gesture confirming user consent for creating a new credential. The authorization gesture MUST include a test of user presence. If the user
200+
/// → confirms consent to create a new credential
201+
/// return an error code equivalent to "InvalidStateError" and terminate the operation.
202+
/// → does not consent to create a new credential
203+
/// return an error code equivalent to "NotAllowedError" and terminate the operation.
204+
/// NOTE: The purpose of this authorization gesture is not to proceed with creating a credential, but for privacy reasons to authorize disclosure of the fact that descriptor.id is bound to this authenticator. If the user consents, the client and Relying Party can detect this and guide the user to use a different authenticator. If the user does not consent, the authenticator does not reveal that descriptor.id is bound to it, and responds as if the user simply declined consent to create a credential.
205+
/// Step 4. If requireResidentKey is true and the authenticator cannot store a client-side discoverable public key credential source, return an error code equivalent to "ConstraintError" and terminate the operation.
206+
/// Step 5. If requireUserVerification is true and the authenticator cannot perform user verification, return an error code equivalent to "ConstraintError" and terminate the operation.
207+
/// Step 6. Collect an authorization gesture confirming user consent for creating a new credential. The prompt for the authorization gesture is shown by the authenticator if it has its own output capability, or by the user agent otherwise. The prompt SHOULD display rpEntity.id, rpEntity.name, userEntity.name and userEntity.displayName, if possible.
208+
/// → If requireUserVerification is true, the authorization gesture MUST include user verification.
209+
/// → If requireUserPresence is true, the authorization gesture MUST include a test of user presence.
210+
/// → If the user does not consent or if user verification fails, return an error code equivalent to "NotAllowedError" and terminate the operation.
211+
/// Step 7. Once the authorization gesture has been completed and user consent has been obtained, generate a new credential object:
212+
/// 1. Let (publicKey, privateKey) be a new pair of cryptographic keys using the combination of PublicKeyCredentialType and cryptographic parameters represented by the first item in credTypesAndPubKeyAlgs that is supported by this authenticator.
213+
/// 2. Let userHandle be userEntity.id.
214+
/// 3. Let credentialSource be a new public key credential source with the fields:
215+
/// type
216+
/// public-key.
217+
/// privateKey
218+
/// privateKey
219+
/// rpId
220+
/// rpEntity.id
221+
/// userHandle
222+
/// userHandle
223+
/// otherUI
224+
/// Any other information the authenticator chooses to include.
225+
/// 4. If requireResidentKey is true or the authenticator chooses to create a client-side discoverable public key credential source:
226+
/// 1. Let credentialId be a new credential id.
227+
/// 2. Set credentialSource.id to credentialId.
228+
/// 3. Let credentials be this authenticator’s credentials map.
229+
/// 4. Set credentials[(rpEntity.id, userHandle)] to credentialSource.
230+
/// 5. Otherwise:
231+
/// Let credentialId be the result of serializing and encrypting credentialSource so that only this authenticator can decrypt it.
232+
let credentialSource = try await generateCredentialSource(
233+
requiresClientSideKeyStorage: requiresClientSideKeyStorage, credentialParameters: chosenCredentialParameters,
234+
relyingPartyID: registration.options.relyingParty.id, userHandle: registration.options.user.id
235+
)
236+
237+
/// Step 8. If any error occurred while creating the new credential object, return an error code equivalent to "UnknownError" and terminate the operation.
238+
/// Step 9. Let processedExtensions be the result of authenticator extension processing for each supported extension identifier → authenticator extension input in extensions.
239+
/// Step 10. If the authenticator:
240+
/// → is a U2F device
241+
/// let the signature counter value for the new credential be zero. (U2F devices may support signature counters but do not return a counter when making a credential. See [FIDO-U2F-Message-Formats].)
242+
/// → supports a global signature counter
243+
/// Use the global signature counter's actual value when generating authenticator data.
244+
/// → supports a per credential signature counter
245+
/// allocate the counter, associate it with the new credential, and initialize the counter value as zero.
246+
/// → does not support a signature counter
247+
/// let the signature counter value for the new credential be constant at zero.
248+
/// Step 15. Let attestedCredentialData be the attested credential data byte array including the credentialId and publicKey.
249+
/// Step 16. Let attestationFormat be the first supported attestation statement format identifier from attestationFormats, taking into account enterpriseAttestationPossible. If attestationFormats contains no supported value, then let attestationFormat be the attestation statement format identifier most preferred by this authenticator.
250+
/// Step 17. Let authenticatorData be the byte array specified in § 6.1 Authenticator Data, including attestedCredentialData as the attestedCredentialData and processedExtensions, if any, as the extensions.
251+
/// Step 18. Create an attestation object for the new credential using the procedure specified in § 6.5.4 Generating an Attestation Object, the attestation statement format attestationFormat, and the values authenticatorData and hash, as well as taking into account the value of enterpriseAttestationPossible. For more details on attestation, see § 6.5 Attestation.
252+
/// On successful completion of this operation, the authenticator returns the attestation object to the client.
253+
// try await registration.attemptRegistration.submitAttestationObject(
254+
// attestationFormat: <#T##AttestationFormat#>,
255+
// authenticatorData: <#T##AuthenticatorData#>,
256+
// attestationStatement: <#T##CBOR#>
257+
// )
258+
259+
return credentialSource
131260
}
132261
}

0 commit comments

Comments
 (0)