Skip to content

Commit a6779d3

Browse files
Implemented remaining client registration procedure
1 parent 4c6515f commit a6779d3

File tree

7 files changed

+107
-67
lines changed

7 files changed

+107
-67
lines changed

Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import Crypto
1818

1919
/// Contains the cryptographic attestation that a new key pair was created by that authenticator.
2020
public struct AttestationObject: Sendable {
21-
let authenticatorData: AuthenticatorData
22-
let rawAuthenticatorData: [UInt8]
23-
let format: AttestationFormat
24-
let attestationStatement: CBOR
21+
var authenticatorData: AuthenticatorData
22+
var rawAuthenticatorData: [UInt8]
23+
var format: AttestationFormat
24+
var attestationStatement: CBOR
2525

2626
init(
2727
authenticatorData: AuthenticatorData,
@@ -45,6 +45,37 @@ public struct AttestationObject: Sendable {
4545
self.format = format
4646
self.attestationStatement = attestationStatement
4747
}
48+
49+
init(bytes: [UInt8]) throws {
50+
guard let decodedAttestationObject = try? CBOR.decode(bytes, options: CBOROptions(maximumDepth: 16))
51+
else { throw WebAuthnError.invalidAttestationObject }
52+
53+
guard
54+
let authData = decodedAttestationObject["authData"],
55+
case let .byteString(authDataBytes) = authData
56+
else { throw WebAuthnError.invalidAuthData }
57+
self.authenticatorData = try AuthenticatorData(bytes: authDataBytes)
58+
self.rawAuthenticatorData = authDataBytes
59+
60+
guard
61+
let formatCBOR = decodedAttestationObject["fmt"],
62+
case let .utf8String(format) = formatCBOR,
63+
let attestationFormat = AttestationFormat(rawValue: format)
64+
else { throw WebAuthnError.invalidFmt }
65+
self.format = attestationFormat
66+
67+
guard let attestationStatement = decodedAttestationObject["attStmt"]
68+
else { throw WebAuthnError.missingAttStmt }
69+
self.attestationStatement = attestationStatement
70+
}
71+
72+
var bytes: [UInt8] {
73+
CBOR.encode([
74+
"authData": CBOR.byteString(authenticatorData.bytes),
75+
"fmt": CBOR.utf8String(format.rawValue),
76+
"attStmt": attestationStatement,
77+
])
78+
}
4879

4980
func verify(
5081
relyingPartyID: String,

Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift

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

1515
// Contains the new public key created by the authenticator.
1616
public struct AttestedCredentialData: Equatable, Sendable {
17-
let authenticatorAttestationGUID: AAGUID
18-
let credentialID: [UInt8]
19-
let publicKey: [UInt8]
17+
var authenticatorAttestationGUID: AAGUID
18+
var credentialID: [UInt8]
19+
var publicKey: [UInt8]
2020
}

Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -55,30 +55,6 @@ struct ParsedAuthenticatorAttestationResponse {
5555
self.clientData = clientData
5656

5757
// Step 11. (assembling attestationObject)
58-
let attestationObjectData = Data(rawResponse.attestationObject)
59-
guard let decodedAttestationObject = try? CBOR.decode([UInt8](attestationObjectData), options: CBOROptions(maximumDepth: 16)) else {
60-
throw WebAuthnError.invalidAttestationObject
61-
}
62-
63-
guard let authData = decodedAttestationObject["authData"],
64-
case let .byteString(authDataBytes) = authData else {
65-
throw WebAuthnError.invalidAuthData
66-
}
67-
guard let formatCBOR = decodedAttestationObject["fmt"],
68-
case let .utf8String(format) = formatCBOR,
69-
let attestationFormat = AttestationFormat(rawValue: format) else {
70-
throw WebAuthnError.invalidFmt
71-
}
72-
73-
guard let attestationStatement = decodedAttestationObject["attStmt"] else {
74-
throw WebAuthnError.missingAttStmt
75-
}
76-
77-
attestationObject = AttestationObject(
78-
authenticatorData: try AuthenticatorData(bytes: authDataBytes),
79-
rawAuthenticatorData: authDataBytes,
80-
format: attestationFormat,
81-
attestationStatement: attestationStatement
82-
)
58+
attestationObject = try AttestationObject(bytes: rawResponse.attestationObject)
8359
}
8460
}

Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ public struct RegistrationCredential: Sendable {
3030

3131
/// The attestation response from the authenticator.
3232
public let attestationResponse: AuthenticatorAttestationResponse
33+
34+
init(
35+
type: CredentialType = .publicKey,
36+
id: [UInt8],
37+
attestationResponse: AuthenticatorAttestationResponse
38+
) {
39+
self.id = id.base64URLEncodedString()
40+
self.type = type
41+
self.rawID = id
42+
self.attestationResponse = attestationResponse
43+
}
3344
}
3445

3546
extension RegistrationCredential: Decodable {

Sources/WebAuthn/WebAuthnClient.swift

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public struct WebAuthnClient {
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 {
154154
/// 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
155+
var attestationObjectResult: AttestationObject = try await withCancellableFirstSuccessfulContinuation { [attestRegistration, publicKeyCredentialParameters] continuation in
156156
/// → If lifetimeTimer expires,
157157
/// For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests.
158158
Task {
@@ -213,41 +213,66 @@ public struct WebAuthnClient {
213213
/// whose value is an AuthenticationExtensionsClientOutputs object containing extension identifier → client extension output entries. The entries are created by running each extension’s client extension processing algorithm to create the client extension outputs, for each client extension in pkOptions.extensions.
214214
/// 3. Let constructCredentialAlg be an algorithm that takes a global object global, and whose steps are:
215215
/// 1. If credentialCreationData.attestationConveyancePreferenceOption’s value is
216-
/// → none
217-
/// Replace potentially uniquely identifying information with non-identifying versions of the same:
218-
/// 1. If the aaguid in the attested credential data is 16 zero bytes, credentialCreationData.attestationObjectResult.fmt is "packed", and "x5c" is absent from credentialCreationData.attestationObjectResult, then self attestation is being used and no further action is needed.
219-
/// 2. Otherwise
220-
/// 1. Replace the aaguid in the attested credential data with 16 zero bytes.
221-
/// 2. Set the value of credentialCreationData.attestationObjectResult.fmt to "none", and set the value of credentialCreationData.attestationObjectResult.attStmt to be an empty CBOR map. (See § 8.7 None Attestation Statement Format and § 6.5.4 Generating an Attestation Object).
222-
/// → indirect
223-
/// The client MAY replace the aaguid and attestation statement with a more privacy-friendly and/or more easily verifiable version of the same data (for example, by employing an Anonymization CA).
224-
/// → direct or enterprise
225-
/// Convey the authenticator's AAGUID and attestation statement, unaltered, to the Relying Party.
216+
switch options.attestation {
217+
/// → none
218+
case .none:
219+
/// Replace potentially uniquely identifying information with non-identifying versions of the same:
220+
/// 1. If the aaguid in the attested credential data is 16 zero bytes, credentialCreationData.attestationObjectResult.fmt is "packed", and "x5c" is absent from credentialCreationData.attestationObjectResult, then self attestation is being used and no further action is needed.
221+
/// 2. Otherwise
222+
if attestationObjectResult.authenticatorData.attestedData?.authenticatorAttestationGUID != .anonymous,
223+
attestationObjectResult.format != .packed,
224+
attestationObjectResult.attestationStatement["x5c"] == nil {
225+
/// 1. Replace the aaguid in the attested credential data with 16 zero bytes.
226+
attestationObjectResult.authenticatorData.attestedData?.authenticatorAttestationGUID = .anonymous
227+
/// 2. Set the value of credentialCreationData.attestationObjectResult.fmt to "none", and set the value of credentialCreationData.attestationObjectResult.attStmt to be an empty CBOR map. (See § 8.7 None Attestation Statement Format and § 6.5.4 Generating an Attestation Object).
228+
attestationObjectResult.format = .none
229+
attestationObjectResult.attestationStatement = [:]
230+
}
231+
/// → indirect
232+
/// The client MAY replace the aaguid and attestation statement with a more privacy-friendly and/or more easily verifiable version of the same data (for example, by employing an Anonymization CA).
233+
/// → direct or enterprise
234+
/// Convey the authenticator's AAGUID and attestation statement, unaltered, to the Relying Party.
235+
}
226236
/// 5. Let attestationObject be a new ArrayBuffer, created using global’s %ArrayBuffer%, containing the bytes of credentialCreationData.attestationObjectResult’s value.
237+
let attestationObject = attestationObjectResult.bytes
238+
227239
/// 6. Let id be attestationObject.authData.attestedCredentialData.credentialId.
240+
guard let credentialID = attestationObjectResult.authenticatorData.attestedData?.credentialID
241+
else { throw WebAuthnError.attestedCredentialDataMissing }
242+
228243
/// 7. Let pubKeyCred be a new PublicKeyCredential object associated with global whose fields are:
229-
/// [[identifier]]
230-
/// id
231-
/// authenticatorAttachment
232-
/// The AuthenticatorAttachment value matching the current authenticator attachment modality of authenticator.
233-
/// response
234-
/// A new AuthenticatorAttestationResponse object associated with global whose fields are:
235-
/// clientDataJSON
236-
/// A new ArrayBuffer, created using global’s %ArrayBuffer%, containing the bytes of credentialCreationData.clientDataJSONResult.
237-
/// attestationObject
238-
/// attestationObject
239-
/// [[transports]]
240-
/// A sequence of zero or more unique DOMStrings, in lexicographical order, that the authenticator is believed to support. The values SHOULD be members of AuthenticatorTransport, but client platforms MUST ignore unknown values.
241-
/// If a user agent does not wish to divulge this information it MAY substitute an arbitrary sequence designed to preserve privacy. This sequence MUST still be valid, i.e. lexicographically sorted and free of duplicates. For example, it may use the empty sequence. Either way, in this case the user agent takes the risk that Relying Party behavior may be suboptimal.
242-
/// If the user agent does not have any transport information, it SHOULD set this field to the empty sequence.
243-
/// NOTE: How user agents discover transports supported by a given authenticator is outside the scope of this specification, but may include information from an attestation certificate (for example [FIDO-Transports-Ext]), metadata communicated in an authenticator protocol such as CTAP2, or special-case knowledge about a platform authenticator.
244-
/// [[clientExtensionsResults]]
245-
/// A new ArrayBuffer, created using global’s %ArrayBuffer%, containing the bytes of credentialCreationData.clientExtensionResults.
244+
let publicKeyCredential = RegistrationCredential(
245+
/// [[identifier]]
246+
/// id
247+
id: credentialID,
248+
/// authenticatorAttachment
249+
/// The AuthenticatorAttachment value matching the current authenticator attachment modality of authenticator.
250+
/// response
251+
/// A new AuthenticatorAttestationResponse object associated with global whose fields are:
252+
attestationResponse: AuthenticatorAttestationResponse(
253+
/// clientDataJSON
254+
/// A new ArrayBuffer, created using global’s %ArrayBuffer%, containing the bytes of credentialCreationData.clientDataJSONResult.
255+
clientDataJSON: Array(clientDataJSON),
256+
/// attestationObject
257+
/// attestationObject
258+
attestationObject: attestationObject
259+
/// [[transports]]
260+
/// A sequence of zero or more unique DOMStrings, in lexicographical order, that the authenticator is believed to support. The values SHOULD be members of AuthenticatorTransport, but client platforms MUST ignore unknown values.
261+
/// If a user agent does not wish to divulge this information it MAY substitute an arbitrary sequence designed to preserve privacy. This sequence MUST still be valid, i.e. lexicographically sorted and free of duplicates. For example, it may use the empty sequence. Either way, in this case the user agent takes the risk that Relying Party behavior may be suboptimal.
262+
/// If the user agent does not have any transport information, it SHOULD set this field to the empty sequence.
263+
/// NOTE: How user agents discover transports supported by a given authenticator is outside the scope of this specification, but may include information from an attestation certificate (for example [FIDO-Transports-Ext]), metadata communicated in an authenticator protocol such as CTAP2, or special-case knowledge about a platform authenticator.
264+
)
265+
/// [[clientExtensionsResults]]
266+
/// A new ArrayBuffer, created using global’s %ArrayBuffer%, containing the bytes of credentialCreationData.clientExtensionResults.
267+
)
246268
/// 8. Return pubKeyCred.
269+
// Returned below.
270+
247271
/// 4. For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
248-
/// 5. Return constructCredentialAlg and terminate this algorithm.
272+
// Already performed.
249273

250-
throw WebAuthnError.unsupported
274+
/// 5. Return constructCredentialAlg and terminate this algorithm.
275+
return publicKeyCredential
251276
} catch {
252277
/// 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.
253278
/// 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.

Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ final class WebAuthnManagerIntegrationTests: XCTestCase {
6464
).build().cborEncoded
6565

6666
let registrationResponse = RegistrationCredential(
67-
id: mockCredentialID.base64URLEncodedString(),
68-
type: .publicKey,
69-
rawID: mockCredentialID,
67+
id: mockCredentialID,
7068
attestationResponse: AuthenticatorAttestationResponse(
7169
clientDataJSON: mockClientDataJSON.jsonBytes,
7270
attestationObject: mockAttestationObject

Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,8 @@ final class WebAuthnManagerRegistrationTests: XCTestCase {
375375
try await webAuthnManager.finishRegistration(
376376
challenge: challenge,
377377
credentialCreationData: RegistrationCredential(
378-
id: rawID.base64URLEncodedString(),
379378
type: type,
380-
rawID: rawID,
379+
id: rawID,
381380
attestationResponse: AuthenticatorAttestationResponse(
382381
clientDataJSON: clientDataJSON,
383382
attestationObject: attestationObject

0 commit comments

Comments
 (0)