-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathGetResponse.php
More file actions
182 lines (153 loc) · 5.71 KB
/
Copy pathGetResponse.php
File metadata and controls
182 lines (153 loc) · 5.71 KB
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<?php
declare(strict_types=1);
namespace Firehed\WebAuthn;
use UnexpectedValueException;
/**
* This is the internal representation of a PublicKeyCredential containing an
* AuthenticatorAssertionResponse; i.e. the result of calling
* `navigator.credentials.get()`.
*
* @internal
*/
class GetResponse implements Responses\AssertionInterface
{
private AuthenticatorData $authData;
public function __construct(
private BinaryString $credentialId,
private BinaryString $rawAuthenticatorData,
private BinaryString $clientDataJson,
private BinaryString $signature,
) {
$this->authData = AuthenticatorData::parse($this->rawAuthenticatorData);
}
/**
* @internal
*/
public function getUsedCredentialId(): BinaryString
{
return $this->credentialId;
}
/**
* @see 7.2
* @link https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion
*/
public function verify(
ChallengeManagerInterface $challenge,
RelyingParty $rp,
CredentialContainer | CredentialInterface $credential,
UserVerificationRequirement $uv = UserVerificationRequirement::Preferred,
): CredentialInterface {
// 7.2.1-7.2.4 are done in client side js & the ResponseParser
// 7.2.5
// if allowCredentials != [], assert this.id in allowCredetials
// 7.2.6
// id the auth'd user & verify this.id is in their credentials
// if user came from existing data (session/cookie/etc),
// a) verify that the user owns the passed credential
// b) if this.userHandle is set, check for a match
// if not (e.g. passkeys ~ medication:conditional)
// a) verify that this.userHandle is present & matches owner
//
// upstream implication:
// - try to load user by userHandle
// - get their credentials & perform existing matching procedure
// 7.2.7
// get credential from JS credential.id
// (index into possible credential list?)
// $credential is this value.
if ($credential instanceof CredentialContainer) {
$credential = $credential->findCredentialUsedByResponse($this);
if ($credential === null) {
$this->fail('7.2.7', 'Credential not found in container');
}
}
// 7.2.8
$cData = $this->clientDataJson->unwrap();
$authData = $this->authData;
$sig = $this->signature->unwrap();
// 7.2.9
$JSONtext = $cData; // already utf8
// 7.2.10
$C = json_decode($JSONtext, true);
if (!is_array($C)) {
throw new Errors\ParseError('7.2.10', 'JSON decoding returned the wrong format');
}
// 7.2.11
if ($C['type'] !== 'webauthn.get') {
$this->fail('7.2.11', 'C.type');
}
// 7.2.12
$cdjChallenge = $C['challenge'];
$challenge = $challenge->useFromClientDataJSON($cdjChallenge);
if ($challenge === null) {
$this->fail('7.2.12', 'C.challenge');
}
$b64u = Codecs\Base64Url::encode($challenge->getBinary()->unwrap());
if (!hash_equals($b64u, $cdjChallenge)) {
$this->fail('7.2.12', 'C.challenge');
}
// 7.2.13
if (!hash_equals($rp->getOrigin(), $C['origin'])) {
$this->fail('7.2.13', 'C.origin');
}
// 7.2.14
// TODO: tokenBinding (may not exist on localhost??)
// 7.2.15
if (!$rp->permitsRpIdHash($authData)) {
$this->fail('7.2.15', 'authData.rpIdHash');
}
// 7.2.16
if (!$authData->isUserPresent()) {
$this->fail('7.2.16', 'authData.isUserPresent');
}
// 7.2.17
$isUserVerificationRequired = ($uv === UserVerificationRequirement::Required);
if ($isUserVerificationRequired && !$authData->isUserVerified()) {
$this->fail('7.2.17', 'authData.isUserVerified');
}
// 7.2.18
// TODO: clientExtensionResults / options.extensions
// 7.2.19
$hash = hash('sha256', $cData, true);
// 7.2.20
$credentialPublicKey = $credential->getPublicKey();
// Spec note: the signature is over the concatenation of the authData
// and the hash of clientDataJSON. Due to the above checks (relying
// party id, challenge, origin, etc) contained within those data, this
// sig check ensures a login attempt for *this site* with the known
// server-generated challenge has been signed by a key already registed
// to the user.
$verificationData = sprintf(
'%s%s',
$this->rawAuthenticatorData->unwrap(),
$hash,
);
$result = openssl_verify(
$verificationData,
$sig,
$credentialPublicKey->getPemFormatted(),
\OPENSSL_ALGO_SHA256,
);
if ($result !== 1) {
$this->fail('7.2.20', 'Signature verification');
}
// 7.2.21
$storedSignCount = $credential->getSignCount();
if ($authData->getSignCount() !== 0 || $storedSignCount !== 0) {
if ($authData->getSignCount() > $storedSignCount) {
$credential = $credential->withUpdatedSignCount($authData->getSignCount());
} else {
// FIXME: throw/alert for risk
}
}
// 7.2.22
// var_dump(__METHOD__, __LINE__, $authData);
// Send back the (updated?) credential so that the sign counter can be
// updated.
return $credential;
}
private function fail(string $section, string $desc): never
{
throw new Errors\VerificationError($section, $desc);
}
}