-
Notifications
You must be signed in to change notification settings - Fork 29
/
Copy pathAuthenticatorAssertionResponse.swift
141 lines (120 loc) · 6.24 KB
/
AuthenticatorAssertionResponse.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift WebAuthn open source project
//
// Copyright (c) 2022 the Swift WebAuthn project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Foundation
import Crypto
/// This is what the authenticator device returned after we requested it to authenticate a user.
///
/// When decoding using `Decodable`, byte arrays are decoded from base64url to bytes.
public struct AuthenticatorAssertionResponse: Sendable {
/// Representation of what we passed to `navigator.credentials.get()`
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let clientDataJSON: [UInt8]
/// Contains the authenticator data returned by the authenticator.
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let authenticatorData: [UInt8]
/// Contains the raw signature returned from the authenticator
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let signature: [UInt8]
/// Contains the user handle returned from the authenticator, or null if the authenticator did not return
/// a user handle. Used by to give scope to credentials.
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let userHandle: [UInt8]?
/// Contains an attestation object, if the authenticator supports attestation in assertions.
/// The attestation object, if present, includes an attestation statement. Unlike the attestationObject
/// in an AuthenticatorAttestationResponse, it does not contain an authData key because the authenticator
/// data is provided directly in an AuthenticatorAssertionResponse structure.
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let attestationObject: [UInt8]?
}
extension AuthenticatorAssertionResponse: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
authenticatorData = try container.decodeBytesFromURLEncodedBase64(forKey: .authenticatorData)
signature = try container.decodeBytesFromURLEncodedBase64(forKey: .signature)
userHandle = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .userHandle)
attestationObject = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .attestationObject)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(clientDataJSON.base64URLEncodedString(), forKey: .clientDataJSON)
try container.encode(authenticatorData.base64URLEncodedString(), forKey: .authenticatorData)
try container.encode(signature.base64URLEncodedString(), forKey: .signature)
try container.encodeIfPresent(userHandle?.base64URLEncodedString(),forKey: .userHandle)
try container.encodeIfPresent(attestationObject?.base64URLEncodedString(),forKey: .attestationObject)
}
private enum CodingKeys: String, CodingKey {
case clientDataJSON
case authenticatorData
case signature
case userHandle
case attestationObject
}
}
struct ParsedAuthenticatorAssertionResponse: Sendable {
let rawClientData: [UInt8]
let clientData: CollectedClientData
let rawAuthenticatorData: [UInt8]
let authenticatorData: AuthenticatorData
let signature: URLEncodedBase64
let userHandle: [UInt8]?
init(from authenticatorAssertionResponse: AuthenticatorAssertionResponse) throws {
rawClientData = authenticatorAssertionResponse.clientDataJSON
clientData = try JSONDecoder().decode(CollectedClientData.self, from: Data(rawClientData))
rawAuthenticatorData = authenticatorAssertionResponse.authenticatorData
authenticatorData = try AuthenticatorData(bytes: rawAuthenticatorData)
signature = authenticatorAssertionResponse.signature.base64URLEncodedString()
userHandle = authenticatorAssertionResponse.userHandle
}
// swiftlint:disable:next function_parameter_count
func verify(
expectedChallenge: [UInt8],
relyingPartyOrigin: String,
relyingPartyID: String,
requireUserVerification: Bool,
credentialPublicKey: [UInt8],
credentialCurrentSignCount: UInt32
) throws {
try clientData.verify(
storedChallenge: expectedChallenge,
ceremonyType: .assert,
relyingPartyOrigin: relyingPartyOrigin
)
let expectedRelyingPartyIDData = Data(relyingPartyID.utf8)
let expectedRelyingPartyIDHash = SHA256.hash(data: expectedRelyingPartyIDData)
guard expectedRelyingPartyIDHash == authenticatorData.relyingPartyIDHash else {
throw WebAuthnError.relyingPartyIDHashDoesNotMatch
}
guard authenticatorData.flags.userPresent else { throw WebAuthnError.userPresentFlagNotSet }
if requireUserVerification {
guard authenticatorData.flags.userVerified else { throw WebAuthnError.userVerifiedFlagNotSet }
}
if authenticatorData.counter > 0 || credentialCurrentSignCount > 0 {
guard authenticatorData.counter > credentialCurrentSignCount else {
// This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential
// private key may exist and are being used in parallel.
throw WebAuthnError.potentialReplayAttack
}
}
let clientDataHash = SHA256.hash(data: rawClientData)
let signatureBase = rawAuthenticatorData + clientDataHash
let credentialPublicKey = try CredentialPublicKey(publicKeyBytes: credentialPublicKey)
guard let signatureData = signature.urlDecoded.decoded else { throw WebAuthnError.invalidSignature }
try credentialPublicKey.verify(signature: signatureData, data: signatureBase)
}
}